点击上方蓝字 江湖评谈关注我们
前言
除非你使用了SkipLocalsInit这种特性,一般的stackalloc分配的任何栈空间都需要JIT进行清零的操作,Zeroing(归零)优化的是JIT里面生成的清零代码。本篇继续来看下这些极致的优化技术。
示例
来看看一个简单的例子:
public void Constant256() => Use(stackalloc byte[256]);
public void Constant1024() => Use(stackalloc byte[1024]);
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Use(Span<byte> span) { }
详细
.NET7里面Constant256和Constant1024的ASM如下:
; Tests.Constant256()
push rbp
sub rsp,40
lea rbp,[rsp+20]
xor eax,eax
mov [rbp+10],rax
mov [rbp+18],rax
mov rax,0A77E4BDA96AD
mov [rbp+8],rax
add rsp,20
mov ecx,10
M00_L00:
push 0
push 0
dec rcx
jne short M00_L00
sub rsp,20
lea rcx,[rsp+20]
mov [rbp+10],rcx
mov dword ptr [rbp+18],100
lea rcx,[rbp+10]
call qword ptr [7FFF3DD37900]; Tests.Use(System.Span`1<Byte>)
mov rcx,0A77E4BDA96AD
cmp [rbp+8],rcx
je short M00_L01
call CORINFO_HELP_FAIL_FAST
M00_L01:
nop
lea rsp,[rbp+20]
pop rbp
ret
; Total bytes of code 110
; Tests.Constant1024()
push rbp
sub rsp,40
lea rbp,[rsp+20]
xor eax,eax
mov [rbp+10],rax
mov [rbp+18],rax
mov rax,606DD723A061
mov [rbp+8],rax
add rsp,20
mov ecx,40
M00_L00:
push 0
push 0
dec rcx
jne short M00_L00
sub rsp,20
lea rcx,[rsp+20]
mov [rbp+10],rcx
mov dword ptr [rbp+18],400
lea rcx,[rbp+10]
call qword ptr [7FFF3DD47900]; Tests.Use(System.Span`1<Byte>)
mov rcx,606DD723A061
cmp [rbp+8],rcx
je short M00_L01
call CORINFO_HELP_FAIL_FAST
M00_L01:
nop
lea rsp,[rbp+20]
pop rbp
ret
; Total bytes of code 110
我们可以看到,在中间,JIT编写了一个清零循环,每次迭代通过将两个8字节的0推入堆栈来清零16字节:
M00_L00:
push 0
push 0
dec rcx
jne short M00_L00
这种循环式的清零,对于性能并不友好,所以.NET8的Constant256如下:
; Tests.Constant256()
push rbp
sub rsp,40
vzeroupper
lea rbp,[rsp+20]
xor eax,eax
mov [rbp+10],rax
mov [rbp+18],rax
mov rax,6281D64D33C3
mov [rbp+8],rax
test [rsp],esp
sub rsp,100
lea rcx,[rsp+20]
vxorps ymm0,ymm0,ymm0
vmovdqu ymmword ptr [rcx],ymm0
vmovdqu ymmword ptr [rcx+20],ymm0
vmovdqu ymmword ptr [rcx+40],ymm0
vmovdqu ymmword ptr [rcx+60],ymm0
vmovdqu ymmword ptr [rcx+80],ymm0
vmovdqu ymmword ptr [rcx+0A0],ymm0
vmovdqu ymmword ptr [rcx+0C0],ymm0
vmovdqu ymmword ptr [rcx+0E0],ymm0
mov [rbp+10],rcx
mov dword ptr [rbp+18],100
lea rcx,[rbp+10]
call qword ptr [7FFEB7D3F498]; Tests.Use(System.Span`1<Byte>)
mov rcx,6281D64D33C3
cmp [rbp+8],rcx
je short M00_L00
call CORINFO_HELP_FAIL_FAST
M00_L00:
nop
lea rsp,[rbp+20]
pop rbp
ret
; Total bytes of code 156
.NET8里面通过ymm0寄存器一次性移动32个字节来进行清零,大大提高性能。
Constant1024在.NET8里面如下:
; Tests.Constant1024()
push rbp
sub rsp,40
lea rbp,[rsp+20]
xor eax,eax
mov [rbp+10],rax
mov [rbp+18],rax
mov rax,0CAF12189F783
mov [rbp],rax
test [rsp],esp
sub rsp,400
lea rcx,[rsp+20]
mov [rbp+8],rcx
xor edx,edx
mov r8d,400
call CORINFO_HELP_MEMSET
mov rcx,[rbp+8]
mov [rbp+10],rcx
mov dword ptr [rbp+18],400
lea rcx,[rbp+10]
call qword ptr [7FFEB7D5F498]; Tests.Use(System.Span`1<Byte>)
mov rcx,0CAF12189F783
cmp [rbp],rcx
je short M00_L00
call CORINFO_HELP_FAIL_FAST
M00_L00:
nop
lea rsp,[rbp+20]
pop rbp
ret
; Total bytes of code 119
也没有用循环清零的方式,而是 通过CORINFO_HELP_MEMSET帮助函数来进行清零
以上两种.NET8里面提高性能的方式如何呢?我们测试如下结果:
Method | Runtime | Mean | Ratio |
---|---|---|---|
Constant256 | .NET 7.0 | 7.927 ns | 1.00 |
Constant256 | .NET 8.0 | 3.181 ns | 0.40 |
Constant1024 | .NET 7.0 | 30.523 ns | 1.00 |
Constant1024 | .NET 8.0 | 8.850 ns | 0.29 |
可以看到.NET8相对于.NET7提升了两到三倍的性能
是不是到这里就优化的可以了呢?并不是,我们还可以继续优化,通过AVX512的zmm0寄存器一次性清64个字节的零。
; Tests.Constant256()
push rbp
sub rsp,40
vzeroupper
lea rbp,[rsp+20]
xor eax,eax
mov [rbp+10],rax
mov [rbp+18],rax
mov rax,992482B435F7
mov [rbp+8],rax
test [rsp],esp
sub rsp,100
lea rcx,[rsp+20]
vxorps ymm0,ymm0,ymm0
vmovdqu32 [rcx],zmm0
vmovdqu32 [rcx+40],zmm0
vmovdqu32 [rcx+80],zmm0
vmovdqu32 [rcx+0C0],zmm0
mov [rbp+10],rcx
mov dword ptr [rbp+18],100
lea rcx,[rbp+10]
call qword ptr [7FFCE555F4B0]; Tests.Use(System.Span`1<Byte>)
mov rcx,992482B435F7
cmp [rbp+8],rcx
je short M00_L00
call CORINFO_HELP_FAIL_FAST
M00_L00:
nop
lea rsp,[rbp+20]
pop rbp
ret
; Total bytes of code 132
; Tests.Use(System.Span`1<Byte>)
ret
; Total bytes of code 1
现在,请注意,我们不再看到使用ymm0的八个vmovdqu指令,而是看到使用zmm0的四个vmovdqu32指令,因为每个移动指令能够清零两倍的内容,每个指令一次处理64个字节。性能进一步提升了。
往期精彩回顾: