前言
branch(分支)顾名思义:代码通过判断条件形成的执行情况,比如if/else这种判断分支。分支有用的代码是非常有意义的,它意味着是程序不可分割的部分。但是分支无意义的代码同样副作用巨大,比如一个永远不会执行的分支却一直被CLR加载,被JIT编译,被CPU识别,这种开销是巨大的。现代化的硬件,通过流水线技术,预测下一个指令执行的是谁,并且在上一个指令尚未执行完成,下一个指令已经被读取,解码,加载了。这也是一种巨大的开销,为了减少这种开销。目前的技术情况,编译器能做的是努力将分支最小化。本篇来看下。
原文:.NET8极致性能优化Branch ,作者:江湖评谈,欢迎关注。
![]()
概述
有一种简单的减少分支影响性能的方法就是完全的删除分支,但是这似乎不可能。比如多个分支通向同一个结果,我们可以通过一种方式找到冗余的分支,全部删掉,只留下一个分支。这是目前的一种优化方案。
public class Tests{ private static readonly Random s_rand = new(); private readonly string _text = "hello world!";
[Params(1.0, 0.5)] public double Probability { get; set; }
public ReadOnlySpan<char> TrySlice() => SliceOrDefault(_text.AsSpan(), s_rand.NextDouble() < Probability ? 3 : 20);
[MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan<char> SliceOrDefault(ReadOnlySpan<char> span, int i) { if ((uint)i < (uint)span.Length) { return span.Slice(i); }
return default; }}
TrySlice在.NET7上的ASM如下:
; Tests.TrySlice() push rdi
push rsi push rbp push rbx sub rsp,28 vzeroupper mov rdi,rcx mov rsi,rdx mov rcx,[rdi+8] test rcx,rcx je short M00_L01 lea rbx,[rcx+0C] mov ebp,[rcx+8]M00_L00: mov rcx,1EBBFC01FA0 mov rcx,[rcx] mov rcx,[rcx+8] mov rax,[rcx] mov rax,[rax+48] call qword ptr [rax+20] vmovsd xmm1,qword ptr [rdi+10] vucomisd xmm1,xmm0 ja short M00_L02 mov eax,14 jmp short M00_L03M00_L01: xor ebx,ebx xor ebp,ebp jmp short M00_L00M00_L02: mov eax,3M00_L03: cmp eax,ebp jae short M00_L04 cmp eax,ebp ja short M00_L06 mov edx,eax lea rdx,[rbx+rdx*2] sub ebp,eax jmp short M00_L05M00_L04: xor edx,edx xor ebp,ebpM00_L05: mov [rsi],rdx mov [rsi+8],ebp mov rax,rsi add rsp,28 pop rbx pop rbp pop rsi pop rdi retM00_L06: call qword ptr [7FF999FEB498] int 3; Total bytes of code 136
看下M00_L03
M00_L03: cmp eax,ebp
jae short M00_L04 cmp eax,ebp ja short M00_L06 mov edx,eax lea rdx,[rbx+rdx*2]
eax中已经被放置了3或者0x14,把它与dbp(也就是span.length)进行比较,这里有两个比较
cmp eax,ebp //看这个cmp jae short M00_L04 cmp eax,ebp //以及这个cmp 它们完全一样 比较
两个一样的cmp指令,前者cmp是if条件判断,后者cmp是span.Slice内联的机器码。所以这可以进行优化。在.NET8里面一个cmp分支被消除掉了:
M00_L04: cmp eax,ebp jae short M00_L07 mov ecx,eax lea rdx,[rdi+rcx*2]
另外一种优化方式是,可以用简单的位操作技术来避免分支。
public class Tests{
[Arguments(42, 84)] public bool BothGreaterThanOrEqualZero(int i, int j) => i >= 0 && j >= 0;}
.NET7里面BothGreaterThanOrEqualZero的ASM如下:
; Tests.BothGreaterThanOrEqualZero(Int32, Int32) test edx,edx jl short M00_L00 mov eax,r8d not eax shr eax,1F retM00_L00: xor eax,eax ret; Total bytes of code 16; Tests.BothGreaterThanOrEqualZero(Int32, Int32) test edx,edx jl short M00_L00 mov eax,r8d not eax shr eax,1F retM00_L00: xor eax,eax ret; Total bytes of code 16
以上代码的&&符号可以用位操作符号来替代,可以将他们重写为等效的(i | j) >= 0。现在.NET8如下:
; Tests.BothGreaterThanOrEqualZero(Int32, Int32) or edx,r8d mov eax,edx not eax shr eax,1F ret; Total bytes of code 11
如上所述,分支完全给避免掉了,不用判断,用位操作替代。