【问题标题】:SSE SIMD Optimization For LoopSSE SIMD 优化循环
【发布时间】:2010-05-27 03:39:39
【问题描述】:

我在循环中有一些代码

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];
}

【问题讨论】:

标签: visual-c++ sse simd


【解决方案1】:

是的,这是矢量化的绝佳候选者。但是,在您这样做之前,确保您已经分析了您的代码,以确保这确实值得优化。也就是说,矢量化将是这样的:

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];

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

【讨论】:

  • 肯定发现这段代码是瓶颈。一个检查我学习和实现向量化的问题是不是浪费精力 - 编译器通常不会自动对此类代码进行向量化,对吗?
  • @Projectile 如果你告诉编译器别名,通常它会。根据我自己的经验,不费很大力气就生成比编译器更好的代码是非常不寻常的。
【解决方案2】:

_mm_set_pd 未矢量化。如果从字面上看,它使用标量操作读取两个双精度数,然后组合两个标量双精度数并将它们复制到 SSE 寄存器中。请改用_mm_load_pd

【讨论】:

    【解决方案3】:

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

    我自己对 Visual C++ 并不熟悉,但我听说它对矢量化没有好处。 考虑改用英特尔编译器。 英特尔允许对生成的程序集进行非常细粒度的控制:http://www.intel.com/software/products/compilers/docs/clin/main_cls/cref_cls/common/cppref_pragma_vector.htm

    【讨论】:

    • 谁比自己更了解英特尔处理器? :)
    【解决方案4】:

    是的,假设 U 和 B 数组没有重叠,这是矢量化的理想选择。但是代码受内存访问(加载/存储)的约束。矢量化有助于减少每个循环的周期,但指令将由于 U 和 B 数组上的缓存未命中而停止。英特尔 C/C++ 编译器使用 Xeon x5500 处理器的默认标志生成以下代码。编译器将循环展开 8 并使用 xmm[0-16] SIMD 寄存器使用 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
    

    【讨论】:

      【解决方案5】:

      这取决于你如何将 u 和 b 放在内存中。 如果两个内存块相距很远,SSE 在这种情况下不会有太大提升。

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

      【讨论】:

      • 我不同意在这里使用 AOS 比 SOA 更有利。您仍在为每个商店执行 2 次加载,而使用 AOS,您现在只需写回每 4 个单元中的 2 个。使用 SOA,您可以从 u 加载 4 个单元,从 b 加载 4 个单元,然后将 4 写回 u,而无需执行任何改组或屏蔽。
      • 在这两点上都不同意叶飞。 SOA 通常优于垂直 SIMD。由于缓存,内存距离不是一个相关因素 - 即使 AOS 允许使用更少的缓存行(例如,由于极端对齐填充,这在 SIMD 化的良好候选者中本质上不太可能发生),重组那些仍然会更好缓存行成为 SOA。
      猜你喜欢
      • 2019-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-07-20
      相关资源
      最近更新 更多