SSE SIMD优化循环

6

我有一些在循环中的代码

for(int i = 0; i < n; i++)
{
  u[i] = c * u[i] + s * b[i];
}

所以,u和b是同样长度的向量,c和s是标量。这段代码是否适合使用SSE进行向量化以获得加速?

更新

我学习了向量化(如果使用内部函数则并不难),并在SSE中实现了我的循环。然而,在VC++编译器中设置SSE2标志时,我得到的性能与我的SSE代码相当。另一方面,英特尔编译器比我的SSE代码或VC++编译器快得多。

以下是我编写的参考代码

double *u = (double*) _aligned_malloc(n * sizeof(double), 16);
for(int i = 0; i < n; i++)
{
   u[i] = 0;
}

int j = 0;
__m128d *uSSE = (__m128d*) u;
__m128d cStore = _mm_set1_pd(c);
__m128d sStore = _mm_set1_pd(s);
for (j = 0; j <= i - 2; j+=2)
{
  __m128d uStore = _mm_set_pd(u[j+1], u[j]);

  __m128d cu = _mm_mul_pd(cStore, uStore);
  __m128d so = _mm_mul_pd(sStore, omegaStore);

  uSSE[j/2] = _mm_add_pd(cu, so);
}
for(; j <= i; ++j)
{
  u[j] = c * u[j] + s * omegaCache[j];
}

