SSE:使用 _mm_add_epi32 没有看到加速
SSE: not seeing a speedup by using _mm_add_epi32
我希望 SSE 比不使用 SSE 更快。我需要添加一些额外的编译器标志吗?难道我没有看到加速,因为这是整数代码而不是浮点数?
invocation/output
$ 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;
//
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
禁用 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.h
uint32_t
。我必须将其添加到 compile on Godbolt to see the asm。 (假设 clang-5.0 类似于您正在使用的 Apple clang 版本。IDK 如果 Apple 的 clang 暗示默认 -mtune=
选项,但这是有道理的,因为它仅针对 Mac。也是基线 SSSE3对于 x86-64 OS X 上的 64 位是有意义的。)
debug_print 不需要 noinline
。另外,我建议 CYCLE_COUNT
使用不同的名称。这种情况下的周期让我想到了时钟周期,所以称它为 REP_COUNT
或 REPEATS
或其他名称。
将数组放入 main
中的堆栈可能没问题。您确实初始化了两个输入数组(大部分为零,但添加性能与数据无关)。
这很好,因为让它们保持未初始化状态可能意味着每个数组的多个 4k 页被写时复制映射到相同的物理零页,因此您将获得比预期的 L1D 缓存命中数更多.
SSE2 循环应该是 L2 / L3 缓存带宽的瓶颈,因为工作集是 4 * 32kiB * 3 = 384 kiB,所以它大约是 Intel CPUs 中 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 个融合域 uops,并且可以 运行 每 3 个时钟最多 2 个向量,在每个时钟 4 uops 的前端问题带宽上存在瓶颈。
它没有使用对齐加载,因为编译器没有内联到对齐已知的 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 个融合域微指令。它应该比自动矢量化循环快 运行ning。您的计时方法可能导致了问题。
(除非混合加载和存储实际上使它不太理想。自动矢量化循环做了 4 次加载然后 2 次存储。实际上这可以解释它。你的数组是 4kiB 的倍数,并且可能都有相同的相对对齐。所以你可能在这里得到 4k 别名,这意味着 CPU 不确定商店是否与负载重叠。我认为有一个性能计数器你可以检查它。)
另请参阅 Agner Fog's microarch guide (and instruction tables + optimization guide, and other links in the x86 标记 wiki,尤其是英特尔的优化指南。
sse 标签 wiki 中也有一些不错的 SSE/SIMD 初学者资料。
我希望 SSE 比不使用 SSE 更快。我需要添加一些额外的编译器标志吗?难道我没有看到加速,因为这是整数代码而不是浮点数?
invocation/output
$ 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;
//
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
禁用 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.h
uint32_t
。我必须将其添加到 compile on Godbolt to see the asm。 (假设 clang-5.0 类似于您正在使用的 Apple clang 版本。IDK 如果 Apple 的 clang 暗示默认 -mtune=
选项,但这是有道理的,因为它仅针对 Mac。也是基线 SSSE3对于 x86-64 OS X 上的 64 位是有意义的。)
debug_print 不需要 noinline
。另外,我建议 CYCLE_COUNT
使用不同的名称。这种情况下的周期让我想到了时钟周期,所以称它为 REP_COUNT
或 REPEATS
或其他名称。
将数组放入 main
中的堆栈可能没问题。您确实初始化了两个输入数组(大部分为零,但添加性能与数据无关)。
这很好,因为让它们保持未初始化状态可能意味着每个数组的多个 4k 页被写时复制映射到相同的物理零页,因此您将获得比预期的 L1D 缓存命中数更多.
SSE2 循环应该是 L2 / L3 缓存带宽的瓶颈,因为工作集是 4 * 32kiB * 3 = 384 kiB,所以它大约是 Intel CPUs 中 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 个融合域 uops,并且可以 运行 每 3 个时钟最多 2 个向量,在每个时钟 4 uops 的前端问题带宽上存在瓶颈。
它没有使用对齐加载,因为编译器没有内联到对齐已知的 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 个融合域微指令。它应该比自动矢量化循环快 运行ning。您的计时方法可能导致了问题。
(除非混合加载和存储实际上使它不太理想。自动矢量化循环做了 4 次加载然后 2 次存储。实际上这可以解释它。你的数组是 4kiB 的倍数,并且可能都有相同的相对对齐。所以你可能在这里得到 4k 别名,这意味着 CPU 不确定商店是否与负载重叠。我认为有一个性能计数器你可以检查它。)
另请参阅 Agner Fog's microarch guide (and instruction tables + optimization guide, and other links in the x86 标记 wiki,尤其是英特尔的优化指南。
sse 标签 wiki 中也有一些不错的 SSE/SIMD 初学者资料。