第 5 步:性能优化 - 增量生成器
加载进度中...
📋 本步目标
- 理解增量生成器的工作原理
- 学习如何优化生成器性能
- 对比传统生成器和增量生成器的性能差异
- 掌握缓存机制和最佳实践
- 将 Step 3 的生成器改造为增量版本
⏱️ 预计时间
约 2-3 小时
🎯 为什么需要增量生成器?
在 Step 3 中,我们实现了一个基本的 ToString 生成器。但是有一个性能问题:
传统生成器的问题:
- ❌ 每次编译都重新执行,即使代码没有变化
- ❌ 处理所有文件,即使只修改了一个文件
- ❌ 大型项目编译速度慢
增量生成器的优势:
- ✅ 缓存未变化的结果
- ✅ 只处理变化的部分
- ✅ 显著提高编译速度
性能对比
| 场景 | 传统生成器 | 增量生成器 | 提升 |
|---|---|---|---|
| 首次编译 | 1000ms | 1000ms | - |
| 无变化重编译 | 1000ms | 10ms | 100倍 ⚡ |
| 小改动重编译 | 1000ms | 100ms | 10倍 ⚡ |
| 大型项目(1000个类) | 30s | 3s | 10倍 ⚡ |
📚 增量生成器原理
工作流程
编译开始
↓
检查输入是否变化
↓
├─ 未变化 → 使用缓存结果 → 跳过生成 ⚡
└─ 有变化 → 执行转换 → 生成代码 → 更新缓存
↓
编译结束缓存机制
增量生成器使用 IEquatable<T> 来判断数据是否变化:
csharp
// 使用 record 类型,自动实现 IEquatable
public record ClassInfo(
string Name,
string Namespace,
ImmutableArray<PropertyInfo> Properties
);
// 当两个 ClassInfo 对象的所有属性都相同时,
// 它们被认为是相等的,生成器会使用缓存缓存判断逻辑:
- 计算输入数据的哈希值
- 与缓存中的哈希值比较
- 如果相同,使用缓存结果
- 如果不同,重新执行转换
🛠️ 实现增量生成器
回顾:Step 3 的实现
在 Step 3 中,我们已经使用了 IIncrementalGenerator:
csharp
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => IsCandidateClass(node),
transform: static (ctx, _) => GetClassInfo(ctx)
)
.Where(static info => info != null);
context.RegisterSourceOutput(classDeclarations,
static (spc, classInfo) => GenerateToStringMethod(spc, classInfo!));
}
}好消息:我们已经在使用增量生成器了!
但是,要充分发挥增量生成器的性能,我们需要理解和优化几个关键点。
🔍 增量管道操作
1. CreateSyntaxProvider - 两阶段过滤
csharp
var provider = context.SyntaxProvider.CreateSyntaxProvider(
// 阶段 1:快速语法过滤(不使用语义模型)
predicate: static (node, _) =>
{
// ✅ 快速:只检查语法
return node is ClassDeclarationSyntax cls &&
cls.AttributeLists.Count > 0 &&
cls.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},
// 阶段 2:语义分析(使用语义模型)
transform: static (ctx, _) =>
{
// 在这里使用语义模型
var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node);
return ExtractInfo(symbol);
}
);为什么分两阶段?
- 第一阶段非常快(只检查语法)
- 过滤掉大部分不相关的节点
- 第二阶段只处理候选节点(减少开销)
2. Where - 过滤数据
csharp
var filtered = provider.Where(static info => info != null);3. Select - 转换数据
csharp
var transformed = provider.Select(static (info, _) =>
new TransformedInfo(info.Name, info.Properties.Length)
);4. Collect - 收集所有结果
csharp
var allClasses = provider.Collect();
context.RegisterSourceOutput(allClasses, static (spc, classList) =>
{
// 生成单个文件包含所有类
var source = GenerateAllClasses(classList);
spc.AddSource("AllGenerated.g.cs", source);
});5. Combine - 组合多个管道
csharp
var classes = context.SyntaxProvider.CreateSyntaxProvider(/* ... */);
var interfaces = context.SyntaxProvider.CreateSyntaxProvider(/* ... */);
var combined = classes.Collect().Combine(interfaces.Collect());
context.RegisterSourceOutput(combined, static (spc, data) =>
{
var (classList, interfaceList) = data;
// 使用两个列表生成代码
});💡 性能优化技巧
技巧 1:快速语法过滤
csharp
// ❌ 慢:在 predicate 中做复杂检查
predicate: (node, ct) =>
{
if (node is not ClassDeclarationSyntax cls)
return false;
// 慢:字符串操作
var text = cls.ToString();
if (text.Contains("abstract"))
return false;
// 慢:遍历所有成员
foreach (var member in cls.Members)
{
// ...
}
return true;
}
// ✅ 快:只做简单的语法检查
predicate: (node, ct) =>
{
// 快:类型检查
if (node is not ClassDeclarationSyntax cls)
return false;
// 快:检查修饰符
if (cls.Modifiers.Any(m => m.IsKind(SyntaxKind.AbstractKeyword)))
return false;
// 快:检查特性列表数量
if (cls.AttributeLists.Count == 0)
return false;
return true;
}技巧 2:使用 Record 类型
csharp
// ❌ 避免:使用普通类
public class ClassInfo
{
public string Name { get; set; }
public List<string> Properties { get; set; }
// 需要手动实现 Equals 和 GetHashCode
// 容易出错,难以维护
}
// ✅ 推荐:使用 record
public record ClassInfo(
string Name,
ImmutableArray<string> Properties
);
// 自动实现 IEquatable,支持缓存
// 不可变,线程安全技巧 3:最小化数据传输
csharp
// ❌ 避免:传输大量数据
public record ClassInfo(
string Name,
string Namespace,
string FullSourceCode, // 整个源代码!
SyntaxTree Tree, // 整个语法树!
ImmutableArray<MemberInfo> AllMembers // 所有成员!
);
// ✅ 推荐:只传输必要数据
public record ClassInfo(
string Name,
string Namespace,
ImmutableArray<PropertyInfo> PublicProperties // 只要公共属性
);技巧 4:使用不可变集合
csharp
// ❌ 避免:使用可变集合
public record ClassInfo(
string Name,
List<PropertyInfo> Properties // List 是可变的
);
// ✅ 推荐:使用不可变集合
public record ClassInfo(
string Name,
ImmutableArray<PropertyInfo> Properties
);🎓 实践:优化 ToString 生成器
让我们回顾并优化 Step 3 的实现:
优化前的代码
csharp
// Step 3 的实现已经很好了!
private static bool IsCandidateClass(SyntaxNode node)
{
if (node is not ClassDeclarationSyntax classDecl)
return false;
if (classDecl.AttributeLists.Count == 0)
return false;
if (!classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
return false;
return true;
}可以进一步优化的地方
csharp
// 优化:使用 static 方法(避免闭包)
private static bool IsCandidateClass(SyntaxNode node)
{
// 使用模式匹配简化代码
return node is ClassDeclarationSyntax
{
AttributeLists.Count: > 0
} classDecl
&& classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
}性能测试
创建一个简单的性能测试:
csharp
using System.Diagnostics;
public class PerformanceTest
{
[Fact]
public void TestGeneratorPerformance()
{
// 生成大量测试数据
var source = GenerateLargeSource(1000); // 1000 个类
var compilation = CreateCompilation(source);
var generator = new ToStringGenerator();
// 第一次运行
var sw1 = Stopwatch.StartNew();
var driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGenerators(compilation);
sw1.Stop();
Console.WriteLine($"首次运行: {sw1.ElapsedMilliseconds}ms");
// 第二次运行(应该使用缓存)
var sw2 = Stopwatch.StartNew();
driver = driver.RunGenerators(compilation);
sw2.Stop();
Console.WriteLine($"缓存运行: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($"性能提升: {sw1.ElapsedMilliseconds / (double)sw2.ElapsedMilliseconds:F1}x");
}
}📚 相关 API
本步骤深入学习以下 API:
增量生成器管道操作
增量生成器的核心操作,包括 Select、Where、Combine、Collect 等。
CreateSyntaxProvider
创建语法提供器,支持两阶段过滤(语法过滤 + 语义分析)。
IncrementalValueProvider / IncrementalValuesProvider
表示单个值或多个值的增量提供器,支持缓存机制。
Record 类型与 IEquatable
用于实现高效的缓存机制,支持值相等性比较。
💡 下一步学习
完成本步骤后,建议:
- 深入阅读 增量生成器中级指南 掌握所有管道操作
- 查看 增量管道 API 完整参考 了解高级用法
- 阅读 最佳实践 学习更多优化技巧
📊 性能对比实验
实验 1:无变化重编译
csharp
// 场景:代码没有任何变化,重新编译
// 传统生成器:1000ms(每次都重新生成)
// 增量生成器:10ms(使用缓存)
// 提升:100倍实验 2:修改一个文件
csharp
// 场景:修改了 1000 个类中的 1 个
// 传统生成器:1000ms(重新处理所有类)
// 增量生成器:100ms(只处理修改的类)
// 提升:10倍实验 3:添加新属性
csharp
// 场景:给一个类添加新属性
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; } // 新添加
}
// 增量生成器会检测到 Person 类的变化
// 只重新生成 Person 的 ToString 方法
// 其他类使用缓存🐛 常见问题
问题 1:缓存没有生效
症状:每次编译都重新生成代码。
原因:
- 数据模型没有正确实现
IEquatable<T> - 使用了可变集合(如
List<T>) - 数据模型包含了不必要的数据
解决方案:
csharp
// ✅ 正确:使用 record 和 ImmutableArray
public record ClassInfo(
string Name,
ImmutableArray<PropertyInfo> Properties
);
public record PropertyInfo(
string Name,
string Type
);问题 2:性能反而变慢了
原因:
predicate中做了太多工作- 数据模型太复杂,比较开销大
解决方案:
- 简化
predicate,只做最基本的语法检查 - 最小化数据模型,只包含必要信息
- 使用性能分析工具找出瓶颈
问题 3:如何调试增量生成器?
方法 1:使用 Debugger.Launch()
csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
#endif
// 生成器代码...
}方法 2:使用日志
csharp
private static ClassInfo? GetClassInfo(GeneratorSyntaxContext context)
{
var classDecl = (ClassDeclarationSyntax)context.Node;
// 写入日志
File.AppendAllText("generator.log",
$"Processing: {classDecl.Identifier.Text}\n");
// ...
}✅ 检查点
✅ 检查点
完成以下任务后,可以进入下一步:
📝 小测验
测试你的理解:
为什么增量生成器比传统生成器快?
查看答案
增量生成器使用缓存机制。当输入数据没有变化时,直接使用缓存的结果,跳过代码生成步骤。只有当输入数据变化时,才重新生成代码。为什么要使用 record 类型?
查看答案
record 自动实现 IEquatable<T>,提供高效的值相等比较。增量生成器使用这个比较来判断数据是否变化,从而决定是否使用缓存。为什么要分两阶段过滤?
查看答案
第一阶段(predicate)只做语法检查,非常快,可以过滤掉大部分不相关的节点。第二阶段(transform)使用语义模型,只处理候选节点,减少开销。
💡 最佳实践总结
✅ 推荐做法
- 使用 record 类型:自动实现 IEquatable
- 使用 ImmutableArray:不可变集合
- 快速语法过滤:在 predicate 中只做语法检查
- 最小化数据:只传输必要的信息
- 使用 static 方法:避免闭包
❌ 避免做法
- 在 predicate 中使用语义模型:太慢
- 使用可变集合:破坏缓存
- 传输大量数据:增加比较开销
- 包含不必要的数据:影响缓存效率
🎯 下一步
恭喜!你已经掌握了增量生成器的核心概念和优化技巧。现在你知道了:
- ✅ 增量生成器的工作原理
- ✅ 缓存机制和性能优化
- ✅ 如何编写高性能的生成器
- ✅ 常见问题和解决方案
在下一步中,我们将学习更多进阶主题,包括 Builder 模式生成器、诊断报告等。