注意:VC11现在在其优化中使用SIMD。 (http://blogs.microsoft.co.il/blogs/sasha/archive/2011/10/17/simd-optimized-c-code-in-visual-studio-11.aspx) - bobobobo
5个回答

5

是的,这是矢量化的绝佳候选。但在此之前,确保已对代码进行了分析,以确保真正值得优化。即使如此,矢量化也应该按照以下方式进行:

int i;
for(i = 0; i < n - 3; i += 4)
{
  load elements u[i,i+1,i+2,i+3]
  load elements b[i,i+1,i+2,i+3]
  vector multiply u * c
  vector multiply s * b
  add partial results
  store back to u[i,i+1,i+2,i+3]
}

// Finish up the uneven edge cases (or skip if you know n is a multiple of 4)
for( ; i < n; i++)
  u[i] = c * u[i] + s * b[i];

为了获得更好的性能,您可以考虑预取更多的数组元素,并/或展开循环并使用软件流水线将一个循环中的计算与来自不同迭代的内存访问交错。


我肯定发现这段代码是瓶颈。我有一个问题想要确认一下,学习和实现向量化不是徒劳无功 - 编译器通常不会自动将这样的代码向量化,对吗? - Projectile Fish
1
@Projectile 如果你告诉编译器关于别名的问题,通常它会处理好。根据我的经验,如果没有付出非常大的努力,很少能够生成比编译器更好的代码。 - Anycorn

2

_mm_set_pd 没有向量化。如果字面意思理解,它会使用标量操作读取两个双精度浮点数,然后将这两个标量双精度浮点数组合并入SSE寄存器中。请使用_mm_load_pd代替。


1

是的,假设U和B数组没有重叠,这是矢量化的不错选择。但代码受内存访问(读写)约束。矢量化有助于减少循环周期,但指令将由于U和B数组的缓存未命中而停顿。Intel C/C++编译器使用Xeon x5500处理器的默认标志生成以下代码。编译器通过8次展开循环并使用xmm[0-16] SIMD寄存器的ADD(addpd)和MULTIPLY(mulpd)指令。在每个周期中,处理器可以发出2个SIMD指令,产生4路标量ILP,假设您已经在寄存器中准备好数据。

这里的U、B、C和S都是双精度(8字节)。

    ..B1.14:                        # Preds ..B1.12 ..B1.10
    movaps    %xmm1, %xmm3                                  #5.1
    unpcklpd  %xmm3, %xmm3                                  #5.1
    movaps    %xmm0, %xmm2                                  #6.12
    unpcklpd  %xmm2, %xmm2                                  #6.12
      # LOE rax rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm0 xmm1 xmm2 xmm3
    ..B1.15:     # Preds ..B1.15 ..B1.14
    movsd     (%rsi,%rcx,8), %xmm4                          #6.21
    movhpd    8(%rsi,%rcx,8), %xmm4                         #6.21
    mulpd     %xmm2, %xmm4                                  #6.21
    movaps    (%rdi,%rcx,8), %xmm5                          #6.12
    mulpd     %xmm3, %xmm5                                  #6.12
    addpd     %xmm4, %xmm5                                  #6.21
    movaps    16(%rdi,%rcx,8), %xmm7                        #6.12
    movaps    32(%rdi,%rcx,8), %xmm9                        #6.12
    movaps    48(%rdi,%rcx,8), %xmm11                       #6.12
    movaps    %xmm5, (%rdi,%rcx,8)                          #6.3
    mulpd     %xmm3, %xmm7                                  #6.12
    mulpd     %xmm3, %xmm9                                  #6.12
    mulpd     %xmm3, %xmm11                                 #6.12
    movsd     16(%rsi,%rcx,8), %xmm6                        #6.21
    movhpd    24(%rsi,%rcx,8), %xmm6                        #6.21
    mulpd     %xmm2, %xmm6                                  #6.21
    addpd     %xmm6, %xmm7                                  #6.21
    movaps    %xmm7, 16(%rdi,%rcx,8)                        #6.3
    movsd     32(%rsi,%rcx,8), %xmm8                        #6.21
    movhpd    40(%rsi,%rcx,8), %xmm8                        #6.21
    mulpd     %xmm2, %xmm8                                  #6.21
    addpd     %xmm8, %xmm9                                  #6.21
    movaps    %xmm9, 32(%rdi,%rcx,8)                        #6.3
    movsd     48(%rsi,%rcx,8), %xmm10                       #6.21
    movhpd    56(%rsi,%rcx,8), %xmm10                       #6.21
    mulpd     %xmm2, %xmm10                                 #6.21
    addpd     %xmm10, %xmm11                                #6.21
    movaps    %xmm11, 48(%rdi,%rcx,8)                       #6.3
    addq      $8, %rcx                                      #5.1
    cmpq      %r8, %rcx                                     #5.1
    jl        ..B1.15       # Prob 99%                      #5.1

1

可能是的,但你需要给编译器一些提示。 指针上放置__restrict__告诉编译器两个指针之间没有别名。 如果你知道向量的对齐方式,请将其告知编译器(Visual C++可能有一些工具)。

我自己不熟悉Visual C++,但我听说它不适合向量化。 考虑使用Intel编译器代替。 Intel允许对生成的汇编进行非常精细的控制:http://www.intel.com/software/products/compilers/docs/clin/main_cls/cref_cls/common/cppref_pragma_vector.htm


1
谁比自己更了解英特尔处理器呢? :) - YeenFei

-2

这取决于您如何在内存中放置u和b。 如果两个内存块相距很远,在这种情况下,SSE不会提高太多。

建议将数组u和b设置为AOE(结构数组),而不是SOA(数组结构),因为您可以在单个指令中将它们都加载到寄存器中。


2
我不同意在这里使用AOS比SOA更有优势。你仍然需要为每个存储执行2次加载,而使用AOS时,你现在只需要将每4个单位中的2个写回。使用SOA,你可以从u加载4个单位,从b加载4个单位,然后将4个单位写回到u,而无需执行任何洗牌或掩码操作。 - Adam Rosenfield
1
在这两个观点上,我不同意YeenFei的看法。对于垂直SIMD,SOA通常更优越。由于缓存的存在,内存距离并不是一个相关因素 - 即使AOS允许使用较少的缓存行(例如,由于极端对齐填充而本质上不太可能出现在适合进行SIMD化的好候选项中),重构那些缓存行为SOA仍然更好。 - mabraham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接