【问题标题】:Accumulate vector of integer with sse用 sse 累加整数向量
【发布时间】:2016-01-04 14:49:42
【问题描述】:

我尝试更改此代码以处理std::vector<int>

float accumulate(const std::vector<float>& v)
{
 // copy the length of v and a pointer to the data onto the local stack
 const size_t N = v.size();
 const float* p = (N > 0) ? &v.front() : NULL;

 __m128 mmSum = _mm_setzero_ps();
 size_t i = 0;

 // unrolled loop that adds up 4 elements at a time
 for(; i < ROUND_DOWN(N, 4); i+=4)
 {
  mmSum = _mm_add_ps(mmSum, _mm_loadu_ps(p + i));
 }

 // add up single values until all elements are covered
 for(; i < N; i++)
 {
  mmSum = _mm_add_ss(mmSum, _mm_load_ss(p + i));
 }

 // add up the four float values from mmSum into a single value and return
 mmSum = _mm_hadd_ps(mmSum, mmSum);
 mmSum = _mm_hadd_ps(mmSum, mmSum);
 return _mm_cvtss_f32(mmSum);
}

参考:http://fastcpp.blogspot.com.au/2011/04/how-to-process-stl-vector-using-sse.html

我将_mm_setzero_ps 更改为_mm_setzero_si128,将_mm_loadu_ps 更改为mm_loadl_epi64,将_mm_add_ps 更改为_mm_add_epi64

我收到此错误:

error: cannot convert ‘const int*’ to ‘const __m128i* {aka const __vector(2) long long int*}’ for argument ‘1’ to ‘__m128i _mm_loadl_epi64(const __m128i*)’
         mmSum = _mm_add_epi64(mmSum, _mm_loadl_epi64(p + i + 0));

我是这个领域的新手。有什么好的资源可以学习这些东西吗?

【问题讨论】:

  • “我是这个领域的新手。有什么好的资源可以学习这些东西吗?” -- 尝试在 StackOverflow 上搜索 [sse] 标签 - 有很多很好的问题和答案以及一些有用的代码示例 - 您可能可以从中学到很多东西。
  • 您是否有理由需要对 SSE 执行此操作?在这样一个微不足道的操作中,无论有没有 SSE,您都会受到内存带宽的限制。但是对于 SSE,您需要在最后进行水平添加才能获得正确的结果。它更复杂,而且运行速度可能不会更快。更重要的是——除非数字非常小——你不能对大量元素进行前缀求和,因为你会遇到溢出。所以……可读的 C++ 代码同样快,而且……可读。我倾向于将此称为“过早的优化”卓越
  • @Damon,我认为这太微不足道了,编译器无论如何都会用正确的标志对其进行矢量化。
  • @Zboson:同意,如果对齐允许的话。但它仍然没有任何区别。缓存行中有 16 个整数(4 个 SSE 寄存器)的数据,预取器需要 150-200 个周期才能加载下一个缓存行。因此,除非您做了值得至少一百个周期的事情,否则考虑优化它完全是荒谬的。整个 4 个添加操作(其中 16 个也是如此)几乎不在那个范围内。
  • @Damon,是的,这是我首先对自动矢量化持怀疑态度的一个原因。在大多数情况下,当它工作时,它受内存带宽限制,而在少数情况下,当它不受内存限制时,自动矢量化不会像你想要的那样工作(这就是内在函数有用的原因)。所以最后你需要手工完成。 OP 所做的只是对教育有用。

标签: c++ vector x86 sse simd


【解决方案1】:

这是我刚刚拼凑的int 版本:

#include <iostream>
#include <vector>

#include <smmintrin.h>  // SSE4

#define ROUND_DOWN(m, n) ((m) & ~((n) - 1))

static int accumulate(const std::vector<int>& v)
{
    // copy the length of v and a pointer to the data onto the local stack
    const size_t N = v.size();
    const int* p = (N > 0) ? &v.front() : NULL;

    __m128i mmSum = _mm_setzero_si128();
    int sum = 0;
    size_t i = 0;

    // unrolled loop that adds up 4 elements at a time
    for(; i < ROUND_DOWN(N, 4); i+=4)
    {
        mmSum = _mm_add_epi32(mmSum, _mm_loadu_si128((__m128i *)(p + i)));
    }

    // add up the four int values from mmSum into a single value
    mmSum = _mm_hadd_epi32(mmSum, mmSum);
    mmSum = _mm_hadd_epi32(mmSum, mmSum);
    sum = _mm_extract_epi32(mmSum, 0);

    // add up single values until all elements are covered
    for(; i < N; i++)
    {
        sum += p[i];
    }

    return sum;
}

int main()
{
    std::vector<int> v;

    for (int i = 0; i < 10; ++i)
    {
        v.push_back(i);
    }

    int sum = accumulate(v);

    std::cout << sum << std::endl;

    return 0;
}

