AVX vs. SSE:期望看到更大的加速

Posted

技术标签:

【中文标题】AVX vs. SSE:期望看到更大的加速【英文标题】:AVX vs. SSE: expect to see a larger speedup 【发布时间】:2017-11-04 20:50:16 【问题描述】:

我预计 AVX 比 SSE 快 1.5 倍。所有 3 个数组(3 个数组 * 16384 个元素 *4 字节/元素 = 196608 字节)都应该适合 Intel Core CPU (Broadwell) 上的二级缓存 (256KB)。

我应该使用任何特殊的编译器指令或标志吗?

编译器版本

$  clang --version
Apple LLVM version 9.0.0 (clang-900.0.38)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

编译行

$  make avx
clang -O3 -fno-tree-vectorize -msse -msse2 -msse3 -msse4.1 -mavx -mavx2 avx.c ; ./a.out 123
n: 123
  AVX Time taken: 0 seconds 177 milliseconds
vector+vector:begin int: 1 5 127 0

  SSE Time taken: 0 seconds 195 milliseconds
vector+vector:begin int: 1 5 127 0

avx.c

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

// add int vectors via AVX
__attribute__((noinline)) 
void add_iv_avx(__m256i *restrict a, __m256i *restrict b, __m256i *restrict out, int N) 

    __m256i *x = __builtin_assume_aligned(a, 32);
    __m256i *y = __builtin_assume_aligned(b, 32);
    __m256i *z = __builtin_assume_aligned(out, 32);

    const int loops = N / 8; // 8 is number of int32 in __m256i
    for(int i=0; i < loops; i++)  
        _mm256_store_si256(&z[i], _mm256_add_epi32(x[i], y[i]));
    


// add int vectors via SSE; https://en.wikipedia.org/wiki/Restrict
__attribute__((noinline)) 
void add_iv_sse(__m128i *restrict a, __m128i *restrict b, __m128i *restrict out, int N) 

    __m128i *x = __builtin_assume_aligned(a, 16);
    __m128i *y = __builtin_assume_aligned(b, 16);
    __m128i *z = __builtin_assume_aligned(out, 16);

    const int loops = N / sizeof(int);
    for(int i=0; i < loops; i++)  
        //out[i]= _mm_add_epi32(a[i], b[i]); // this also works
        _mm_storeu_si128(&z[i], _mm_add_epi32(x[i], y[i]));
     


// printing
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]);


__attribute__((noinline)) 
void debug_print(int *h) 
    printf("vector+vector:begin ");
    p128_as_int(* (__m128i*) &h[0] );


