前言:
今天兄弟们对“正则表达式生成器”大体比较关切,咱们都想要了解一些“正则表达式生成器”的相关文章。那么小编在网络上汇集了一些有关“正则表达式生成器””的相关知识,希望小伙伴们能喜欢,同学们一起来了解一下吧!前言
本文是 Performance Improvements in .NET 7 Bounds Check Elimination(消除边界检查)部分的翻译.下面开始正文:
//原文地址:
.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
没有边界检查,这是很容易看到的,因为在方法的最后没有显示call CORINFO_HELP_RNGCHKFAIL.通过这个提交, JIT能够理解某些乘法和移位操作的影响,以及它们对数据结构边界的关系.因为它可以看到结果数组的长度是8,并且循环从0迭代到那个排他的上界,它知道i将始终在[0,7]范围内,这意味着i *2将始终在 [0,14]范围内,i* 2 + 1将始终在[0,15]范围内.因此,它能够证明不需要边界检查.
dotnet/runtime#61569和dotnet/runtime#62864也有助于消除边界检查,当处理常量字符串和Span初始化从静态RVA(“相对虚拟地址”静态字段,基本上是模块数据段中的静态字段).例如,考虑以下基准测试:
[Benchmark][Arguments(1)]public char GetChar(int i){ const string Text = "hello"; return (uint)i < Text.Length ? Text[i] : '\0';}
在.Net 6中,我们得到了这样的汇编代码:
// Program.GetChar(Int32) sub rsp,28 mov eax,edx cmp rax,5 jl short M00_L00 xor eax,eax add rsp,28 retM00_L00: cmp edx,5 jae short M00_L01 mov rax,2278B331450 mov rax,[rax] movsxd rdx,edx movzx eax,word ptr [rax+rdx*2+0C] add rsp,28 retM00_L01: call CORINFO_HELP_RNGCHKFAIL int 3// Total bytes of code 56
这一开始是有意义的:JIT显然能够看到Text的长度是5,所以它实现了 (uint)i < Text.长度检查通过做 cmp rax,5,如果 i作为一个无符号值大于或等于5,它然后零'返回值(返回 '\0')和退出.如果长度小于5(在这种情况下,由于无符号比较,它至少是0),然后跳转到M00_L00从字符串中读取值…但我们随后看到另一个 cmp针对5,这一次是作为范围检查的一部分.因此,即使JIT知道索引在边界内,它也不能删除边界检查.在.Net 7中,我们得到这样的结果:
// Program.GetChar(Int32) cmp edx,5 jb short M00_L00 xor eax,eax retM00_L00: mov rax,2B0AF002530 mov rax,[rax] mov edx,edx movzx eax,word ptr [rax+rdx*2+0C] ret// Total bytes of code 29
生成汇编代码变得很好.
dotnet/runtime#67141是一个很好的例子,说明了不断发展的系统需求如何推动JIT的特定优化.Regex(正则表达式)编译器和正则表达式源代码生成器通过使用存储在字符串中的位图查找来处理正则表达式字符类的某些情况.例如,为了确定 char c是否在字符类“[a-Za-z0-9_]”(将匹配下划线或任何ASCII字母或数字),实现最终生成一个类似以下方法体的表达式:
Benchmark][Arguments('a')]public bool IsInSet(char c) => c < 128 && ("\0\0\0\u03FF\uFFFE\u87FF\uFFFE\u07FF"[c >> 4] & (1 << (c & 0xF))) != 0;
该实现将8个字符的字符串作为128位查找表处理.如果已知字符在范围内(例如它实际上是一个7位值),那么它将使用该值的前3位索引到字符串的8个元素中,并使用后4位选择该元素的16位中的一个,从而告诉我们这个输入字符是否在集合中.在.Net 6中,即使我们知道字符在字符串的范围内,JIT也无法通过长度比较或位移位来识别.
// Program.IsInSet(Char) sub rsp,28 movzx eax,dx cmp eax,80 jge short M00_L00 mov edx,eax sar edx,4 cmp edx,8 jae short M00_L01 mov rcx,299835A1518 mov rcx,[rcx] movsxd rdx,edx movzx edx,word ptr [rcx+rdx*2+0C] and eax,0F bt edx,eax setb al movzx eax,al add rsp,28 retM00_L00: xor eax,eax add rsp,28 retM00_L01: call CORINFO_HELP_RNGCHKFAIL int 3// Total bytes of code 75
前面提到的提交负责长度检查.这个提交负责位偏移.所以在.Net 7中,我们得到了这样的代码:
// Program.IsInSet(Char) movzx eax,dx cmp eax,80 jge short M00_L00 mov edx,eax sar edx,4 mov rcx,197D4800608 mov rcx,[rcx] mov edx,edx movzx edx,word ptr [rcx+rdx*2+0C] and eax,0F bt edx,eax setb al movzx eax,al retM00_L00: xor eax,eax ret// Total bytes of code 51
注意,没有调用CORINFO_HELP_RNGCHKFAIL.正如您可能猜到的,这种检查在Regex中经常发生,这使它成为一个非常有用的添加.
当谈到数组访问时,边界检查是一个明显的开销,但它们不是唯一的.还需要尽可能使用开销较低的指令.在.Net 6中,使用如下方法:
[MethodImpl(MethodImplOptions.NoInlining)]private static int Get(int[] values, int i) => values[i];
将生成如下汇编代码:
// Program.Get(Int32[], Int32) sub rsp,28 cmp edx,[rcx+8] jae short M01_L00 movsxd rax,edx mov eax,[rcx+rax*4+10] add rsp,28 retM01_L00: call CORINFO_HELP_RNGCHKFAIL int 3// Total bytes of code 27
这看起来与我们之前的讨论相当熟悉;JIT加载数组的长度([rcx+8]),并将其与i的值(在edx中)进行比较,然后跳到末尾,如果i超出了边界就抛出异常.在这个跳转之后,我们立即看到一个movsxd rax, edx指令,它从edx中取i的32位值,并将其移动到64位寄存器rax中.作为移动的一部分,它是符号延伸;这就是指令名的“sxd”部分(符号扩展意味着新的64位值的上32位将被设置为32位值的上32位的值,以便数字保留其有符号的值).
有趣的是,我们知道数组和张成空间的长度是非负的,因为我们只是对长度进行了i的边界检查,我们也知道i是非负的.这使得符号扩展毫无用处,因为上边的位保证是0.由于mov指令的零扩展比movsxd稍微便宜一点,我们可以简单地使用它来代替.这正是dotnet/runtime#57970 from @pentp为数组和span所做的(dotnet/runtime#70884也类似地在其他情况下避免一些签名类型转换).在.Net 7中,我们得到了这样的结果:
// Program.Get(Int32[], Int32) sub rsp,28 cmp edx,[rcx+8] jae short M01_L00 mov eax,edx mov eax,[rcx+rax*4+10] add rsp,28 retM01_L00: call CORINFO_HELP_RNGCHKFAIL int 3// Total bytes of code 26
不过,这并不是数组访问开销的唯一来源.事实上,有一个非常大的数组访问开销类别一直存在,但这是众所周知的,甚至有老的FxCop规则和新的Roslyn分析程序对此发出警告: 多维数组访问.在使用多维数组的情况下,开销不仅仅是每个索引操作上的额外分支,或者计算元素位置所需的额外数学运算,而是它们目前基本上未修改地通过JIT的优化阶段.dotnet/runtime#70271通过在JIT管道的早期扩展多维数组访问来改善这里的状态,这样以后的优化阶段就可以像其他代码一样改进多维访问,包括CSE和循环不变提升.通过对多维数组的所有元素进行求和的简单基准测试,就可以看出这一点的影响.
private int[,] _square;[Params(1000)]public int Size { get; set; }[GlobalSetup]public void Setup(){ int count = 0; _square = new int[Size, Size]; for (int i = 0; i < Size; i++) { for (int j = 0; j < Size; j++) { _square[i, j] = count++; } }}[Benchmark]public int Sum(){ int[,] square = _square; int sum = 0; for (int i = 0; i < Size; i++) { for (int j = 0; j < Size; j++) { sum += square[i, j]; } } return sum;}
Method
Runtime
Mean
Ratio
Sum
.NET 6.0
964.1 us
1.00
Sum
.NET 7.0
674.7 us
0.70
前面的示例假设您知道多维数组的每个维度的大小(它直接指循环中的Size大小).显然,情况并非总是如此(甚至很少如此).在这种情况下,您更可能使用Array.GetUpperBound方法,并且由于多维数组可以具有非零的下界,即Array.GetLowerBound.这将导致如下代码:
private int[,] _square;[Params(1000)]public int Size { get; set; }[GlobalSetup]public void Setup(){ int count = 0; _square = new int[Size, Size]; for (int i = 0; i < Size; i++) { for (int j = 0; j < Size; j++) { _square[i, j] = count++; } }}[Benchmark]public int Sum(){ int[,] square = _square; int sum = 0; for (int i = square.GetLowerBound(0); i < square.GetUpperBound(0); i++) { for (int j = square.GetLowerBound(1); j < square.GetUpperBound(1); j++) { sum += square[i, j]; } } return sum;}
在.Net 7中,由于dotnet/runtime#60816,那些GetLowerBound和GetUpperBound调用变成了JIT intrinsic.“intrinsic”由编译器可以替换它认为更好的代码,而不是仅仅依赖于方法定义的实现(如果它甚至有一个).在.Net中有数以千计的方法以这种方式为JIT所知,其中GetLowerBound和GetUpperBound就是最新的两个方法.现在,作为intrinsic,当传递给它们一个常量值(例如0表示第0个rank)时,JIT可以替换必要的程序集指令,直接从存放边界的内存位置读取数据.下面是.Net 6中这个基准测试的汇编代码;这里主要看到的是所有对GetLowerBound和GetUpperBound的调用:
// Program.Sum() push rdi push rsi push rbp push rbx sub rsp,28 mov rsi,[rcx+8] xor edi,edi mov rcx,rsi xor edx,edx cmp [rcx],ecx call System.Array.GetLowerBound(Int32) mov ebx,eax mov rcx,rsi xor edx,edx call System.Array.GetUpperBound(Int32) cmp eax,ebx jle short M00_L03M00_L00: mov rcx,[rsi] mov ecx,[rcx+4] add ecx,0FFFFFFE8 shr ecx,3 cmp ecx,1 jbe short M00_L05 lea rdx,[rsi+10] inc ecx movsxd rcx,ecx mov ebp,[rdx+rcx*4] mov rcx,rsi mov edx,1 call System.Array.GetUpperBound(Int32) cmp eax,ebp jle short M00_L02M00_L01: mov ecx,ebx sub ecx,[rsi+18] cmp ecx,[rsi+10] jae short M00_L04 mov edx,ebp sub edx,[rsi+1C] cmp edx,[rsi+14] jae short M00_L04 mov eax,[rsi+14] imul rax,rcx mov rcx,rdx add rcx,rax add edi,[rsi+rcx*4+20] inc ebp mov rcx,rsi mov edx,1 call System.Array.GetUpperBound(Int32) cmp eax,ebp jg short M00_L01M00_L02: inc ebx mov rcx,rsi xor edx,edx call System.Array.GetUpperBound(Int32) cmp eax,ebx jg short M00_L00M00_L03: mov eax,edi add rsp,28 pop rbx pop rbp pop rsi pop rdi retM00_L04: call CORINFO_HELP_RNGCHKFAILM00_L05: mov rcx,offset MT_System.IndexOutOfRangeException call CORINFO_HELP_NEWSFAST mov rsi,rax call System.SR.get_IndexOutOfRange_ArrayRankIndex() mov rdx,rax mov rcx,rsi call System.IndexOutOfRangeException..ctor(System.String) mov rcx,rsi call CORINFO_HELP_THROW int 3// Total bytes of code 219
下面是.Net 7生成的汇编代码:
// Program.Sum() push r14 push rdi push rsi push rbp push rbx sub rsp,20 mov rdx,[rcx+8] xor eax,eax mov ecx,[rdx+18] mov r8d,ecx mov r9d,[rdx+10] lea ecx,[rcx+r9+0FFFF] cmp ecx,r8d jle short M00_L03 mov r9d,[rdx+1C] mov r10d,[rdx+14] lea r10d,[r9+r10+0FFFF]M00_L00: mov r11d,r9d cmp r10d,r11d jle short M00_L02 mov esi,r8d sub esi,[rdx+18] mov edi,[rdx+10]M00_L01: mov ebx,esi cmp ebx,edi jae short M00_L04 mov ebp,[rdx+14] imul ebx,ebp mov r14d,r11d sub r14d,[rdx+1C] cmp r14d,ebp jae short M00_L04 add ebx,r14d add eax,[rdx+rbx*4+20] inc r11d cmp r10d,r11d jg short M00_L01M00_L02: inc r8d cmp ecx,r8d jg short M00_L00M00_L03: add rsp,20 pop rbx pop rbp pop rsi pop rdi pop r14 retM00_L04: call CORINFO_HELP_RNGCHKFAIL int 3// Total bytes of code 130
重要的是,注意没有更多的call(调用) (除了末尾的边界检查异常).例如,不是第一次调用GetUpperBound:
call System.Array.GetUpperBound(Int32)
我们得到:
mov r9d,[rdx+1C]mov r10d,[rdx+14]lea r10d,[r9+r10+0FFFF]
最终使代码运行得更快:
Method
Runtime
Mean
Ratio
Sum
.NET 6.0
2,657.5 us
1.00
Sum
.NET 7.0
676.3 us
0.25
个人能力有限,如果您发现有什么不对,请私信我
如果您觉得对您有用的话,可以点个赞或者加个关注,欢迎大家一起进行技术交流
标签: #正则表达式生成器