编译运行:

$ g++ -Wall -msse4 -O3 accumulate.cpp && ./a.out 
45

【讨论】:

  • 我要添加的主要内容是让编译器执行此操作。在启用矢量化 (-O3) 和 -funroll-loops 的情况下进行编译,因为它具有依赖链。还值得指出的是,对于浮点数,除非启用关联数学,否则编译器甚至不会展开,例如-Ofast.
  • @user1095108,查看程序集,看看编译器做了什么。我不认为对齐是一个问题。编译器知道如何处理未对齐以及 SIMD 宽度的非倍数(SSE 为 4)。如果您希望编译器使用对齐的负载,您可以使用 __builtin_assume_aligned。看到这个sum of overlapping arrays, auto-vectorization, and restrict
  • @Zboson:我刚刚检查了 ICC,这似乎做得更好 - 它在没有被告知的情况下展开循环并且没有像 gcc 这样的串行依赖项。
  • Clang 似乎使用 SSE 展开 4 次,但它的 AVX 代码看起来很难看。至少 Clang 有正确的想法。我什至不必使用-funroll-loops。无论如何,我不应该这样做。编译器应该已经知道展开。
【解决方案2】:

执行此操作的理想方法是让编译器自动矢量化您的代码并保持您的代码简单易读。你不应该需要更多的东西

int sum = 0;
for(int i=0; i<v.size(); i++) sum += v[i];

您指向的链接http://fastcpp.blogspot.com.au/2011/04/how-to-process-stl-vector-using-sse.html 似乎不了解如何使编译器矢量化代码。

对于该链接使用的浮点,您需要知道的是浮点运算不是关联的,因此取决于您进行归约的顺序。 GCC、MSVC 和 Clang 不会进行自动矢量化来减少,除非您告诉它使用不同的浮点模型,否则您的结果可能取决于您的硬件。然而,ICC 默认为关联浮点数学,因此它将使用例如向量化代码。 -O3.

除非允许关联数学,否则 GCC、MSVC 和 Clang 不仅不会向量化,而且它们不会展开循环以允许部分求和以克服求和的延迟。 In this case only Clang and ICC will unroll to partial sums anyway. Clang unrolls four times and ICC twice.

使用 GCC 启用关联浮点运算的一种方法是使用 -Ofast 标志。使用 MSVC 使用 /fp:fast

我使用 GCC 4.9.2、XeonE5-1620 (IVB) @ 3.60GHz、Ubuntu 15.04 测试了以下代码。

-O3 -mavx -fopenmp                       0.93 s
-Ofast -mavx -fopenmp                    0.19 s
-Ofast -mavx -fopenmp -funroll-loops     0.19 s

这大约是五倍的加速。虽然,GCC 确实展开了八次循环,但它不会进行独立的部分求和(参见下面的程序集)。这就是展开版本没有更好的原因。

我只使用 OpenMP 方便的跨平台/编译器计时功能:omp_get_wtime()

自动矢量化的另一个优势是它只需启用编译器开关(例如-mavx)即可用于 AVX。否则,如果您想要 AVX,则必须重写代码以使用 AVX 内在函数,并且可能需要就如何执行此操作提出另一个关于 SO 的问题。

因此,目前唯一可以自动矢量化您的循环以及展开到四个部分和的编译器是 Clang。请参阅此答案末尾的代码和程序集。


这是我用来测试性能的代码

#include <stdio.h>
#include <omp.h>
#include <vector>

float sumf(float *x, int n)
{
  float sum = 0;
  for(int i=0; i<n; i++) sum += x[i];
  return sum;
}

#define N 10000 // the link used this value
int main(void)
{
  std::vector<float> x;
  for(int i=0; i<N; i++) x.push_back(1 -2*(i%2==0));
  //float x[N]; for(int i=0; i<N; i++) x[i] = 1 -2*(i%2==0);                                                                                                                                                        
  float sum = 0;
  sum += sumf(x.data(),N);
  double dtime = -omp_get_wtime();
  for(int r=0; r<100000; r++) {
    sum += sumf(x.data(),N);
  }
  dtime +=omp_get_wtime();
  printf("sum %f time %f\n", sum, dtime);
}

编辑:

我应该听取自己的建议并查看程序集。

-O3 的主循环。很明显它只做一个标量和。

.L3:
    vaddss  (%rdi), %xmm0, %xmm0
    addq    $4, %rdi
    cmpq    %rax, %rdi
    jne .L3

-Ofast 的主循环。它会进行向量求和,但不会展开。

.L8:
    addl    $1, %eax
    vaddps  (%r8), %ymm1, %ymm1
    addq    $32, %r8
    cmpl    %eax, %ecx
    ja  .L8

-O3 -funroll-loops 的主循环。具有 8 倍展开的向量和

