【问题标题】:SSE: not seeing a speedup by using _mm_add_epi32SSE:使用 _mm_add_epi32 没有看到加速
【发布时间】:2018-03-27 11:09:31
【问题描述】:

我希望 SSE 比不使用 SSE 更快。我需要添加一些额外的编译器标志吗?是不是因为这是整数代码而不是浮点数,所以我没有看到加速?

调用/输出

$ make sum2
clang -O3 -msse -msse2 -msse3 -msse4.1 sum2.c ; ./a.out 123
n: 123
  SSE Time taken: 0 seconds 124 milliseconds
vector+vector:begin int: 1 5 127 0
vector+vector:end int: 0 64 66 68
NOSSE Time taken: 0 seconds 115 milliseconds
vector+vector:begin int: 1 5 127 0
vector+vector:end int: 0 64 66 68

编译器

$ clang --version
Apple LLVM version 9.0.0 (clang-900.0.37)
Target: x86_64-apple-darwin16.7.0
Thread model: posix

sum2.c

#include <stdlib.h>
#include <stdio.h>
#include <x86intrin.h>
#include <time.h>
#ifndef __cplusplus
#include <stdalign.h>   // C11 defines _Alignas().  This header defines alignas()
#endif
#define CYCLE_COUNT  10000

// add vector and return resulting value on stack
__attribute__((noinline)) __m128i add_iv(__m128i *a, __m128i *b) {
    return _mm_add_epi32(*a,*b);
}

// add int vectors via sse
__attribute__((noinline)) void add_iv_sse(__m128i *a, __m128i *b, __m128i *out, int N) {
    for(int i=0; i<N/sizeof(int); i++) { 
        //out[i]= _mm_add_epi32(a[i], b[i]); // this also works
        _mm_storeu_si128(&out[i], _mm_add_epi32(a[i], b[i]));
    } 
}

// add int vectors without sse
__attribute__((noinline)) void add_iv_nosse(int *a, int *b, int *out, int N) {
    for(int i=0; i<N; i++) { 
        out[i] = a[i] + b[i];
    } 
}

__attribute__((noinline)) void p128_as_int(__m128i in) {
    alignas(16) uint32_t v[4];
    _mm_store_si128((__m128i*)v, in);
    printf("int: %i %i %i %i\n", v[0], v[1], v[2], v[3]);
}

// print first 4 and last 4 elements of int array
__attribute__((noinline)) void debug_print(int *h) {
    printf("vector+vector:begin ");
    p128_as_int(* (__m128i*) &h[0] );
    printf("vector+vector:end ");
    p128_as_int(* (__m128i*) &h[32764] );
}

