强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第8章:C# RyuJIT

第8章:C# RyuJIT

“RyuJIT 是 .NET 的性能引擎,它让 C# 从一门’托管语言’进化为性能竞争者。”

8.1 RyuJIT 概述

RyuJIT(读作 “ree-you-JIT”)是 .NET 运行时的 JIT 编译器,自 .NET Core 2.0 起成为 x64 平台的默认编译器。它的名称源自日本的"龙"(Ryu),寓意高性能和力量。

8.1.1 .NET 编译架构演进

.NET Framework              .NET Core / .NET 5+
    (经典)                      (现代)

┌──────────┐              ┌──────────────────┐
│ JIT32    │              │    RyuJIT         │
│ (x86)    │              │  (x64/ARM64)      │
├──────────┤              └────────┬─────────┘
│ JIT64    │                       │
│ (x64)    │              ┌────────┴─────────┐
└──────────┘              │ 分层编译 (Tiered) │
                          └────────┬─────────┘
                                   │
                          ┌────────┴─────────┐
                          │ 动态 PGO          │
                          └──────────────────┘

8.1.2 RyuJIT 架构

┌─────────────────────────────────────────────────────────────────┐
│                     RyuJIT 架构                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  C# 源代码                                                      │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────┐                                            │
│  │ Roslyn 编译器   │  ← C# → IL (中间语言)                      │
│  └──────┬──────────┘                                            │
│         ▼                                                       │
│  ┌─────────────────┐                                            │
│  │ IL (中间语言)    │  ← Common Intermediate Language            │
│  └──────┬──────────┘                                            │
│         ▼                                                       │
│  ┌─────────────────┐                                            │
│  │ RyuJIT          │                                            │
│  │ ┌─────────────┐ │                                            │
│  │ │ 前端         │ │  ← IL → 内部 IR (GenTree)                 │
│  │ └──────┬──────┘ │                                            │
│  │        ▼        │                                            │
│  │ ┌─────────────┐ │                                            │
│  │ │ 优化器       │ │  ← 多趟优化                               │
│  │ └──────┬──────┘ │                                            │
│  │        ▼        │                                            │
│  │ ┌─────────────┐ │                                            │
│  │ │ 后端         │ │  ← IR → 机器码                           │
│  │ └─────────────┘ │                                            │
│  └─────────────────┘                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

8.2 RyuJIT IR:GenTree

8.2.1 GenTree 节点类型

RyuJIT 使用 GenTree 作为内部 IR,这是一种线性的、基于语句的表示。

// C# 代码
public int Add(int a, int b) {
    return a + b;
}

// RyuJIT GenTree IR (概念表示)
// 
// GenTreeStmt
// └── GenTreeReturn
//     └── GenTreeOp(ADD)
//         ├── GenTreeLclVar(a)
//         └── GenTreeLclVar(b)

8.2.2 RyuJIT 的 IR 特点

特性说明
线性 IR基于语句的线性表示,而非图表示
低级抽象接近机器码,但仍保留高级语义
隐式栈使用表达式栈而非 SSA
语句导向每个语句独立优化
// 更复杂的 IR 示例
public int Compute(int x) {
    int a = x * 2;
    int b = a + 1;
    return b * b;
}

// GenTree 表示 (简化)
// [0] LCL_VAR  x = ARG
// [1] CNS_INT  2
// [2] MUL      a = x * 2
// [3] CNS_INT  1
// [4] ADD      b = a + 1
// [5] MUL      result = b * b
// [6] RETURN   result

8.3 RyuJIT 优化技术

8.3.1 主要优化遍

┌─────────────────────────────────────────────────────────────────┐
│                   RyuJIT 优化管线                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 前端 (Importing)                                           │
│     └─ IL → GenTree 转换                                       │
│                                                                 │
│  2. 标记 (Morphing)                                            │
│     ├─ 常量折叠                                                │
│     ├─ 死代码消除                                              │
│     ├─ 内联展开                                                │
│     └─ 结构提升                                                │
│                                                                 │
│  3. 全局优化 (Global Optimizations)                            │
│     ├─ 值编号 (Value Numbering)                                │
│     ├─ 循环优化                                                │
│     ├─ 边界检查消除                                            │
│     └─ 通用子表达式消除                                        │
│                                                                 │
│  4. 后端 (Code Generation)                                     │
│     ├─ 线性扫描寄存器分配                                      │
│     ├─ 指令选择                                                │
│     └─ 指令调度                                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

8.3.2 内联优化

// RyuJIT 内联示例
public class InliningDemo {
    
    // 简单方法 - 会被内联
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int Square(int x) => x * x;
    
