为什么我的SSE代码比本地C++代码慢?
首先,我是上海证券交易所的新手。我决定加速我的代码,但它似乎比我的本机代码运行得慢 这是一个计算平方和的示例。在我的Intel i7-6700HQ上,本机代码需要0.43秒,SSE需要0.52秒。那么,瓶颈在哪里为什么我的SSE代码比本地C++代码慢?,c++,sse,simd,C++,Sse,Simd,首先,我是上海证券交易所的新手。我决定加速我的代码,但它似乎比我的本机代码运行得慢 这是一个计算平方和的示例。在我的Intel i7-6700HQ上,本机代码需要0.43秒,SSE需要0.52秒。那么,瓶颈在哪里 inline float squared_sum(const float x, const float y) { return x * x + y * y; } #define USE_SIMD void calculations() { high_resoluti
inline float squared_sum(const float x, const float y)
{
return x * x + y * y;
}
#define USE_SIMD
void calculations()
{
high_resolution_clock::time_point t1, t2;
int result_v = 0;
t1 = high_resolution_clock::now();
alignas(16) float data_x[4];
alignas(16) float data_y[4];
alignas(16) float result[4];
__m128 v_x, v_y, v_res;
for (int y = 0; y < 5120; y++)
{
data_y[0] = y;
data_y[1] = y + 1;
data_y[2] = y + 2;
data_y[3] = y + 3;
for (int x = 0; x < 5120; x++)
{
data_x[0] = x;
data_x[1] = x + 1;
data_x[2] = x + 2;
data_x[3] = x + 3;
#ifdef USE_SIMD
v_x = _mm_load_ps(data_x);
v_y = _mm_load_ps(data_y);
v_x = _mm_mul_ps(v_x, v_x);
v_y = _mm_mul_ps(v_y, v_y);
v_res = _mm_add_ps(v_x, v_y);
_mm_store_ps(result, v_res);
#else
result[0] = squared_sum(data_x[0], data_y[0]);
result[1] = squared_sum(data_x[1], data_y[1]);
result[2] = squared_sum(data_x[2], data_y[2]);
result[3] = squared_sum(data_x[3], data_y[3]);
#endif
result_v += (int)(result[0] + result[1] + result[2] + result[3]);
}
}
t2 = high_resolution_clock::now();
duration<double> time_span1 = duration_cast<duration<double>>(t2 - t1);
std::cout << "Exec time:\t" << time_span1.count() << " s\n";
}
更新:根据注释修复代码。
我正在使用Visual Studio 2017。为x64编译
优化:最大优化速度/O2;
内联功能扩展:任何合适的/Ob2;
偏好大小或速度:偏好快速编码/Ot;
省略帧指针:是/Oy
结论
编译器生成已经优化的代码,所以现在很难再加速了。为了进一步加速代码,您可以做的一件事是并行化
谢谢你的回答。它们基本上是相同的,所以我接受Søren V.Poulsen的答案,因为这是第一个答案。现代编译器是难以置信的机器,如果可能的话,它们已经使用SIMD指令,并且具有正确的编译标志 确定编译器正在做什么的一个通用策略是查看代码的反汇编。如果您不想在自己的机器上执行此操作,可以使用Godbolt:之类的在线服务
一个技巧是避免像您在这里所做的那样存储中间结果。原子值用于确保线程之间的同步,相对而言,这可能需要非常高的计算成本。现代编译器是难以置信的机器,如果可能,将使用SIMD指令,并使用正确的编译标志 确定编译器正在做什么的一个通用策略是查看代码的反汇编。如果您不想在自己的机器上执行此操作,可以使用Godbolt:之类的在线服务
一个技巧是避免像您在这里所做的那样存储中间结果。原子值用于确保线程之间的同步,相对而言,这可能会带来非常高的计算成本。在没有SIMD的情况下查看程序集中编译器的代码
calculations():
pxor xmm2, xmm2
xor edx, edx
movdqa xmm0, XMMWORD PTR .LC0[rip]
movdqa xmm11, XMMWORD PTR .LC1[rip]
movdqa xmm9, XMMWORD PTR .LC2[rip]
movdqa xmm8, XMMWORD PTR .LC3[rip]
movdqa xmm7, XMMWORD PTR .LC4[rip]
.L4:
movdqa xmm5, xmm0
movdqa xmm4, xmm0
cvtdq2ps xmm6, xmm0
movdqa xmm10, xmm0
paddd xmm0, xmm7
cvtdq2ps xmm3, xmm0
paddd xmm5, xmm9
paddd xmm4, xmm8
cvtdq2ps xmm5, xmm5
cvtdq2ps xmm4, xmm4
mulps xmm6, xmm6
mov eax, 5120
paddd xmm10, xmm11
mulps xmm5, xmm5
mulps xmm4, xmm4
mulps xmm3, xmm3
pxor xmm12, xmm12
.L2:
movdqa xmm1, xmm12
cvtdq2ps xmm14, xmm12
mulps xmm14, xmm14
movdqa xmm13, xmm12
paddd xmm12, xmm7
cvtdq2ps xmm12, xmm12
paddd xmm1, xmm9
cvtdq2ps xmm0, xmm1
mulps xmm0, xmm0
paddd xmm13, xmm8
cvtdq2ps xmm13, xmm13
sub eax, 1
mulps xmm13, xmm13
addps xmm14, xmm6
mulps xmm12, xmm12
addps xmm0, xmm5
addps xmm13, xmm4
addps xmm12, xmm3
addps xmm0, xmm14
addps xmm0, xmm13
addps xmm0, xmm12
movdqa xmm12, xmm1
cvttps2dq xmm0, xmm0
paddd xmm2, xmm0
jne .L2
add edx, 1
movdqa xmm0, xmm10
cmp edx, 1280
jne .L4
movdqa xmm0, xmm2
psrldq xmm0, 8
paddd xmm2, xmm0
movdqa xmm0, xmm2
psrldq xmm0, 4
paddd xmm2, xmm0
movd eax, xmm2
ret
main:
xor eax, eax
ret
_GLOBAL__sub_I_calculations():
sub rsp, 8
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
add rsp, 8
jmp __cxa_atexit
.LC0:
.long 0
.long 1
.long 2
.long 3
.LC1:
.long 4
.long 4
.long 4
.long 4
.LC2:
.long 1
.long 1
.long 1
.long 1
.LC3:
.long 2
.long 2
.long 2
.long 2
.LC4:
.long 3
.long 3
.long 3
.long 3
您的SIMD代码生成:
calculations():
pxor xmm5, xmm5
xor eax, eax
mov r8d, 1
movabs rdi, -4294967296
cvtsi2ss xmm5, eax
.L4:
mov r9d, r8d
mov esi, 1
movd edx, xmm5
pxor xmm5, xmm5
pxor xmm4, xmm4
mov ecx, edx
mov rdx, QWORD PTR [rsp-24]
cvtsi2ss xmm5, r8d
add r8d, 1
cvtsi2ss xmm4, r8d
and rdx, rdi
or rdx, rcx
pxor xmm2, xmm2
mov edx, edx
movd ecx, xmm5
sal rcx, 32
or rdx, rcx
mov QWORD PTR [rsp-24], rdx
movd edx, xmm4
pxor xmm4, xmm4
mov ecx, edx
mov rdx, QWORD PTR [rsp-16]
and rdx, rdi
or rdx, rcx
lea ecx, [r9+2]
mov edx, edx
cvtsi2ss xmm4, ecx
movd ecx, xmm4
sal rcx, 32
or rdx, rcx
mov QWORD PTR [rsp-16], rdx
movaps xmm4, XMMWORD PTR [rsp-24]
mulps xmm4, xmm4
.L2:
movd edx, xmm2
mov r10d, esi
pxor xmm2, xmm2
pxor xmm7, xmm7
mov ecx, edx
mov rdx, QWORD PTR [rsp-40]
cvtsi2ss xmm2, esi
add esi, 1
and rdx, rdi
cvtsi2ss xmm7, esi
or rdx, rcx
mov ecx, edx
movd r11d, xmm2
movd edx, xmm7
sal r11, 32
or rcx, r11
pxor xmm7, xmm7
mov QWORD PTR [rsp-40], rcx
mov ecx, edx
mov rdx, QWORD PTR [rsp-32]
and rdx, rdi
or rdx, rcx
lea ecx, [r10+2]
mov edx, edx
cvtsi2ss xmm7, ecx
movd ecx, xmm7
sal rcx, 32
or rdx, rcx
mov QWORD PTR [rsp-32], rdx
movaps xmm0, XMMWORD PTR [rsp-40]
mulps xmm0, xmm0
addps xmm0, xmm4
movaps xmm3, xmm0
movaps xmm1, xmm0
shufps xmm3, xmm0, 85
addss xmm1, xmm3
movaps xmm3, xmm0
unpckhps xmm3, xmm0
shufps xmm0, xmm0, 255
addss xmm1, xmm3
addss xmm0, xmm1
cvttss2si edx, xmm0
add eax, edx
cmp r10d, 5120
jne .L2
cmp r9d, 5120
jne .L4
rep ret
main:
xor eax, eax
ret
_GLOBAL__sub_I_calculations():
sub rsp, 8
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
add rsp, 8
jmp __cxa_atexit
请注意,编译器的版本使用cvtdq2ps、PADD、cvtdq2ps、mulps、addps和cvttps2dq。所有这些都是SIMD指令。通过有效地组合它们,编译器可以生成快速代码
相反,您的代码生成了许多add、and、cvtsi2ss、lea、mov、movd、or、pxor、sal等非SIMD指令
我怀疑编译器在处理数据类型转换和数据重新排列方面比您做得更好,这使它能够更有效地安排数学运算。在没有SIMD的情况下查看汇编程序中的编译器代码
calculations():
pxor xmm2, xmm2
xor edx, edx
movdqa xmm0, XMMWORD PTR .LC0[rip]
movdqa xmm11, XMMWORD PTR .LC1[rip]
movdqa xmm9, XMMWORD PTR .LC2[rip]
movdqa xmm8, XMMWORD PTR .LC3[rip]
movdqa xmm7, XMMWORD PTR .LC4[rip]
.L4:
movdqa xmm5, xmm0
movdqa xmm4, xmm0
cvtdq2ps xmm6, xmm0
movdqa xmm10, xmm0
paddd xmm0, xmm7
cvtdq2ps xmm3, xmm0
paddd xmm5, xmm9
paddd xmm4, xmm8
cvtdq2ps xmm5, xmm5
cvtdq2ps xmm4, xmm4
mulps xmm6, xmm6
mov eax, 5120
paddd xmm10, xmm11
mulps xmm5, xmm5
mulps xmm4, xmm4
mulps xmm3, xmm3
pxor xmm12, xmm12
.L2:
movdqa xmm1, xmm12
cvtdq2ps xmm14, xmm12
mulps xmm14, xmm14
movdqa xmm13, xmm12
paddd xmm12, xmm7
cvtdq2ps xmm12, xmm12
paddd xmm1, xmm9
cvtdq2ps xmm0, xmm1
mulps xmm0, xmm0
paddd xmm13, xmm8
cvtdq2ps xmm13, xmm13
sub eax, 1
mulps xmm13, xmm13
addps xmm14, xmm6
mulps xmm12, xmm12
addps xmm0, xmm5
addps xmm13, xmm4
addps xmm12, xmm3
addps xmm0, xmm14
addps xmm0, xmm13
addps xmm0, xmm12
movdqa xmm12, xmm1
cvttps2dq xmm0, xmm0
paddd xmm2, xmm0
jne .L2
add edx, 1
movdqa xmm0, xmm10
cmp edx, 1280
jne .L4
movdqa xmm0, xmm2
psrldq xmm0, 8
paddd xmm2, xmm0
movdqa xmm0, xmm2
psrldq xmm0, 4
paddd xmm2, xmm0
movd eax, xmm2
ret
main:
xor eax, eax
ret
_GLOBAL__sub_I_calculations():
sub rsp, 8
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
add rsp, 8
jmp __cxa_atexit
.LC0:
.long 0
.long 1
.long 2
.long 3
.LC1:
.long 4
.long 4
.long 4
.long 4
.LC2:
.long 1
.long 1
.long 1
.long 1
.LC3:
.long 2
.long 2
.long 2
.long 2
.LC4:
.long 3
.long 3
.long 3
.long 3
您的SIMD代码生成:
calculations():
pxor xmm5, xmm5
xor eax, eax
mov r8d, 1
movabs rdi, -4294967296
cvtsi2ss xmm5, eax
.L4:
mov r9d, r8d
mov esi, 1
movd edx, xmm5
pxor xmm5, xmm5
pxor xmm4, xmm4
mov ecx, edx
mov rdx, QWORD PTR [rsp-24]
cvtsi2ss xmm5, r8d
add r8d, 1
cvtsi2ss xmm4, r8d
and rdx, rdi
or rdx, rcx
pxor xmm2, xmm2
mov edx, edx
movd ecx, xmm5
sal rcx, 32
or rdx, rcx
mov QWORD PTR [rsp-24], rdx
movd edx, xmm4
pxor xmm4, xmm4
mov ecx, edx
mov rdx, QWORD PTR [rsp-16]
and rdx, rdi
or rdx, rcx
lea ecx, [r9+2]
mov edx, edx
cvtsi2ss xmm4, ecx
movd ecx, xmm4
sal rcx, 32
or rdx, rcx
mov QWORD PTR [rsp-16], rdx
movaps xmm4, XMMWORD PTR [rsp-24]
mulps xmm4, xmm4
.L2:
movd edx, xmm2
mov r10d, esi
pxor xmm2, xmm2
pxor xmm7, xmm7
mov ecx, edx
mov rdx, QWORD PTR [rsp-40]
cvtsi2ss xmm2, esi
add esi, 1
and rdx, rdi
cvtsi2ss xmm7, esi
or rdx, rcx
mov ecx, edx
movd r11d, xmm2
movd edx, xmm7
sal r11, 32
or rcx, r11
pxor xmm7, xmm7
mov QWORD PTR [rsp-40], rcx
mov ecx, edx
mov rdx, QWORD PTR [rsp-32]
and rdx, rdi
or rdx, rcx
lea ecx, [r10+2]
mov edx, edx
cvtsi2ss xmm7, ecx
movd ecx, xmm7
sal rcx, 32
or rdx, rcx
mov QWORD PTR [rsp-32], rdx
movaps xmm0, XMMWORD PTR [rsp-40]
mulps xmm0, xmm0
addps xmm0, xmm4
movaps xmm3, xmm0
movaps xmm1, xmm0
shufps xmm3, xmm0, 85
addss xmm1, xmm3
movaps xmm3, xmm0
unpckhps xmm3, xmm0
shufps xmm0, xmm0, 255
addss xmm1, xmm3
addss xmm0, xmm1
cvttss2si edx, xmm0
add eax, edx
cmp r10d, 5120
jne .L2
cmp r9d, 5120
jne .L4
rep ret
main:
xor eax, eax
ret
_GLOBAL__sub_I_calculations():
sub rsp, 8
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
add rsp, 8
jmp __cxa_atexit
请注意,编译器的版本使用cvtdq2ps、PADD、cvtdq2ps、mulps、addps和cvttps2dq。所有这些都是SIMD指令。通过有效地组合它们,编译器可以生成快速代码
相反,您的代码生成了许多add、and、cvtsi2ss、lea、mov、movd、or、pxor、sal等非SIMD指令
我怀疑编译器在处理数据类型转换和数据重新排列方面比您做得更好,这使它能够更有效地安排数学。编译器。将来,请准备好您的代码以进行复制/粘贴。这意味着修复诸如tbb而不是std和endif而不是endif之类的类型。@RaymondChen:我推测tbb是英特尔线程构建块使用的名称空间,而不是键入错误的std。这不是不可能的,但会非常困难。一种方法是优化算法,而不是尝试优化算法的步骤。例如,结果向量总是{x*x+y+y,x+1*x+1+y+1*y+1,x+2*x+2+y+2*y+2,x+3*x+3+y+3*y+3}。y部分在循环中不会改变,因此您不必继续计算它,而x部分可以减少强度。实际上,结果总是一样的,所以你可以预先计算整个过程,将result_v设置为最终答案,而不运行任何循环。@user1554270确保你的性能测试实际上与你的实际用例相似。如果您动态生成数据或从内存中读取数据,则会产生显著的差异。此外,编译器有时足够聪明,可以在编译时计算整个循环,特别是如果你在任何地方都使用整数的话。@chtz:是的,但GCC合同这是正确的技术术语,而不是崩溃,oops到FMA,即使没有-ffast数学。这是合法的,在一个表达式在ISO C,我假设C++,但不是严格的法律跨表达式如嵌套本质或分配到一个TMP变量在一个单独的声明。默认情况下,编译器允许使用pragma STDC FP_契约。但是海湾合作委员会仍然在这里改变规则。不过,这通常是件好事。编译器。将来,请准备好您的代码以进行复制/粘贴
. 这意味着修复诸如tbb而不是std和endif而不是endif之类的类型。@RaymondChen:我推测tbb是英特尔线程构建块使用的名称空间,而不是键入错误的std。这不是不可能的,但会非常困难。一种方法是优化算法,而不是尝试优化算法的步骤。例如,结果向量总是{x*x+y+y,x+1*x+1+y+1*y+1,x+2*x+2+y+2*y+2,x+3*x+3+y+3*y+3}。y部分在循环中不会改变,因此您不必继续计算它,而x部分可以减少强度。实际上,结果总是一样的,所以你可以预先计算整个过程,将result_v设置为最终答案,而不运行任何循环。@user1554270确保你的性能测试实际上与你的实际用例相似。如果您动态生成数据或从内存中读取数据,则会产生显著的差异。此外,编译器有时足够聪明,可以在编译时计算整个循环,特别是如果你在任何地方都使用整数的话。@chtz:是的,但GCC合同这是正确的技术术语,而不是崩溃,oops到FMA,即使没有-ffast数学。这是合法的,在一个表达式在ISO C,我假设C++,但不是严格的法律跨表达式如嵌套本质或分配到一个TMP变量在一个单独的声明。默认情况下,编译器允许使用pragma STDC FP_契约。但是海湾合作委员会仍然在这里改变规则。不过,这通常是件好事。原子是多线程优化留下的。把它拿走了,好的。原子是多线程优化留下的。删除了它。所以编译器生成的代码已经过SIMD优化,注入SSE内部函数不会加快它的速度?这取决于这是一个公平的答案。当然,在某些情况下,您可以通过自己添加SIMD操作来优化性能,但是在许多情况下,现代编译器可以在不受您干扰的情况下完成出色的优化工作。@SørenV.Poulsen OK。我不太擅长汇编,但看起来编译器会翻译:v_x=\umm\umul\upsv\ux,v_x;进入这5条指令:movaps xmm0、XMMWORD PTR v_x$[rsp]mulps xmm0、XMMWORD PTR v_x$[rsp]movaps XMMWORD PTR$T8[rsp],xmm0 movaps XMMWORD PTR$T8[rsp]movaps XMMWORD PTR v_x$[rsp],xmm0为什么在内存之间移动这么多?有必要吗?或者它是一个坏的优化?你必须考虑SIMD操作只有在处理器内有特殊的寄存器才能启用。将值移入和移出这些寄存器会带来成本,一般来说,寄存器中保存的数据越长越好。仅仅为了几次乘法就将数据移入/移出SSE寄存器可能不值得。编译器将为您考虑这些问题。@user1554270:如果您使用-O2或-O3甚至-O1进行编译,编译器将添加SIMD。使用-march=native将允许它使用更多的SIMD。所以编译器生成的代码已经过SIMD优化,注入SSE intrinsic不会使其加速更多?这取决于这里的答案是否合理。当然,在某些情况下,您可以通过自己添加SIMD操作来优化性能,但是在许多情况下,现代编译器可以在不受您干扰的情况下完成出色的优化工作。@SørenV.Poulsen OK。我不太擅长汇编,但看起来编译器会翻译:v_x=\umm\umul\upsv\ux,v_x;进入这5条指令:movaps xmm0、XMMWORD PTR v_x$[rsp]mulps xmm0、XMMWORD PTR v_x$[rsp]movaps XMMWORD PTR$T8[rsp],xmm0 movaps XMMWORD PTR$T8[rsp]movaps XMMWORD PTR v_x$[rsp],xmm0为什么在内存之间移动这么多?有必要吗?或者它是一个坏的优化?你必须考虑SIMD操作只有在处理器内有特殊的寄存器才能启用。将值移入和移出这些寄存器会带来成本,一般来说,寄存器中保存的数据越长越好。仅仅为了几次乘法就将数据移入/移出SSE寄存器可能不值得。编译器将为您考虑这些问题。@user1554270:如果您使用-O2或-O3甚至-O1进行编译,编译器将添加SIMD。使用-march=native将允许它使用更多的SIMD。