我希望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.h
的uint32_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_COUNT
或REPEATS
或其他名称。
将数组放在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和x86标签Wiki中的其他链接,尤其是英特尔的优化指南。
sse标签Wiki中也有一些不错的SSE / SIMD初学者资料。
关于c - SSE:使用_mm_add_epi32看不到加速,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59233266/