.NET8极致性能优化CHRL

前言

.NET8在.NET7的基础上进行了进一步的优化,比如CHRL(全称:CORINFO_HELP_RNGCHKFAIL)优化技术,CORINFO_HELP_RNGCHKFAIL是边界检查,在.NET7里面它已经进行了部分优化,但是.NET8里面它继续优化,类似人工智能,.NET8能意识到某些性能问题,从而进行优化。本篇来看下。原文:.NET8极致性能优化CHRL

概述

JIT会对数组,字符串的范围边界进行检查。比如数组的索引是否在数组长度范围内,不能超过。所以JIT就会产生边界检查的步骤。

csharp 复制代码
public class Tests
{
    private byte[] _array = new byte[8];
    private int _index = 4;

    public void Get() => Get(_array, _index);

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static byte Get(byte[] array, int index) => array[index];
}

Get函数.NET7的ASM如下:

vbnet 复制代码
; Tests.Get(Byte[], Int32)
       sub       rsp,28
       cmp       edx,[rcx+8]
       jae       short M01_L00
       mov       eax,edx
       movzx     eax,byte ptr [rcx+rax+10]
       add       rsp,28
       ret
M01_L00:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3

cmp指令把数组的MT(方法表)偏移8位置的数组长度与当前的数组索引对比,两者如果索引大于(后者)或等于(jae)数组长度(前者)的时候。就会跳转到CORINFO_HELP_RNGCHKFAIL进行边界检查,可能会引发超出引范围的异常IndexOutOfRangeException。但是实际上这段这段代码的访问只需要两个mov,一个是数组的索引,一个是(MT(方法表)+0x10+索引)取其值返回即可。所以这个地方有清晰可见的优化的地方。

.NET8学习了一些范围边界的智能化优化,也就说,有的地方不需要边界检查,从而把边界检查优化掉,用以提高代码的性能。下面例子:

csharp 复制代码
 private readonly int[] _array = new int[7];
   public int GetBucket() => GetBucket(_array, 42);
   private static int GetBucket(int[] buckets, int hashcode) =>
   buckets[(uint)hashcode % buckets.Length];

.NET7它的ASM如下:

vbnet 复制代码
; Tests.GetBucket()
       sub       rsp,28
       mov       rcx,[rcx+8]
       mov       eax,2A
       mov       edx,[rcx+8]
       mov       r8d,edx
       xor       edx,edx
       idiv      r8
       cmp       rdx,r8
       jae       short M00_L00
       mov       eax,[rcx+rdx*4+10]
       add       rsp,28
       ret
M00_L00:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3

它依然进行了边界检查,然.NET8的JIT能自动识别到(uint)hashcode%buckets.Length这个索引不可能超过数组的长度也就是buckets.Length。所以.NET8可以省略掉边界检查,如下.NET8 ASM

css 复制代码
; Tests.GetBucket()
       mov       rcx,[rcx+8]
       mov       eax,2A
       mov       r8d,[rcx+8]
       xor       edx,edx
       div       r8
       mov       eax,[rcx+rdx*4+10]
       ret

再看下另外一个例子:

csharp 复制代码
public class Tests
{
    private readonly string _s = "\"Hello, World!\"";

    public bool IsQuoted() => IsQuoted(_s);

    private static bool IsQuoted(string s) =>
    s.Length >= 2 && s[0] == '"' && s[^1] == '"';
}

IsQuoted检查字符串是否至少有两个字符,并且字符串开头和结尾均以引号结束,s\^1表示ss.Length - 1也就是字符串的长度。.NET7 ASM如下:

vbnet 复制代码
; Tests.IsQuoted(System.String)
       sub       rsp,28
       mov       eax,[rcx+8]
       cmp       eax,2
       jl        short M01_L00
       cmp       word ptr [rcx+0C],22
       jne       short M01_L00
       lea       edx,[rax-1]
       cmp       edx,eax
       jae       short M01_L01
       mov       eax,edx
       cmp       word ptr [rcx+rax*2+0C],22
       sete      al
       movzx     eax,al
       add       rsp,28
       ret
M01_L00:
       xor       eax,eax
       add       rsp,28
       ret
M01_L01:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3

注意看.NET7的骚操,它实际上进行了边界检查,但是只检查了一个,因为它只有一个jae指令跳转。这是为什么呢?JIT已经知道不需要对s0进行边界检查,因为s.Length >= 2已经检查过了,只要是小于2的索引(因为索引是无符号,没有负数)都不需要检查。但是依然对ss.Length - 1进行了边界检查,所以.NET7虽然也是骚操,但是它这个骚操不够彻底。

我们来看下彻底骚操的.NET8

arduino 复制代码
; Tests.IsQuoted(System.String)
       mov       eax,[rcx+8]
       cmp       eax,2
       jl        short M01_L00
       cmp       word ptr [rcx+0C],22
       jne       short M01_L00
       dec       eax
       cmp       word ptr [rcx+rax*2+0C],22
       sete      al
       movzx     eax,al
       ret
M01_L00:
       xor       eax,eax
       ret

完全没有了边界检查,JIT不仅意识到s0是安全的,因为检查过了s.Length >= 2。因为检查过了s.Length >= 2,还意识到s.length> s.Length-1 >=1。所以不需要边界检查,全给它优化掉了。

可以看到.NET8的性能优化的极致有多厉害,它基本上榨干了JIT的引擎,让其进行最大智能化程度的优化。

公众号:江湖评谈(jianghupt),欢迎关注,文章首发地。

想要加群的点击下加入技术讨论群:

欢迎加入.NET技术交流群

相关推荐
明月_清风13 分钟前
🛡️ Web3 安全入门:新手防骗完全指南
后端·web3
明月_清风23 分钟前
🔐 Solidity 完全指南:关键语法解析与智能合约中的核心作用
后端·web3
geovindu40 分钟前
CSharp: Iterative Algorithms
开发语言·后端·算法·c#·.net·迭代算法
雪隐1 小时前
AI股票小助手10-我用代码管住自己的手:一个普通人的市场观察笔记
前端·人工智能·后端
艾斯特_2 小时前
混合检索与重排:提升召回与排序质量
后端·python·ai
Ai拆代码的曹操2 小时前
RocketMQ 消息堆积排查实战:从 20 个消费者线程卡死到 rebalance 死亡螺旋
后端·rocketmq
吃饱了得干活2 小时前
@Transactional 又失效了?把这 8 个坑全填了!
java·后端·spring
颜进强2 小时前
从零搭建私人 RAG 知识库:让项目决策真正“可检索、可追溯”
前端·后端·ai编程
颜进强2 小时前
Embedding 模型介绍:热门模型对比与应用场景
前端·后端·ai编程
颜进强2 小时前
LanceDB 基础使用:用 TypeScript 完成第一次向量检索
前端·后端·ai编程