int main(int argc, char *argv[]) 
    int n = atoi (argv[1]);
    printf("n: %d\n", n);

    int *x,*y,*z;
    if (posix_memalign((void**)&x, 32, 16384*sizeof(int)))  free(x); return EXIT_FAILURE; 
    if (posix_memalign((void**)&y, 32, 16384*sizeof(int)))  free(y); return EXIT_FAILURE; 
    if (posix_memalign((void**)&z, 32, 16384*sizeof(int)))  free(z); return EXIT_FAILURE; 
    x[0]=0; x[1]=2; x[2]=4;
    y[0]=1; y[1]=3; y[2]=n;

    // touch each 4K page in x,y,z to avoid copy-on-write optimizations
    for (int i=512; i<AR; i+= 512)  x[i]=1; y[i]=1; z[i]=1; 

    // warmup
    for(int i=0; i<REPS; ++i)  add_iv_avx((__m256i*)x, (__m256i*)y, (__m256i*)z, AR); 
    // AVX
    clock_t start = clock();
    for(int i=0; i<REPS; ++i)  add_iv_avx((__m256i*)x, (__m256i*)y, (__m256i*)z, AR); 
    int msec = (clock()-start) * 1000 / CLOCKS_PER_SEC;
    printf("  AVX Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000);
    debug_print(z);

    // warmup
    for(int i=0; i<REPS; ++i)  add_iv_sse((__m128i*)x, (__m128i*)y, (__m128i*)z, AR); 
    // SSE
    start = clock();
    for(int i=0; i<REPS; ++i)  add_iv_sse((__m128i*)x, (__m128i*)y, (__m128i*)z, AR); 
    msec = (clock()-start) * 1000 / CLOCKS_PER_SEC;
    printf("\n  SSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000);
    debug_print(z);

    return EXIT_SUCCESS;

【问题讨论】:

每个元素的计算量如此之少(只是一个加法),我希望执行时间主要受内存限制。 您使用的是 Broadwell,因此您没有 Skylake 的硬件 P 状态功能,无法快速提升到最大涡轮增压。 36ms 对于测量挂钟时间(而不是核心时钟周期)来说非常短。还有大约 14 us 的 AVX 预热期,其中 256b 指令可能慢 4 倍:agner.org/optimize/blog/read.php?i=415。 Agner 说他没有在 Skylake 之前观察到它,但其他人有。无论如何,首先运行 SSE 将有一些时间以低时钟速度运行,除非您通过先预热来控制这一点。 您还测试了第一次接触输出数组的成本。 (TLB 未命中和软页面错误)。不过,这对于小型阵列上的 10k REPS 来说可能非常小。但这是堆栈内存,因此在堆栈的更下方触及新内存也需要内核扩展您的堆栈映射。根据 gcc 在堆栈上布局数组的方式,AVX 接触 z[] 可能会扩展堆栈映射以包含 SSE 使用的 h[]。或者 gcc 很聪明,可以重用死掉的 x,y,z 堆栈内存来节省空间,有利于第二个循环。 在没有计时的情况下调用一次代码,以确保内存已分页、缓存已预热、时钟速度已加快等,然后在有计时的循环中调用它。 @AG1:只需检查编译器的 asm 输出以确保您得到了预期的结果。我可能会为这两个测试重用相同的数组。我可能会将它们放在 BSS 中。正如 Paul R 建议的那样,在计时部分之前运行更多迭代或进行热身。如果您使迭代计数好且高,并且仅在一次执行中执行 AVX SSE,您可以使用perf stat 分析整个程序,以获得由您的实际工作主导的性能计数器想要测量,而不是启动/清理。 (几秒钟应该没问题,特别是如果你计算核心时钟周期,而不是秒。) 【参考方案1】:

问题是您的数据不适合 L1 缓存。 Broadwell 的 L1 带宽远大于 L2 带宽。 L1 带宽足够大,可以在每个 cpu 周期加载两个 32 字节向量。因此,更好的 AVX 与 SSE 加速 如果您的数据集小得多,则可能会出现这种情况。但是,请注意 组合的 L1 读/写带宽小于 2*32(r)+32(w)=96 个字节/周期。 实际上每个周期 75 个字节是可能的,请参阅here。

this 页面上的第二张图显示 L2 带宽确实要小得多: 在 Test_block_size=128KB(=每核 32KB)时,带宽为 900GB/s。 在 Test_block_size=1MB(=256KB/核心)时,带宽仅为 300GB/s。 (请注意,Haswell 4770k 与 Broadwell 具有或多或少相同的 L1 和 L2 缓存架构。)

尝试将 AR 减少到 2000 并将 NREP 增加到 1000000,看看 SSE 与 AVX 加速比会发生什么。

【讨论】:

我突然想到 gcc 的循环也可能是前端瓶颈。尽管这种效果有利于 AVX2,但因为每 uop 的工作量是 2 倍是一种胜利。 @PeterCordes 是的,这也可能在这里发挥作用。正如预期的那样,将数组大小AR减少到 2000 确实导致 AVX 与 SSE 的加速比为 2。

以上是关于AVX vs. SSE:期望看到更大的加速的主要内容,如果未能解决你的问题,请参考以下文章

为啥 AVX2 和 SSE2 按位 OR 运算符并不比简单的快?操作员?

为啥 SSE 和 AVX 具有相同的效率?

多媒体指令(AVX加速数组求和)

c/c++ 代码中使用sse指令集加速

使用 sse 和 avx 内在函数将一组打包的单曲添加到一个值中

适合点播应用的P2P加速系统