OpenMP 原子 _mm_add_pd
Posted
技术标签:
【中文标题】OpenMP 原子 _mm_add_pd【英文标题】:OpenMP atomic _mm_add_pd 【发布时间】:2012-12-23 21:32:25 【问题描述】:我正在尝试使用 OpenMP 将已经矢量化的代码与内在函数并行化,但问题是我使用一个 XMM 寄存器作为外部“变量”,我会增加每个循环。现在我使用shared
子句
__m128d xmm0 = _mm_setzero_pd();
__declspec(align(16)) double res[2];
#pragma omp parallel for shared(xmm0)
for (int i = 0; i < len; i++)
__m128d xmm7 = ... result of some operations
xmm0 = _mm_add_pd(xmm0, xmm7);
_mm_store_pd(res, xmm0);
double final_result = res[0] + res[1];
因为不支持atomic
操作(在VS2010中)
__m128d xmm0 = _mm_setzero_pd();
__declspec(align(16)) double res[2];
#pragma omp parallel for
for (int i = 0; i < len; i++)
__m128d xmm7 = ... result of some operations
#pragma omp atomic
xmm0 = _mm_add_pd(xmm0, xmm7);
_mm_store_pd(res, xmm0);
double final_result = res[0] + res[1];
有谁知道一个聪明的解决方法?
编辑:我刚刚也尝试过使用并行模式库:
__declspec(align(16)) double res[2];
combinable<__m128d> xmm0_comb([]()return _mm_setzero_pd(););
parallel_for(0, len, 1, [&xmm0_comb, ...](int i)
__m128d xmm7 = ... result of some operations
__m128d& xmm0 = xmm0_comb.local();
xmm0 = _mm_add_pd(xmm0, xmm7);
);
__m128d xmm0 = xmm0_comb.combine([](__m128d a, __m128d b)return _mm_add_pd(a, b););
_mm_store_pd(res, xmm0);
double final_result = res[0] + res[1];
但结果令人失望。
【问题讨论】:
我认为没有任何原子 SSE 算术指令。因此,我认为获得所需内容的唯一方法是将其包装在关键部分中。对性能不利?是的。你最好使用适当的归约算法。 @Mysticial:适当的归约算法是什么意思? 并行缩减。你本质上是在问一个 XY 问题。您想使用原子添加来解决您的问题。正确的解决方案是使用减少。 OpenMP 通过reduction()
指令支持它。您将无法直接在 SSE 操作数上执行此操作,但您可以先将其缩减为 double
。
顺便说一句,VS 不支持 atomic
的原因之一是它只实现了 10 年的 OpenMP 2.0 标准,它只允许像 x binop= y
这样的语句、x++ / ++x
和 x-- / --x
出现在 atomic
构造中。
【参考方案1】:
您以错误的方式解决问题。您应该使用归约而不是原子操作:
这是一个更好的方法:
double sum = 0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < len; i++)
__m128d xmm7;// = ... result of some operations
// Collapse to a "double".
_declspec(align(16)) double res[2];
_mm_store_pd(res, xmm7);
// Add to reduction variable.
sum += res[0] + res[1];
double final_result = sum;
归约本质上是一种使用关联操作(例如+
)将所有内容“归约”为单个变量的操作。
如果您要进行缩减,请始终尝试使用实际的缩减方法。不要试图用原子操作或关键部分来欺骗它。
这样做的原因是原子/关键部分方法本质上是不可扩展的,因为它们保持长的关键路径数据依赖性。适当的归约方法可将这条关键路径归为log(# of threads)
。
当然,唯一的缺点是它破坏了浮点关联性。如果这很重要,那么您基本上会坚持按顺序总结每次迭代。
【讨论】:
太好了,它已经比我以前的方法更快了,但我对循环内的崩溃并不完全满意。我也会尝试一些其他的东西:) 有很多方法可以做到这一点。我只在循环内折叠它,因为 AFAIK,OpenMP 缩减只支持原始类型。虽然我承认我没有尝试过在类上使用运算符重载。 我希望我在问这个问题之前已经阅读了这个答案***.com/questions/16551307/… 你有任何参考资料来讨论“适当的减少”是如何完成的吗? @raxman 我没有任何想法,但想法很简单。线性减少(将它们逐一添加)具有O(n)
关键路径。树约简,将元素集分解为子集并独立地求和。然后对结果进行总结。当递归完成时,它有一个O(log(n))
关键路径。
@Mysticial,谢谢,我现在看到我关于使用原子减少 OpenMP 的假设可能不正确。我应该尽可能坚持使用 OpenMP 缩减(而不是试图作弊)。【参考方案2】:
您正在寻找的是减少。如果您的编译器支持它(gcc 支持),您可以将其作为 omp 减少来执行,或者您可以通过为每个线程汇总到一个私有 xmm 来自己滚动一个。下面是一个简单的版本:
#include <emmintrin.h>
#include <omp.h>
#include <stdio.h>
int main(int argc, char **argv)
const int NTHREADS=8;
const int len=100;
__m128d xmm0[NTHREADS];
__m128d xmmreduction = _mm_setzero_pd();
#pragma omp parallel for num_threads(NTHREADS)
for (int i=0; i<NTHREADS; i++)
xmm0[i]= _mm_setzero_pd();
__attribute((aligned(16))) double res[2];
#pragma omp parallel num_threads(NTHREADS) reduction(+:xmmreduction)
int tid = omp_get_thread_num();
#pragma omp for
for (int i = 0; i < len; i++)
double d = (double)i;
__m128d xmm7 = _mm_set_pd( d, 2.*d );
xmm0[tid] = _mm_add_pd(xmm0[tid], xmm7);
xmmreduction = _mm_add_pd(xmmreduction, xmm7);
for (int i=1; i<NTHREADS; i++)
xmm0[0] = _mm_add_pd(xmm0[0], xmm0[i]);
_mm_store_pd(res, xmm0[0]);
double final_result = res[0] + res[1];
printf("Expected result = %f\n", 3.0*(len-1)*(len)/2);
printf("Calculated result = %lf\n", final_result);
_mm_store_pd(res, xmmreduction);
final_result = res[0] + res[1];
printf("Calculated result (reduction) = %lf\n", final_result);
return 0;
【讨论】:
+1,虽然我只是想确认 VC++ 不支持“非标量”类型的 OpenMP 缩减。如果 GCC 有,那么这将使它成为一个有用的 GCC 扩展。【参考方案3】:在回答我问题的人的大力帮助下,我想出了这个:
double final_result = 0.0;
#pragma omp parallel reduction(+:final_result)
__declspec(align(16)) double r[2];
__m128d xmm0 = _mm_setzero_pd();
#pragma omp for
for (int i = 0; i < len; i++)
__m128d xmm7 = ... result of some operations
xmm0 = _mm_add_pd(xmm0, xmm7);
_mm_store_pd(r, xmm0);
final_result += r[0] + r[1];
它基本上把collapse和reduction分开了,性能很好。
非常感谢所有帮助过我的人!
【讨论】:
一个小评论。我认为您可以通过使用“pragma omp for nowait”移除 for 循环上的障碍来提高代码效率【参考方案4】:我猜你不能将你自己的内在函数添加到编译器中,并且 MS 编译器决定跳过内联汇编器。不确定是否有一个简单的解决方案。
【讨论】:
以上是关于OpenMP 原子 _mm_add_pd的主要内容,如果未能解决你的问题,请参考以下文章
Helgrind (Valgrind) 和 OpenMP (C):避免误报?
如果已经定义了宏_OPENMP,它是一个int类型的十进制数。编写一个程序打印它的值,这个值的意义是啥?
在 macOS Catalina 上链接 OpenMP 时出现“未定义符号 _main”