.L8:
    vaddps  (%rax), %ymm1, %ymm2
    addl    $8, %ebx
    addq    $256, %rax
    vaddps  -224(%rax), %ymm2, %ymm3
    vaddps  -192(%rax), %ymm3, %ymm4
    vaddps  -160(%rax), %ymm4, %ymm5
    vaddps  -128(%rax), %ymm5, %ymm6
    vaddps  -96(%rax), %ymm6, %ymm7
    vaddps  -64(%rax), %ymm7, %ymm8
    vaddps  -32(%rax), %ymm8, %ymm1
    cmpl    %ebx, %r9d
    ja  .L8

编辑:

将以下代码放入 Clang 3.7 (-O3 -fverbose-asm -mavx)

float sumi(int *x)
{
  x = (int*)__builtin_assume_aligned(x, 64);
  int sum = 0;
  for(int i=0; i<2048; i++) sum += x[i];
  return sum;
}

生成以下程序集。请注意,它被向量化为四个独立的部分和。

sumi(int*):                              # @sumi(int*)
    vpxor   xmm0, xmm0, xmm0
    xor eax, eax
    vpxor   xmm1, xmm1, xmm1
    vpxor   xmm2, xmm2, xmm2
    vpxor   xmm3, xmm3, xmm3
.LBB0_1:                                # %vector.body
    vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rax]
    vpaddd  xmm1, xmm1, xmmword ptr [rdi + 4*rax + 16]
    vpaddd  xmm2, xmm2, xmmword ptr [rdi + 4*rax + 32]
    vpaddd  xmm3, xmm3, xmmword ptr [rdi + 4*rax + 48]
    vpaddd  xmm0, xmm0, xmmword ptr [rdi + 4*rax + 64]
    vpaddd  xmm1, xmm1, xmmword ptr [rdi + 4*rax + 80]
    vpaddd  xmm2, xmm2, xmmword ptr [rdi + 4*rax + 96]
    vpaddd  xmm3, xmm3, xmmword ptr [rdi + 4*rax + 112]
    add rax, 32
    cmp rax, 2048
    jne .LBB0_1
    vpaddd  xmm0, xmm1, xmm0
    vpaddd  xmm0, xmm2, xmm0
    vpaddd  xmm0, xmm3, xmm0
    vpshufd xmm1, xmm0, 78          # xmm1 = xmm0[2,3,0,1]
    vpaddd  xmm0, xmm0, xmm1
    vphaddd xmm0, xmm0, xmm0
    vmovd   eax, xmm0
    vxorps  xmm0, xmm0, xmm0
    vcvtsi2ss   xmm0, xmm0, eax
    ret

【讨论】:

    【解决方案3】:
    static inline int32_t accumulate(const int32_t *data, size_t size) {
      constexpr const static size_t batch = 256 / 8 / sizeof(int32_t);
      int32_t sum = 0;
      size_t pos = 0;
    
      if (size >= batch) {
        // 7
        __m256i mmSum = _mm256_loadu_si256((__m256i *)(data));
        pos = batch;
    
        // unrolled loop
        for (; pos + batch < size; pos += batch) {
          // 1 + 7
          mmSum =
              _mm256_add_epi32(mmSum, _mm256_loadu_si256((__m256i *)(data + pos)));
        }
    
        mmSum = _mm256_hadd_epi32(mmSum, mmSum);
        mmSum = _mm256_hadd_epi32(mmSum, mmSum);
        // 2 + 1 + 3 + 0
        sum = _mm_cvtsi128_si32(_mm_add_epi32(_mm256_extractf128_si256(mmSum, 1),
                                              _mm256_castsi256_si128(mmSum)));
      }
    
      // add up remain values
      while (pos < size) {
        sum += data[pos++];
      }
      return sum;
    }
    

    【讨论】:

    • hsum 效率很低; _mm256_extract_epi32(mmSum, 4); 至少需要 2 条指令(例如 vextracti128 xmm, ymm, 1 / vmovd edx, xmm0)。而hadd 需要 2 次洗牌 + 1 次加法,在 AMD 上更糟。 Fastest way to do horizontal SSE vector sum (or other reduction) 有一些链接,包括Fastest method to calculate sum of all packed 32-bit integers using AVX512 or AVX2
    • 另外,如果你要将_mm256_loadu_si256 放在三元组中,编译器将不得不对其进行分支。如所写,您的代码将在零向量上执行 2x hadd + extract,而不是通过将其放在 if(size &gt;= batch) 中来分支整个 SIMD 部分。编译器可能很聪明,可以为您解决这个问题,但唯一的缺点是函数的 SIMD 部分缩进更深。
    猜你喜欢
    • 1970-01-01
    • 2016-09-02
    • 1970-01-01
    • 1970-01-01
    • 2011-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-11
    相关资源
    最近更新 更多