为什么 SIMD 比标量对应物慢
Why is SIMD slower than scalar counterpart
这是另一个 SSE is slower than normal code! Why?
类型的问题。
我知道有一堆类似的问题,但他们似乎不符合我的情况。
我正在尝试实现 Miller-Rabin primality test with Montgomery Modular Multiplication 以进行快速模运算。
我尝试以标量和 SIMD 方式实现它,结果发现 SIMD 版本慢了大约 10%。
如果有人想知道 [esp+16] 或 [esp+12] 指向 N
的模逆。
我真的很困惑,一个据称是 1 Latency 1c Throughput 1uops 的指令 psrldq
需要超过 3 Latency 0.5c Throughput 1uops pmuludq
.
下面是 Ryzen 5 3600 上 visual studio 运行 的代码和 运行 时间分析。
关于如何改进 SIMD 代码的任何想法 and/or 为什么它比标量代码慢,我们将不胜感激。
P.S。似乎 运行 时间分析由于某种原因被一条指令关闭了
编辑 1:图片评论有误,我在下面附上修正版:
;------------------------------------------
; Calculate base^d mod x
;
; eax = 1
; esi = x
; edi = bases[eax]
; ebp = d
; while d do
; if d & 1 then eax = (eax * edi) mod x
; edi = (edi*edi) mod x
; d >>= 1
; end
;------------------------------------------
标量代码:
LOOP_MODEXP:
push eax
test ebp, 1
jz @F
mul edi
mov ecx, edx
imul eax, DWORD PTR [esp+16]
mul esi
xor ebx, ebx
sub ecx, edx
cmovs ebx, esi
add ecx, ebx
mov DWORD PTR [esp], ecx
@@:
mov edx, edi
mulx ecx, edx, edi
imul edx, DWORD PTR [esp+16]
mulx eax, ebx, esi
xor ebx, ebx
sub ecx, eax
cmovs ebx, esi
add ecx, ebx
mov edi, ecx
pop eax
shr ebp, 1
jnz LOOP_MODEXP
SIMD 代码
movd xmm2, DWORD PTR [esp+12]
movd xmm3, esi
pshufd xmm2, xmm2, 0
pshufd xmm3, xmm3, 0
movd xmm1, edi
pshufd xmm1, xmm1, 0
movdqa xmm0, xmm1
pinsrd xmm0, eax, 2
LOOP_MODEXP:
movdqa xmm4, xmm0
pmuludq xmm0, xmm1
movdqa xmm1, xmm0
pmuludq xmm0, xmm2
pmuludq xmm0, xmm3
psubd xmm1, xmm0
psrldq xmm1, 4
pxor xmm0, xmm0
pcmpgtd xmm0, xmm1
blendvps xmm0, xmm3, xmm0
paddd xmm0, xmm1
movddup xmm1, xmm0
test ebp, 1
jnz @F
blendps xmm0, xmm4, 4
@@:
shr ebp, 1
jnz LOOP_MODEXP
pextrd eax, xmm0, 2
- 您的 SIMD 代码浪费时间错误预测测试 ebp, 1 / jnz 分支。 SSE 中没有条件移动指令,但您仍然可以使用更多指令优化测试 + 分支,如下所示:
mov ebx, ebp
and ebx, 1
sub ebx, 1
pxor xmm5, xmm5
pinsrd xmm5, ebx, 2
blendvps xmm0, xmm4, xmm5
而不是你的
test ebp, 1
jnz @F
blendps xmm0, xmm4, 4
上面的代码计算 ebx = ( ebp & 1 ) ? 0 : -1;
,将该整数插入零向量的第 3 通道,并在 blendvps
指令中将该向量用于选择器。
- 不需要这条指令:
pcmpgtd xmm0, xmm1
连同上一个和下一个,它计算这个:
xmm0 = _mm_cmplt_epi32( xmm1, _mm_setzero_si128() );
xmm0 = _mm_blendv_ps( xmm0, xmm3, xmm0 );
这是一个等价物:
xmm0 = _mm_blendv_ps( _mm_setzero_si128(), xmm3, xmm1 );
该比较指令针对 xmm1 < 0 比较 int32 通道。这导致这些整数的符号位。 _mm_blendv_ps
指令仅测试 32 位通道中的高位,在此之前您真的不需要比较 xmm1 < 0。
- 除非您需要支持没有 AVX 的 CPU,否则您应该对指令使用 VEX 编码,即使是处理 16 字节向量的代码。您的 SIMD 代码使用传统编码,其中大多数采用 2 个参数并将结果写入第一个参数。大多数 VEX 指令采用 3 个参数并将结果写入另一个参数。这应该摆脱像
movdqa xmm4, xmm0
. 这样的冗余移动指令
这是另一个 SSE is slower than normal code! Why?
类型的问题。
我知道有一堆类似的问题,但他们似乎不符合我的情况。
我正在尝试实现 Miller-Rabin primality test with Montgomery Modular Multiplication 以进行快速模运算。
我尝试以标量和 SIMD 方式实现它,结果发现 SIMD 版本慢了大约 10%。
如果有人想知道 [esp+16] 或 [esp+12] 指向 N
的模逆。
我真的很困惑,一个据称是 1 Latency 1c Throughput 1uops 的指令 psrldq
需要超过 3 Latency 0.5c Throughput 1uops pmuludq
.
下面是 Ryzen 5 3600 上 visual studio 运行 的代码和 运行 时间分析。
关于如何改进 SIMD 代码的任何想法 and/or 为什么它比标量代码慢,我们将不胜感激。
P.S。似乎 运行 时间分析由于某种原因被一条指令关闭了
编辑 1:图片评论有误,我在下面附上修正版:
;------------------------------------------
; Calculate base^d mod x
;
; eax = 1
; esi = x
; edi = bases[eax]
; ebp = d
; while d do
; if d & 1 then eax = (eax * edi) mod x
; edi = (edi*edi) mod x
; d >>= 1
; end
;------------------------------------------
标量代码:
LOOP_MODEXP:
push eax
test ebp, 1
jz @F
mul edi
mov ecx, edx
imul eax, DWORD PTR [esp+16]
mul esi
xor ebx, ebx
sub ecx, edx
cmovs ebx, esi
add ecx, ebx
mov DWORD PTR [esp], ecx
@@:
mov edx, edi
mulx ecx, edx, edi
imul edx, DWORD PTR [esp+16]
mulx eax, ebx, esi
xor ebx, ebx
sub ecx, eax
cmovs ebx, esi
add ecx, ebx
mov edi, ecx
pop eax
shr ebp, 1
jnz LOOP_MODEXP
SIMD 代码
movd xmm2, DWORD PTR [esp+12]
movd xmm3, esi
pshufd xmm2, xmm2, 0
pshufd xmm3, xmm3, 0
movd xmm1, edi
pshufd xmm1, xmm1, 0
movdqa xmm0, xmm1
pinsrd xmm0, eax, 2
LOOP_MODEXP:
movdqa xmm4, xmm0
pmuludq xmm0, xmm1
movdqa xmm1, xmm0
pmuludq xmm0, xmm2
pmuludq xmm0, xmm3
psubd xmm1, xmm0
psrldq xmm1, 4
pxor xmm0, xmm0
pcmpgtd xmm0, xmm1
blendvps xmm0, xmm3, xmm0
paddd xmm0, xmm1
movddup xmm1, xmm0
test ebp, 1
jnz @F
blendps xmm0, xmm4, 4
@@:
shr ebp, 1
jnz LOOP_MODEXP
pextrd eax, xmm0, 2
- 您的 SIMD 代码浪费时间错误预测测试 ebp, 1 / jnz 分支。 SSE 中没有条件移动指令,但您仍然可以使用更多指令优化测试 + 分支,如下所示:
mov ebx, ebp
and ebx, 1
sub ebx, 1
pxor xmm5, xmm5
pinsrd xmm5, ebx, 2
blendvps xmm0, xmm4, xmm5
而不是你的
test ebp, 1
jnz @F
blendps xmm0, xmm4, 4
上面的代码计算 ebx = ( ebp & 1 ) ? 0 : -1;
,将该整数插入零向量的第 3 通道,并在 blendvps
指令中将该向量用于选择器。
- 不需要这条指令:
pcmpgtd xmm0, xmm1
连同上一个和下一个,它计算这个:
xmm0 = _mm_cmplt_epi32( xmm1, _mm_setzero_si128() );
xmm0 = _mm_blendv_ps( xmm0, xmm3, xmm0 );
这是一个等价物:
xmm0 = _mm_blendv_ps( _mm_setzero_si128(), xmm3, xmm1 );
该比较指令针对 xmm1 < 0 比较 int32 通道。这导致这些整数的符号位。 _mm_blendv_ps
指令仅测试 32 位通道中的高位,在此之前您真的不需要比较 xmm1 < 0。
- 除非您需要支持没有 AVX 的 CPU,否则您应该对指令使用 VEX 编码,即使是处理 16 字节向量的代码。您的 SIMD 代码使用传统编码,其中大多数采用 2 个参数并将结果写入第一个参数。大多数 VEX 指令采用 3 个参数并将结果写入另一个参数。这应该摆脱像
movdqa xmm4, xmm0
. 这样的冗余移动指令