在.Net 7中性能改进-消除边界检查

前言
本文是 Performance Improvements in .NET 7 Bounds Check Elimination(消除边界检查)部分的翻译.下面开始正文:
//原文地址: ***/dotnet/performance_improvements_in_net_7/.Net最吸引人的地方之一是它的安全性.运行时保护对数组、字符串和跨范围(Span)的访问,这样您就不会意外地破坏内存;如果这样任意读取/写入内存,则会出现异常.当然,这不是魔法;这是由JIT在每次这些数据结构建立索引时插入边界检查来完成的.例如,这个:
[MethodImpl(MethodImplOptions.NoInlining)]static int Read0thElement(int[] array) => array[0];生成汇编代码为:
G_M000_IG01: // offset=0000H 4883EC28 sub rsp, 40G_M000_IG02: // offset=0004H 83790800 cmp dword ptr [rcx+08H], 0 7608 jbe SHORT G_M000_IG04 8B4110 mov eax, dword ptr [rcx+10H]G_M000_IG03: // offset=000DH 4883C428 add rsp, 40 C3 retG_M000_IG04: // offset=0012H E8E9A0C25F call CORINFO_HELP_RNGCHKFAIL CC int3数组在rcx寄存器中传递给这个方法,指向对象中的方法表指针,数组的长度存储在对象中的方法表指针之后(在64位进程中为8字节).因此cmp dword ptr [rcx+08H], 0指令读取数组的长度并将长度与0比较;这是有意义的,因为长度不能是负的,我们试图访问第0个元素,所以只要长度不是0,数组就有足够的元素让我们访问它的第0个元素.在长度为0的情况下,代码跳转到函数的末尾,其中包含调用CORINFO_HELP_RNGCHKFAIL;这是一个JIT helper(辅助函数),它会抛出IndexOutOfRangeException.如果长度足够,然而,它然后读取存储在数组数据开始的int,这在64位是16字节(0x10)超过指针(mov eax, dword ptr [rcx+10H]).
虽然这些边界检查本身并不是非常昂贵,但如果它们很多,它们的成本就会增加.因此,虽然JIT需要确保“安全”访问不会超出边界,但它也试图证明某些访问不会超出边界,在这种情况下,它不必发出边界检查,因为它知道这是多余的.在.Net的每一个版本中,都添加了越来越多的案例来消除这些边界检查, .Net 7中也不例外.
例如,来自@anthonycanino的dotnet/runtime#61662使JIT能够理解各种形式的二进制操作,作为范围检查的一部分.考虑这个方法:
[MethodImpl(MethodImplOptions.NoInlining)]private static ushort[]? Convert(ReadOnlySpan<byte> bytes){ if (bytes.Length != 16) { return null; } var result = new ushort[8]; for (int i = 0; i < result.Length; i++) { result[i] = (ushort)(bytes[i * 2] * 256 + bytes[i * 2 + 1]); } return result;}它验证输入span为16字节长,然后创建一个new ushort[8]数组,其中数组中的每个ushort组合两个输入字节.为此,它将遍历输出数组,并使用i *2和i* 2 + 1作为下标来索引字节数组.在.Net 6中,每一个索引操作都会导致边界检查,汇编代码如下:
cmp r8d,10 jae short G_M000_IG04 movsxd r8,r8d其中G_M000_IG04是我们现在熟悉的CORINFO_HELP_RNGCHKFAIL.但是在.Net 7中,我们得到了这个方法汇编代码:
G_M000_IG01: // offset=0000H 56 push rsi 4883EC20 sub rsp, 32G_M000_IG02: // offset=0005H 488B31 mov rsi, bword ptr [rcx] 8B4908 mov ecx, dword ptr [rcx+08H] 83F910 cmp ecx, 16 754C jne SHORT G_M000_IG05 48B9302F542FFC7F0000 mov rcx, 0x7FFC2F542F30 BA08000000 mov edx, 8 E80C1EB05F call CORINFO_HELP_NEWARR_1_VC 33D2 xor edx, edx align [0 bytes for IG03]G_M000_IG03: // offset=0026H 8D0C12 lea ecx, [rdx+rdx] 448BC1 mov r8d, ecx FFC1 inc ecx 458BC0 mov r8d, r8d 460FB60406 movzx r8, byte ptr [rsi+r8] 41C1E008 shl r8d, 8 8BC9 mov ecx, ecx 0FB60C0E movzx rcx, byte ptr [rsi+rcx] 4103C8 add ecx, r8d 0FB7C9 movzx rcx, cx 448BC2 mov r8d, edx 6642894C4010 mov word ptr [rax+2*r8+10H], cx FFC2 inc edx 83FA08 cmp edx, 8 7CD0 jl SHORT G_M000_IG03G_M000_IG04: ;; offset=0056H 4883C420 add rsp, 32 5E pop rsi C3 retG_M000_IG05: // offset=005CH 33C0 xor rax, raxG_M000_IG06: // offset=005EH 4883C420 add rsp, 32 5E pop rsi C3 ret// Total bytes of code 100

推荐阅读