    // 调用方
    public int Compute(int a, int b) {
        return Square(a) + Square(b);
    }
    // 内联后等价于:
    // return (a * a) + (b * b);
    
    // 避免内联
    [MethodImpl(MethodImplOptions.NoInlining)]
    public void NeverInline() {
        // 这个方法不会被内联
    }
}

// 内联限制
// - 方法体 IL 大小 ≤ 16 字节:总是内联
// - 方法体 IL 大小 > 16 字节:考虑调用频率
// - 虚方法:需要去虚拟化才能内联

8.3.3 边界检查消除

// 边界检查消除
public class BoundsCheckDemo {
    
    // 原始代码 - 有边界检查
    public int Sum(int[] arr) {
        int sum = 0;
        for (int i = 0; i < arr.Length; i++) {
            sum += arr[i];  // 每次访问都有边界检查
        }
        return sum;
    }
    
    // RyuJIT 优化后:
    // - 消除了循环内的边界检查
    // - 只保留循环前的长度检查
    
    // 手动优化 - 使用 Span<T>
    public int SumOptimized(int[] arr) {
        int sum = 0;
        Span<int> span = arr.AsSpan();
        for (int i = 0; i < span.Length; i++) {
            sum += span[i];  // 可以更好地优化
        }
        return sum;
    }
    
    // 使用 ref 局部变量
    public void Double(int[] arr) {
        for (int i = 0; i < arr.Length; i++) {
            ref int val = ref arr[i];
            val *= 2;  // 减少索引操作
        }
    }
}

8.3.4 结构优化

// 结构体优化
public struct Point {
    public double X;
    public double Y;
    
    public Point(double x, double y) {
        X = x;
        Y = y;
    }
    
    public double DistanceTo(Point other) {
        double dx = X - other.X;
        double dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
}

public class StructOptimization {
    // 结构体通常在栈上分配,无需 GC
    public double ComputeDistance() {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(4, 6);
        return p1.DistanceTo(p2);
    }
    
    // readonly struct - 更多优化机会
    public readonly struct Vector3 {
        public readonly double X, Y, Z;
        
        public Vector3(double x, double y, double z) {
            X = x; Y = y; Z = z;
        }
        
        public double Length => Math.Sqrt(X * X + Y * Y + Z * Z);
    }
}

8.4 分层编译

8.4.1 分层编译概述

.NET Core 3.0 引入了分层编译,与 HotSpot 类似,将编译过程分为多个层次。

// 启用分层编译 (默认启用)
// dotnet run -c Release

// 或在 .csproj 中配置
// <PropertyGroup>
//     <TieredCompilation>true</TieredCompilation>
// </PropertyGroup>

8.4.2 分层编译工作流程

┌─────────────────────────────────────────────────────────────────┐
│                   .NET 分层编译流程                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  方法首次调用                                                    │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────┐                                            │
│  │  Tier 0         │  ← 快速 JIT 编译(最小优化)               │
│  │  (Quick JIT)    │     或解释执行                             │
│  └──────┬──────────┘                                            │
│         │ 调用次数超过阈值                                       │
│         ▼                                                       │
│  ┌─────────────────┐                                            │
│  │  Tier 1         │  ← 完全优化编译                           │
│  │  (Optimized)    │     包含所有优化                           │
│  └─────────────────┘                                            │
│                                                                 │
│  阈值参数:                                                       │
│  - 方法调用: 30 次触发 Tier 1                                    │
│  - 循环回边: 30 次触发 OSR                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
# 查看分层编译日志
DOTNET_TieredCompilation=1 dotnet run -c Release
DOTNET_JitEnableNoGC=1 dotnet run -c Release

# 关闭分层编译
DOTNET_TieredCompilation=0 dotnet run -c Release

# 只使用 Tier 0 (快速 JIT)
DOTNET_TieredCompilation=0 DOTNET_TieredQuickJit=1 dotnet run -c Release

8.4.3 On-Stack Replacement (OSR)

// .NET 7+ 支持 OSR
// 允许正在执行的方法被替换为优化版本

public class OSRDemo {
    public long HotLoop() {
        long sum = 0;
        // 长时间运行的循环
        // 当循环变热时,可以被 OSR 替换为优化版本
        for (long i = 0; i < 100_000_000; i++) {
            sum += i;
        }
        return sum;
    }
}

8.5 Profile-Guided Optimization (PGO)

8.5.1 PGO 概述

PGO(Profile-Guided Optimization)利用运行时收集的 Profile 数据来指导编译优化。.NET 支持两种 PGO 模式:

模式说明版本
静态 PGO使用预先收集的 Profile.NET 6+
动态 PGO运行时自动收集和优化.NET 8+

8.5.2 动态 PGO

// .NET 8+ 默认启用动态 PGO
// 无需代码更改,运行时自动优化

public class DynamicPGODemo {
    
