2017-10-16 146 views
0

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

调用/输出

$ 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; 
} 
+0

此外,您在'add_iv'评论(你幸运从不使用)是错误的:一'__m128i'返回值将返回在XMM0在X86-64系统V调用约定,不是在栈中。 –

+0

谢谢彼得!有没有办法阻止编译器在某些块中使用SSE指令? – AG1

+0

我更新了我的答案,对自动矢量化代码的一些性能分析与手动矢量化循环进行了比较。他们都有很多开销,但我认为手动应该更快,除非4k别名损害其带宽。所以涡轮增压效果可以让第二个循环花费更少的时钟周期,即使它需要更多的CPU周期,或者可能会有不同的效果。 –

回答

4

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

使用-fno-tree-vectorize禁用自动向量化,而不会停止使用内在函数。我建议clang -march=native -mno-avx -O3 -fno-tree-vectorize来测试我认为你想测试的,标量整数与legacy-SSE paddd。 (它在gcc和clang中工作,在clang中,AFAIK是clang特有的-fno-vectorize的同义词。)

顺便说一下,在同一个可执行文件中的计时都会伤害第一个计时,因为CPU不会升级到完整的turbo马上。在CPU达到全速之前,您可能已进入代码的定时部分。 (所以运行这个几次背到后面,用for i in {1..10}; do time ./a.out; done

在Linux上我会用perf stat -r5 ./a.out运行它5倍的性能计数器(和我把它分解了这么一个运行测试的一个或其他的,所以我可以看看Perf Counters的整个运行)


代码回顾:。

你忘了stdint.huint32_t我不得不补充说,它得到compile on Godbolt to see the asm(假设clang-。 5.0是类似于Apple使用的clang版本。如果苹果的clang暗示广告,则IDK使用IDK默认-mtune=选项,但这是有道理的,因为它只针对Mac。基准SSSE3对于x86-64 OS X上的64位也是有意义的。)

在debug_print上不需要noinline。另外,我建议为CYCLE_COUNT另外命名。在这种情况下,循环让我想到时钟周期,所以称它为REP_COUNTREPEATS或其他。

将您的阵列放在main的堆栈中可能没问题。你可以初始化两个输入数组(大部分为零,但增加性能并不取决于数据)。

这很好,因为让它们未初始化可能意味着每个阵列的多个4k页都是写时复制映射到相同的物理零页,因此您会获得比预期的L1D高速缓存命中数量更多的时间。

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

铛可能展开它比它的手工内在循环自动向量化的循环。这可能会解释更好的性能,因为如果每个时钟没有获得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负荷。

的手动向量化循环使用对齐的加载,以节省前端微指令,但具有从环路计数器多个环路开销。

.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中的其他链接,尤其是英特尔的优化指南。

还有在标签维基一些好的SSE/SIMD初学者的东西。

+0

对于Clang,我通常使用'-fno-vectorize'。为什么使用'-fno-tree-vectorize'(除了与GCC保持一致)? –

+0

@Zboson:我不知道铿锵对这个选项有不同的名字,谢谢。 –