c - SSE:使用_mm_add_epi32看不到加速

标签 c arrays x86 performance-testing sse

我希望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;
}

最佳答案

看一下asm:clang -O2-O3可能会自动矢量化add_iv_nosse(检查是否有重叠,因为您没有使用int * restrict a依此类推)。

使用-fno-tree-vectorize禁用自动矢量化,而不会阻止您使用内部函数。我建议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版本类似。如果Apple clang暗示默认-mtune=选项,则使用IDK,但这是有道理的,因为它仅针对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初学者资料。

关于c - SSE:使用_mm_add_epi32看不到加速,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59233266/

相关文章:

c - 在结构中设置数组值在结构中设置不同的字段

c - 在c中遍历P*Q网格中的一些单元格

c - 事件驱动编程模式下C代码什么时候中断?

javascript - 在 JavaScript 中什么时候使用 'Array.prototype' 什么时候使用 'this'?

c - 数组打印值的总和

c - x86 内联汇编标志

windows - 当 processHandle = -1 时,这个 OpenProcessToken 做了什么?

msbuild - 使用 MSBuild 构建混合了 x86 和 "any cpu"项目的解决方案的所有项目

c - 声明 C 数组

c - 循环内创建线程