int main(int argc, char *argv[]) {
    int n = atoi (argv[1]);
    printf("n: %d\n", n);
    // sum: vector + vector, of equal length
    int f[32768] __attribute__((aligned(16))) = {0,2,4};
    int g[32768] __attribute__((aligned(16))) = {1,3,n};
    int h[32768] __attribute__((aligned(16))); 
    f[32765] = 33; f[32766] = 34; f[32767] = 35;
    g[32765] = 31; g[32766] = 32; g[32767] = 33;

    // https://stackoverflow.com/questions/459691/best-timing-method-in-c
    clock_t start = clock();
        for(int i=0; i<CYCLE_COUNT; ++i) {
            add_iv_sse((__m128i*)f, (__m128i*)g, (__m128i*)h, 32768);
        }
    int msec = (clock()-start) * 1000 / CLOCKS_PER_SEC;
    printf("  SSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000);
    debug_print(h);

    // process intense function again
    start = clock();
        for(int i=0; i<CYCLE_COUNT; ++i) {
            add_iv_nosse(f, g, h, 32768);
        }
    msec = (clock()-start) * 1000 / CLOCKS_PER_SEC;
    printf("NOSSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000);
    debug_print(h);

    return EXIT_SUCCESS;
}

【问题讨论】:

  • 另外,您对add_iv(幸运的是您从未使用过)的评论是错误的:__m128i 返回值在 x86-64 System V 调用约定中的 XMM0 中返回,而不是在堆栈上.
  • 谢谢彼得!有没有办法阻止编译器在某些块中使用 SSE 指令?
  • 我更新了我的答案,对自动矢量化代码与手动矢量化循环进行了一些性能分析。它们都有很多开销,但我认为手动应该更快,除非 4k 混叠会损害其带宽。所以也许 turbo 效果使第二个循环花费更少的挂钟时间,即使它需要更多的 CPU 周期,或者可能有不同的效果。

标签: c arrays x86 performance-testing sse


【解决方案1】:

查看 asm:clang -O2-O3 可能会自动矢量化 add_iv_nosse(检查重叠,因为您没有使用 int * restrict a 等等)。

使用 -fno-tree-vectorize 禁用 auto 矢量化,而不会阻止您使用内在函数。我建议clang -march=native -mno-avx -O3 -fno-tree-vectorize 来测试我认为您想要测试的内容,标量整数与旧版 SSE paddd。 (它适用于 gcc 和 clang。在 clang 中,AFAIK 是特定于 clang 的 -fno-vectorize 的同义词。)

顺便说一句,在同一个可执行文件中的计时会伤害第一个,因为 CPU 不会立即加速到全涡轮。在 CPU 达到全速之前,您可能已经进入了代码的定时部分。 (因此,使用for i in {1..10}; do time ./a.out; done 连续运行几次。

在 Linux 上,我会使用 perf stat -r5 ./a.out 使用性能计数器运行 5 次(我会将其拆分,以便一次运行测试其中一项,这样我就可以查看整个运行的性能计数器。)


代码审查:

您忘记了 stdint.huint32_t。我必须将其添加到compile on Godbolt to see the asm。 (假设 clang-5.0 类似于您正在使用的 Apple clang 版本。IDK 如果 Apple 的 clang 暗示默认的 -mtune= 选项,但这是有道理的,因为它只针对 Mac。基线 SSSE3 也适用于 64-位在 x86-64 OS X 上。)

您不需要在 debug_print 上使用 noinline。另外,我建议CYCLE_COUNT 使用不同的名称。在这种情况下,周期让我想到了时钟周期,所以称它为 REP_COUNTREPEATS 或其他任何名称。

将您的数组放在main 的堆栈中可能没问题。您确实初始化了两个输入数组(大部分为零,但添加性能不依赖于数据)。

这很好,因为不初始化它们可能意味着每个阵列的多个 4k 页被写入时复制映射到同一个物理零页,因此您将获得超过预期数量的 L1D 缓存命中。

SSE2 循环应该是 L2 / L3 缓存带宽的瓶颈,因为工作设置为 4 * 32kiB * 3 = 384 kiB,因此它是 Intel CPU 中 256kiB L2 缓存的 1.5 倍。

clang 可能会比手动内在函数循环更多地展开它的自动矢量化循环。这可能解释了更好的性能,因为如果每个时钟没有 2 个负载 + 1 个存储,则只有 16B 向量(不是 32B AVX2)可能不会使缓存带宽饱和。

更新:实际上循环开销非常大,有 3 个指针增量 + 一个循环计数器,并且只有展开 2 才能摊销。


自动矢量化循环:

.LBB2_12:                               # =>This Inner Loop Header: Depth=1
    movdqu  xmm0, xmmword ptr [r9 - 16]
    movdqu  xmm1, xmmword ptr [r9]         # hoisted load for 2nd unrolled iter
    movdqu  xmm2, xmmword ptr [r10 - 16]
    paddd   xmm2, xmm0
    movdqu  xmm0, xmmword ptr [r10]
    paddd   xmm0, xmm1
    movdqu  xmmword ptr [r11 - 16], xmm2
    movdqu  xmmword ptr [r11], xmm0
    add     r9, 32
    add     r10, 32
    add     r11, 32
    add     rbx, -8               # add / jne  macro-fused on SnB-family CPUs
    jne     .LBB2_12

所以它是 12 个融合域微指令,每 3 个时钟最多可以运行 2 个向量,在每个时钟 4 微指令的前端问题带宽上存在瓶颈。

它没有使用对齐加载,因为编译器没有内联到已知对齐的main 的情况下没有该信息,并且您不能保证与p = __builtin_assume_aligned(p, 16) 或独立函数中的任何内容对齐。对齐加载(或 AVX)将允许 paddd 使用内存操作数,而不是单独的 movdqu 加载。

手动矢量化循环使用对齐负载来节省前端 uops,但循环计数器的循环开销更大。

.LBB1_7:                                # =>This Inner Loop Header: Depth=1
    movdqa  xmm0, xmmword ptr [rcx - 16]
    paddd   xmm0, xmmword ptr [rax - 16]
    movdqu  xmmword ptr [r11 - 16], xmm0

    movdqa  xmm0, xmmword ptr [rcx]
    paddd   xmm0, xmmword ptr [rax]
    movdqu  xmmword ptr [r11], xmm0

    add     r10, 2               # separate loop counter
    add     r11, 32              # 3 pointer incrmeents
    add     rax, 32
    add     rcx, 32
    cmp     r9, r10              # compare the loop counter
    jne     .LBB1_7

所以它是 11 个融合域微指令。它应该比自动矢量化循环运行得更快。您的计时方法可能导致了问题。

(除非混合加载和存储实际上使其不太理想。自动矢量化循环执行 4 次加载,然后进行 2 次存储。实际上这可以解释它。您的数组是 4kiB 的倍数,并且可能都具有相同的相对对齐。所以你可能会在这里得到 4k 混叠,这意味着 CPU 不确定存储是否与负载重叠。我认为有一个性能计数器可以检查。)


另请参阅Agner Fog's microarch guide (and instruction tables + optimization guide,以及 标签 wiki 中的其他链接,尤其是英特尔的优化指南。

标签 wiki 中还有一些不错的 SSE/SIMD 初学者资料。

【讨论】:

  • 对于 Clang,我通常使用 -fno-vectorize。为什么要使用-fno-tree-vectorize(除了要与GCC保持一致)?
  • @Zboson:我不知道 clang 对该选项有不同的名称,谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-30
  • 1970-01-01
相关资源
最近更新 更多