使用 SSE 矢量化在 OpenMP 中将内部循环与残差计算并行化

Posted

技术标签:

【中文标题】使用 SSE 矢量化在 OpenMP 中将内部循环与残差计算并行化【英文标题】:Parallelizing inner loop with residual calculations in OpenMP with SSE vectorization 【发布时间】:2021-03-30 11:08:25 【问题描述】:

我正在尝试并行化一个程序的内部循环,该程序具有循环范围之外的数据依赖性(最小)。我遇到了一个问题,即残差计算发生在内 j 循环范围之外。如果 j 循环中包含“#pragma omp parallel”部分,即使由于 k 值太低而导致循环根本没有运行,代码也会出错。比如说 (1,2,3)。

for (i = 0; i < 10; i++)
  
    #pragma omp parallel for shared(min) private (j, a, b, storer, arr) //
    for (j = 0; j < k-4; j += 4)
    
      mm_a = _mm_load_ps(&x[j]);
      mm_b = _mm_load_ps(&y[j]);
      mm_a = _mm_add_ps(mm_a, mm_b);
      _mm_store_ps(storer, mm_a);

      #pragma omp critical
      
      if (storer[0] < min)
      
        min = storer[0];
      
      if (storer[1] < min)
      
        min = storer[1];
      
      //etc
      
    
    do
    
        #pragma omp critical
        
        if (x[j]+y[j] < min)
        
          min = x[j]+y[j];
            
         
      
     while (j++ < (k - 1));
    round_min = min
  

【问题讨论】:

您能否更具体地说明您遇到了什么错误?这些是编译时错误还是您得到错误的结果?我的第一个想法是你在min 变量上有一个竞争条件,所以显而易见的解决方案是reduction(min:min) 子句,你必须将它添加到parallel for 指令中。 您应该只使用_mm_min_ps 来获得4 个最小值的向量,并在最后将其减少为一个元素(实际上,由于延迟,您需要多个寄存器),而不是所有分支。这可能有重复。 不过,您需要发布一个实际的minimal reproducible example(没有//etc cmets,也没有使用未声明的变量)。外循环实际上是做什么用的? xy 改变了吗? 【参考方案1】:

基于j 的循环是一个并行循环,因此您不能在循环之后使用j。尤其如此,因为您明确地将 j 设置为 private,因此仅在线程中本地可见,但在并行区域之外不可见。您可以在并行循环之后使用(k-4+3)/4*4 显式计算剩余j 值的位置。

此外,这里有几个要点:

您可能真的不需要自己矢量化代码:您可以使用omp simd reduction。 OpenMP 可以自动为您完成计算残差计算的所有枯燥工作。此外,代码将是可移植的并且更简单。生成的代码也可能比你的更快。但请注意,某些编译器可能无法对代码进行矢量化(GCC 和 ICC 可以,而 Clang 和 MSVC 通常需要一些帮助)。 关键部分 (omp critical) 非常昂贵。在您的情况下,这只会消除与并行部分相关的任何可能的改进。由于缓存行弹跳,代码可能会变慢。 在此处读取_mm_store_ps 写入的数据效率低效,尽管某些编译器(如 GCC)可能能够理解您的代码逻辑并生成更快的实现(提取车道数据)。李> 水平 SIMD 减少效率低下。使用速度更快且在此处易于使用的垂直模式。

考虑到以上几点,这是一个更正的代码:

for (i = 0; i < 10; i++)

    // Assume min is already initialized correctly here

    #pragma omp parallel for simd reduction(min:min) private(j)
    for (j = 0; j < k; ++j)
    
        const float tmp = x[j] + y[j];
        if(tmp < min)
            min = tmp;
    

    // Use min here

上述代码在 GCC/ICC(均使用 -O3 -fopenmp)、Clang(使用 -O3 -fopenmp -ffastmath)和 MSVC(使用 /O2 /fp:precise -openmp:experimental)上的 x86 架构上正确矢量化。

【讨论】:

您也许可以使用 min = min&lt;tmp ? min : tmp 将编译器手持到更好的 asm 中,以确保他们看到它是无分支的,并且(没有 AVX)让它使用更新累加器寄存器的操作数顺序就地,保存movaps (What is the instruction that gives branchless FP min and max on x86?)。即使没有-ffast-math,也可能会得到很好的结果。 OpenMP 缩减可能会使快速数学变得多余 嗯,即使在没有 openMP godbolt.org/z/d5a6Ma9va 的情况下使用 -ffast-math 时,该三元组实际上也击败了 clang 的自动矢量化。 (这 是必要的;min() 实现与 NaN 或有符号零都没有关联。)我简化了外部循环,因此它只是将 1024 个浮点元素减少为一次标量,因此我们可以查看该 asm . 使用 OpenMP,clang 只是并行化,而不是矢量化。 (只是minss,没有minps)godbolt.org/z/s1YzqPq46。即使只使用-O3 -fopenmp,没有-ffast-math,GCC 也会进行矢量化,但这不会让 GCC 使用多个累加器展开以隐藏 MINPS 延迟。 :/ 这对于 GCC 的标准自动矢量化是正常的(没有配置文件引导优化 -fprofile-use),但人们可能希望它的 OpenMP 矢量化器更具侵略性。 是的,确实,Clang 的行为很奇怪(MSVC 也是如此)。这就是我编辑答案以用条件替换三元的原因。我不知道为什么 Clang 自动矢量化器无法识别基于最小值的减少。这似乎是一个错误或内部限制。我很想知道更多关于他为什么会这样的行为。 我猜可能对 OpenMP 的支持有限/不太成熟?并行化外循环并让正常的自动矢量化在内循环上工作可能会很好,这取决于它对局部性的影响。尽管我猜想对于缓存阻塞,您确实希望每个核心都重复接触相同的数据子集,而不是让每个核心循环遍历所有数据。

以上是关于使用 SSE 矢量化在 OpenMP 中将内部循环与残差计算并行化的主要内容,如果未能解决你的问题,请参考以下文章

矢量化代码中随机减速的原因

OpenMP 与向量化的比较

在 OpenMP 中并行化嵌套循环并使用更多线程执行内部循环

SSE/AVX + OpenMP:数组的快速求和

使用 SSE、AVX 和 OpenMP 进行快速内存转置

使用 Vector<T> 的带有 SIMD 的矢量化 C# 代码运行速度比经典循环慢