c++ - 如何在 C++ 中有效地添加两个 vector

标签 c++ x86 sse simd sse2

假设我有两个 vector a 和 b,存储为一个 vector 。我想生成 a += ba +=b * k,其中 k 是一个数字。

我肯定可以做到以下几点,

while (size--) {
    (*a++) += (*b++) * k;
}

但有哪些可能的方法可以轻松利用 SSE2 等 SIMD 指令?

最佳答案

应该唯一需要的是使用您的编译器启用自动矢量化。

例如,使用 GCC (5.2.0) -O3 编译您的代码(假设为 float)会生成此主循环

L8:
    movups  (%rsi,%rax), %xmm1
    addl    $1, %r11d
    mulps   %xmm2, %xmm1
    addps   (%rdi,%rax), %xmm1
    movaps  %xmm1, (%rdi,%rax)
    addq    $16, %rax
    cmpl    %r11d, %r10d
    ja  .L8

Clang 也对循环进行矢量化,但也展开了四次。即使没有依赖链,展开也可能对某些处理器有所帮助 especially on Haswell .事实上,您可以通过添加 -funroll-loops 让 GCC 展开。在这种情况下,GCC 将展开到八个独立操作 unlike in the case when there is a dependency chain .

您可能会遇到的一个问题是,您的编译器可能需要添加一些代码来确定数组是否重叠,并使两个分支在重叠时不进行矢量化,在不重叠时进行矢量化。 GCC 和 Clang 都这样做。但是 ICC 不会对循环进行矢量化。

带有 -O3 的 ICC 13.0.01

..B1.4:                         # Preds ..B1.2 ..B1.4
        movss     (%rsi), %xmm1                                 #3.21
        incl      %ecx                                          #2.5
        mulss     %xmm0, %xmm1                                  #3.28
        addss     (%rdi), %xmm1                                 #3.11
        movss     %xmm1, (%rdi)                                 #3.11
        movss     4(%rsi), %xmm2                                #3.21
        addq      $8, %rsi                                      #3.21
        mulss     %xmm0, %xmm2                                  #3.28
        addss     4(%rdi), %xmm2                                #3.11
        movss     %xmm2, 4(%rdi)                                #3.11
        addq      $8, %rdi                                      #3.11
        cmpl      %eax, %ecx                                    #2.5
        jb        ..B1.4        # Prob 63%                      #2.5

要解决此问题,您需要使用 __restrict 关键字告诉编译器数组不重叠。

void foo(float * __restrict a, float * __restrict b, float k, int size) {
    while (size--) {
        (*a++) += (*b++) * k;
    }
}

在这种情况下,ICC 产生两个分支。一种用于数组为 16 字节对齐时,另一种用于不对齐时。这是对齐的分支

..B1.16:                        # Preds ..B1.16 ..B1.15
        movaps    (%rsi), %xmm2                                 #3.21
        addl      $8, %r8d                                      #2.5
        movaps    16(%rsi), %xmm3                               #3.21
        addq      $32, %rsi                                     #1.6
        mulps     %xmm1, %xmm2                                  #3.28
        mulps     %xmm1, %xmm3                                  #3.28
        addps     (%rdi), %xmm2                                 #3.11
        addps     16(%rdi), %xmm3                               #3.11
        movaps    %xmm2, (%rdi)                                 #3.11
        movaps    %xmm3, 16(%rdi)                               #3.11
        addq      $32, %rdi                                     #1.6
        cmpl      %ecx, %r8d                                    #2.5
        jb        ..B1.16       # Prob 82%                      #2.5

在这两种情况下,ICC 都会展开两次。即使 GCC 和 Clang 生成了一个没有 __restrict 的矢量化和非矢量化分支,您可能仍然希望使用 __restrict 来消除代码的开销以确定要使用哪个分支。

您可以尝试的最后一件事是告诉编译器数组已对齐。这将适用于 GCC 和 Clang (3.6)

void foo(float * __restrict a, float * __restrict b, float k, int size) {
    a = (float*)__builtin_assume_aligned (a, 32);
    b = (float*)__builtin_assume_aligned (b, 32);
    while (size--) {
        (*a++) += (*b++) * k;
    }
}

GCC 在这种情况下产生