    // 运行时会收集以下信息:
    // - 方法调用频率
    // - 分支预测信息
    // - 类型分布
    // - 循环次数
    
    public int ProcessShape(Shape shape) {
        // PGO 记录: 99% 的调用是 Circle
        if (shape is Circle circle) {
            return (int)(circle.Radius * circle.Radius * Math.PI);
        }
        else if (shape is Rectangle rect) {
            return rect.Width * rect.Height;
        }
        return 0;
    }
    
    // 基于 PGO 的优化:
    // 1. 类型检查顺序优化(Circle 放在前面)
    // 2. 内联热路径
    // 3. 分支预测优化
}
# 启用动态 PGO (默认启用)
DOTNET_TieredPGO=1 dotnet run -c Release

# 查看 PGO 统计
DOTNET_JitPrintInlinedMethods=1 dotnet run -c Release

8.5.3 静态 PGO

// 静态 PGO - 使用收集的 profile 数据
// 1. 收集 profile
// dotnet run -c Release -- --pgo-collect

// 2. 使用 profile 编译
// dotnet publish -c Release /p:PGOEnabled=true

// .csproj 配置
// <PropertyGroup>
//     <TieredPGO>true</TieredPGO>
//     <ReadyToRun>true</ReadyToRun>
// </PropertyGroup>

8.5.4 PGO 优化效果

// PGO 优化示例

// 1. 条件分支优化
public int BranchOptimization(bool flag) {
    // PGO 发现 flag 99% 为 true
    if (flag) {      // 热路径,优化执行
        return 1;
    } else {         // 冷路径
        return 0;
    }
}

// 2. 去虚拟化
public int ProcessShape(Shape shape) {
    // PGO 发现 shape 90% 是 Circle
    // 编译器生成: if (shape.GetType() == typeof(Circle)) { 内联Circle代码 }
    return shape.Area();
}

// 3. 内联决策
public int HotMethod(int x) {
    // PGO 发现此方法被频繁调用
    // 即使方法体较大也可能被内联
    return Compute(x) + Transform(x);
}

8.6 ReadyToRun (R2R)

8.6.1 R2R 概述

ReadyToRun 是 .NET 的 AOT 预编译技术,将 IL 预编译为原生代码。

<!-- 启用 ReadyToRun -->
<PropertyGroup>
    <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
# 发布 ReadyToRun 版本
dotnet publish -c Release -r linux-x64 /p:PublishReadyToRun=true

8.6.2 R2R vs 完全 JIT

特性完全 JITReadyToRun
启动时间较慢
文件大小较大
峰值性能稍低(可能回退到 JIT)
优化完全优化部分优化

8.7 性能优化最佳实践

8.7.1 Span 和 Memory

// 使用 Span<T> 减少分配
public class SpanDemo {
    
    // ❌ 会分配新数组
    public byte[] ProcessArray(byte[] data) {
        byte[] result = new byte[data.Length];
        for (int i = 0; i < data.Length; i++) {
            result[i] = (byte)(data[i] * 2);
        }
        return result;
    }
    
    // ✅ 使用 Span<T> 零分配
    public void ProcessSpan(Span<byte> data) {
        for (int i = 0; i < data.Length; i++) {
            data[i] = (byte)(data[i] * 2);
        }
    }
    
    // 字符串处理
    public int CountWords(ReadOnlySpan<char> text) {
        int count = 0;
        bool inWord = false;
        
        foreach (char c in text) {
            if (char.IsWhiteSpace(c)) {
                inWord = false;
            } else if (!inWord) {
                inWord = true;
                count++;
            }
        }
        return count;
    }
}

8.7.2 避免装箱

// 装箱优化
public class BoxingDemo {
    
    // ❌ 装箱 - 分配堆内存
    public void PrintValue(object value) {
        Console.WriteLine(value);  // 值类型会被装箱
    }
    
    // ✅ 泛型 - 避免装箱
    public void PrintValue<T>(T value) {
        Console.WriteLine(value);  // 无装箱
    }
    
    // ❌ 接口装箱
    public void Process(IComparable comparable) {
        // comparable.CompareTo(...) 会导致装箱
    }
    
    // ✅ 泛型约束
    public void Process<T>(T value) where T : IComparable<T> {
        // value.CompareTo(...) 无装箱
    }
}

8.7.3 使用 SIMD

// 使用 System.Numerics.Vector
using System.Numerics;

public class SimdDemo {
    
