gpt4 book ai didi

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

转载 作者:太空宇宙 更新时间:2023-11-04 06:15:32 26 4
gpt4 key购买 nike

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

苏姆2.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来测试我想测试的内容,标量整数与传统的SSEpaddd。(它在gcc和clang工作。在宗族中,AFAIK是特定宗族的同义词
顺便说一句,在同一个可执行文件中对两者进行计时会损害第一个可执行文件,因为CPU不会立即过渡到完全turbo。在CPU达到全速之前,您可能已经进入了代码的计时部分。(因此,使用-fno-vectorize背靠背运行几次。
在Linux上,我将使用for i in {1..10}; do time ./a.out; done使用性能计数器运行它5次(我将它拆分,以便一次运行测试一次或另一次,以便查看整个运行的性能计数器)
代码评审:
你忘记了perf stat -r5 ./a.out的意思。我得加上这个才能把它加到compile on Godbolt to see the asm。(假设clang-5.0类似于您正在使用的Apple clang版本。IDK如果苹果的clang意味着默认的stdint.h选项,但这是有意义的,因为它只针对Mac。对于x86-64 OS X上的64位系统,基线SSSE3也是有意义的。)
调试打印时不需要uint32_t。另外,我建议给-mtune=取一个不同的名字。在这种情况下,周期会让我想到时钟周期,所以称之为noinlineCYCLE_COUNT或其他。
将数组放在REP_COUNT中的堆栈上可能没问题。您确实初始化了这两个输入数组(大部分为零,但是add性能不依赖于数据)。
这很好,因为让它们未初始化可能意味着每个数组的多个4k页在写入时被复制映射到同一个物理零页,因此您将获得超过预期的L1D缓存命中数。
SSE2循环应该会成为二级/三级缓存带宽的瓶颈,因为工作设置为4*32kiB*3=384kib,所以大约是Intel CPU中256kiB二级缓存的1.5倍。
clang可能会展开它的自动矢量化循环,而不是手动的内部循环。这可能解释了更好的性能,因为只有16B矢量(不是32B AVX2)可能不会饱和缓存带宽,如果你没有得到2个负载+每个时钟1个存储。
更新:实际上循环开销是非常极端的,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。
它没有使用对齐加载,因为编译器没有这些信息,而没有内联到已知对齐的 REPEATS中,并且您不能保证与 main或独立函数中的任何内容对齐。对齐加载(或AVX)将允许 main使用内存操作数,而不是单独的 p = __builtin_assume_aligned(p, 16)加载。
手动矢量化的循环使用对齐的负载来保存前端UOP,但是循环计数器有更多的循环开销。
.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个融合域UOP。它应该比自动矢量化循环运行得更快。你的计时方法可能导致了这个问题。
(除非混合负载和存储实际上使其不那么理想。自动矢量化循环执行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/46761679/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com