.L4:
    movaps  (%rsi,%r8), %xmm1
    addl    $1, %r10d
    mulps   %xmm2, %xmm1
    addps   (%rdi,%r8), %xmm1
    movaps  %xmm1, (%rdi,%r8)
    addq    $16, %r8
    cmpl    %r10d, %eax
    ja  .L4

最后,如果您的编译器支持 OpenMP 4.0,您可以像这样使用 OpenMP

void foo(float * __restrict a, float * __restrict b, float k, int size) {
    #pragma omp simd aligned(a:32) aligned(b:32)
    for(int i=0; i<size; i++) {
        a[i] += k*b[i];
    }
}

GCC 在这种情况下生成与使用 __builtin_assume_aligned 时相同的代码。这应该适用于较新版本的 ICC(我没有)。

我没有检查 MSVC。我希望它也矢量化这个循环。

有关 restrict 和编译器生成有重叠和无重叠的不同分支以及对齐和不对齐的更多详细信息,请参阅 sum-of-overlapping-arrays-auto-vectorization-and-restrict .


这里还有一个要考虑的建议。如果您知道循环范围是 SIMD 宽度的倍数,则编译器将不必使用清理代码。以下代码

// gcc -O3
// n = size/8
void foo(float * __restrict a, float * __restrict b, float k, int n) {
    a = (float*)__builtin_assume_aligned (a, 32);
    b = (float*)__builtin_assume_aligned (b, 32);
    //#pragma omp simd aligned(a:32) aligned(b:32)
    for(int i=0; i<n*8; i++) {
        a[i] += k*b[i];
    }
}

生成迄今为止最简单的程序集。

foo(float*, float*, float, int):
    sall    $2, %edx
    testl   %edx, %edx
    jle .L1
    subl    $4, %edx
    shufps  $0, %xmm0, %xmm0
    shrl    $2, %edx
    xorl    %eax, %eax
    xorl    %ecx, %ecx
    addl    $1, %edx
.L4:
    movaps  (%rsi,%rax), %xmm1
    addl    $1, %ecx
    mulps   %xmm0, %xmm1
    addps   (%rdi,%rax), %xmm1
    movaps  %xmm1, (%rdi,%rax)
    addq    $16, %rax
    cmpl    %edx, %ecx
    jb  .L4
.L1:
    rep ret

我使用了多个8 和 32 字节对齐,因为这样一来,只需使用编译器开关 -mavx,编译器就会生成出色的 AVX 向量化。

foo(float*, float*, float, int):
    sall    $3, %edx
    testl   %edx, %edx
    jle .L5
    vshufps $0, %xmm0, %xmm0, %xmm0
    subl    $8, %edx
    xorl    %eax, %eax
    shrl    $3, %edx
    xorl    %ecx, %ecx
    addl    $1, %edx
    vinsertf128 $1, %xmm0, %ymm0, %ymm0
.L4:
    vmulps  (%rsi,%rax), %ymm0, %ymm1
    addl    $1, %ecx
    vaddps  (%rdi,%rax), %ymm1, %ymm1
    vmovaps %ymm1, (%rdi,%rax)
    addq    $32, %rax
    cmpl    %edx, %ecx
    jb  .L4
    vzeroupper
.L5:
    rep ret

我不确定如何使序言更简单,但我看到的唯一改进是删除了一个迭代器和一个比较。即 addl $1, %ecx 指令应该不是必需的。 cmpl %edx, %ecx 是否必要。我不确定如何让 GCC 解决这个问题。我在使用 GCC ( Produce loops without cmp instruction in GCC ) 时遇到了一个问题。

关于c++ - 如何在 C++ 中有效地添加两个 vector ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33274164/

相关文章:

c++ - 将引用参数传递给汇编函数

x86 - 进化算法是否有可能创建机器代码?

c++ - 使用 SSE/AVX 的整数点积?

c++ - SSE 内在函数位向右移动

c++ - 使用值初始化访问构造函数?

c++ - 使用 long 的一般规则

c++ - 保证复制省略在 C++1z 的列表初始化中如何工作?

assembly - 是否可以在 RAM 中执行一些计算?

c++ - 动态内存分配在调试时似乎是即时的,但在 Release模式下是渐进的

c - SSE _mm_load_pd 在 _mm_store_pd 段错误时工作