    // SIMD 向量加法
    public void AddVectors(float[] a, float[] b, float[] result) {
        int simdLength = Vector<float>.Count;
        int i = 0;
        
        // SIMD 处理
        for (; i <= a.Length - simdLength; i += simdLength) {
            var va = new Vector<float>(a, i);
            var vb = new Vector<float>(b, i);
            (va + vb).CopyTo(result, i);
        }
        
        // 处理剩余元素
        for (; i < a.Length; i++) {
            result[i] = a[i] + b[i];
        }
    }
    
    // .NET 8+ Vector128/Vector256
    public void AddVectors128(float[] a, float[] b, float[] result) {
        int i = 0;
        for (; i <= a.Length - Vector128<float>.Count; i += Vector128<float>.Count) {
            var va = Vector128.LoadUnsafe(ref a[i]);
            var vb = Vector128.LoadUnsafe(ref b[i]);
            (va + vb).StoreUnsafe(ref result[i]);
        }
        for (; i < a.Length; i++) {
            result[i] = a[i] + b[i];
        }
    }
}

8.7.4 零分配模式

// 零分配模式
public class ZeroAllocation {
    
    // 使用 ArrayPool
    public void ProcessLargeData(int size) {
        var pool = ArrayPool<int>.Shared;
        int[] buffer = pool.Rent(size);
        
        try {
            // 使用 buffer
            for (int i = 0; i < size; i++) {
                buffer[i] = i;
            }
        } finally {
            pool.Return(buffer);
        }
    }
    
    // 使用 stackalloc
    public void SmallBuffer() {
        Span<byte> buffer = stackalloc byte[256];
        // 使用 buffer
    }
    
    // 使用对象池
    private readonly ObjectPool<StringBuilder> _sbPool =
        new DefaultObjectPoolProvider().CreateStringBuilderPool();
    
    public string BuildString(IEnumerable<string> items) {
        var sb = _sbPool.Get();
        try {
            foreach (var item in items) {
                sb.Append(item);
            }
            return sb.ToString();
        } finally {
            _sbPool.Return(sb);
        }
    }
}

8.8 诊断工具

8.8.1 dotnet-trace

# 安装 dotnet-trace
dotnet tool install --global dotnet-trace

# 收集跟踪
dotnet-trace collect --process-id <PID>

# 使用 Chromium 查看
# chrome://tracing/

8.8.2 BenchmarkDotNet

// 使用 BenchmarkDotNet
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class MyBenchmarks {
    private int[] data;
    
    [GlobalSetup]
    public void Setup() {
        data = Enumerable.Range(0, 10000).ToArray();
    }
    
    [Benchmark(Baseline = true)]
    public long ForLoop() {
        long sum = 0;
        for (int i = 0; i < data.Length; i++) {
            sum += data[i];
        }
        return sum;
    }
    
    [Benchmark]
    public long Foreach() {
        long sum = 0;
        foreach (var item in data) {
            sum += item;
        }
        return sum;
    }
    
    [Benchmark]
    public long Linq() => data.Sum(x => (long)x);
}

// 运行
// BenchmarkRunner.Run<MyBenchmarks>();

8.8.3 JIT 日志

# JIT 编译信息
DOTNET_JitPrintInlinedMethods=1 dotnet run -c Release

# 导出 JIT 日志
DOTNET_JitEnableNoGC=1 dotnet run -c Release

# 使用 jitutils
# https://github.com/dotnet/jitutils
dotnet tool install --global jitutils

8.9 与 HotSpot 对比

特性RyuJIT (.NET)HotSpot (Java)
IR 类型GenTree (线性)Sea of Nodes (图)
分层编译2 层5 层
PGO 支持静态 + 动态有限
AOTReadyToRun, NativeAOTGraalVM Native Image
逃逸分析有限完善
SIMD 支持优秀改进中

8.10 本章小结

关键要点

  1. RyuJIT 是 .NET 的核心 JIT:支持 x64 和 ARM64
  2. 分层编译:快速 JIT → 完全优化
  3. PGO:动态 PGO 基于运行时数据优化
  4. 优化技巧:Span、避免装箱、SIMD、零分配
  5. ReadyToRun:AOT 预编译加速启动

性能检查清单

  • 启用分层编译
  • 使用 Span<T> 减少分配
  • 避免装箱(使用泛型)
  • 使用 SIMD 向量化
  • 使用 ArrayPool 减少 GC 压力
  • 使用 BenchmarkDotNet 验证优化效果

8.11 扩展阅读


上一章: 第7章 - Java HotSpot 下一章: 第9章 - Pyston