有时尝试通过添加循环来测试时序来“优化”C++ 代码通常是非常愚蠢的,这就是其中一种情况 :(
你的代码LITERALLY归结为:
int main()
{
TimeStamp start = Clock::now();
TimeStamp end = Clock::now();
double dt = chrono::duration_cast<chrono::nanoseconds>(end-start).count();
cout<<dt<<endl;
return 0;
}
编译器并不愚蠢,因此它决定删除您的内部循环(因为输出未使用,因此循环是多余的)。
即使编译器决定保留循环,每次添加都会发出 3 条内存指令。如果您的内存是 1600Mhz,而您的 CPU 是 3200Mhz,那么您的测试只是向您证明您的内存带宽有限。像这样的分析循环没有用,在分析器中测试真实世界的情况总是会更好......
无论如何,回到有问题的循环。让我们将代码放入编译器资源管理器并尝试一些选项......
https://godbolt.org/z/5SJQHb
F0:只是一个基本的、无聊的 C 循环。
for(int i = 0 ; i < MAX ; i++)
{
out[i] = in1[i] + in2[i];
}
编译器输出这个内部循环:
vmovups ymm0,YMMWORD PTR [rsi+r8*4]
vmovups ymm1,YMMWORD PTR [rsi+r8*4+0x20]
vmovups ymm2,YMMWORD PTR [rsi+r8*4+0x40]
vmovups ymm3,YMMWORD PTR [rsi+r8*4+0x60]
vaddps ymm0,ymm0,YMMWORD PTR [rdx+r8*4]
vaddps ymm1,ymm1,YMMWORD PTR [rdx+r8*4+0x20]
vaddps ymm2,ymm2,YMMWORD PTR [rdx+r8*4+0x40]
vaddps ymm3,ymm3,YMMWORD PTR [rdx+r8*4+0x60]
vmovups YMMWORD PTR [rdi+r8*4],ymm0
vmovups YMMWORD PTR [rdi+r8*4+0x20],ymm1
vmovups YMMWORD PTR [rdi+r8*4+0x40],ymm2
vmovups YMMWORD PTR [rdi+r8*4+0x60],ymm3
展开,每次迭代处理 32xfloats(在 AVX2 中)[+在迭代结束时处理多达 31 个元素的额外代码]
F1:上面的 SSE“优化”循环。 (显然这段代码在循环结束时最多不能处理 3 个元素)
for(int i = 0 ; i < MAX ; i+=4)
{
__m128 a = _mm_load_ps(&in1[i]);
__m128 b = _mm_load_ps(&in2[i]);
__m128 result = _mm_add_ps(a,b);
_mm_store_ps(&out[i],result);
}
这个输出:
vmovaps xmm0,XMMWORD PTR [rsi+rcx*4]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4]
vmovaps XMMWORD PTR [rdi+rcx*4],xmm0
vmovaps xmm0,XMMWORD PTR [rsi+rcx*4+0x10]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x10]
vmovaps XMMWORD PTR [rdi+rcx*4+0x10],xmm0
vmovaps xmm0,XMMWORD PTR [rsi+rcx*4+0x20]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x20]
vmovaps XMMWORD PTR [rdi+rcx*4+0x20],xmm0
vmovaps xmm0,XMMWORD PTR [rsi+rcx*4+0x30]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x30]
vmovaps XMMWORD PTR [rdi+rcx*4+0x30],xmm0
所以编译器已经展开循环,但它已经退回到 SSE(根据要求),所以现在是原始循环性能的一半(不太正确 - 内存带宽将是这里的限制因素)。
F2:您手动展开的 C++ 循环(索引已更正,但仍然无法处理最后 3 个元素)
for(int i = 0 ; i < MAX ; i += 4)
{
out[i + 0] = in1[i + 0] + in2[i + 0];
out[i + 1] = in1[i + 1] + in2[i + 1];
out[i + 2] = in1[i + 2] + in2[i + 2];
out[i + 3] = in1[i + 3] + in2[i + 3];
}
还有输出:
vmovss xmm0,DWORD PTR [rsi+rax*4]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4]
vmovss DWORD PTR [rdi+rax*4],xmm0
vmovss xmm0,DWORD PTR [rsi+rax*4+0x4]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4+0x4]
vmovss DWORD PTR [rdi+rax*4+0x4],xmm0
vmovss xmm0,DWORD PTR [rsi+rax*4+0x8]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4+0x8]
vmovss DWORD PTR [rdi+rax*4+0x8],xmm0
vmovss xmm0,DWORD PTR [rsi+rax*4+0xc]
vaddss xmm0,xmm0,DWORD PTR [rdx+rax*4+0xc]
vmovss DWORD PTR [rdi+rax*4+0xc],xmm0
好吧,这完全无法矢量化!它一次只处理 1 个添加。好吧,这通常归结为指针别名,所以我将从这里更改函数原型:
void func(float* out, const float* in1, const float* in2, int MAX);
对此:(F4)
void func(
float* __restrict out,
const float* __restrict in1,
const float* __restrict in2,
int MAX);
现在编译器将输出向量化的内容:
vmovups xmm0,XMMWORD PTR [rsi+rcx*4]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4]
vmovups xmm1,XMMWORD PTR [rsi+rcx*4+0x10]
vaddps xmm1,xmm1,XMMWORD PTR [rdx+rcx*4+0x10]
vmovups XMMWORD PTR [rdi+rcx*4],xmm0
vmovups xmm0,XMMWORD PTR [rsi+rcx*4+0x20]
vaddps xmm0,xmm0,XMMWORD PTR [rdx+rcx*4+0x20]
vmovups XMMWORD PTR [rdi+rcx*4+0x10],xmm1
vmovups xmm1,XMMWORD PTR [rsi+rcx*4+0x30]
vaddps xmm1,xmm1,XMMWORD PTR [rdx+rcx*4+0x30]
vmovups XMMWORD PTR [rdi+rcx*4+0x20],xmm0
vmovups XMMWORD PTR [rdi+rcx*4+0x30],xmm1
但是这段代码仍然是第一个版本的一半性能......