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++ / ++xx-- / --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 atomic 比对数组的关键速度要慢得多

如果已经定义了宏_OPENMP,它是一个int类型的十进制数。编写一个程序打印它的值,这个值的意义是啥?

在 macOS Catalina 上链接 OpenMP 时出现“未定义符号 _main”

“ImportError:导入 _openmp_helpers 时 DLL 加载失败”?

Import Error: _openmp_helps :找不到指定的模块