JIT 编译与业务结合实战教程 / 第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
| 特性 | 完全 JIT | ReadyToRun |
|---|---|---|
| 启动时间 | 较慢 | 快 |
| 文件大小 | 小 | 较大 |
| 峰值性能 | 高 | 稍低(可能回退到 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 支持 | 静态 + 动态 | 有限 |
| AOT | ReadyToRun, NativeAOT | GraalVM Native Image |
| 逃逸分析 | 有限 | 完善 |
| SIMD 支持 | 优秀 | 改进中 |
8.10 本章小结
关键要点
- RyuJIT 是 .NET 的核心 JIT:支持 x64 和 ARM64
- 分层编译:快速 JIT → 完全优化
- PGO:动态 PGO 基于运行时数据优化
- 优化技巧:Span
、避免装箱、SIMD、零分配 - ReadyToRun:AOT 预编译加速启动
性能检查清单
- 启用分层编译
- 使用
Span<T>减少分配 - 避免装箱(使用泛型)
- 使用 SIMD 向量化
- 使用
ArrayPool减少 GC 压力 - 使用 BenchmarkDotNet 验证优化效果
8.11 扩展阅读
上一章: 第7章 - Java HotSpot 下一章: 第9章 - Pyston