Skip to content

第 5 步:性能优化 - 增量生成器

加载进度中...

📋 本步目标

  • 理解增量生成器的工作原理
  • 学习如何优化生成器性能
  • 对比传统生成器和增量生成器的性能差异
  • 掌握缓存机制和最佳实践
  • 将 Step 3 的生成器改造为增量版本

⏱️ 预计时间

约 2-3 小时


🎯 为什么需要增量生成器?

在 Step 3 中,我们实现了一个基本的 ToString 生成器。但是有一个性能问题:

传统生成器的问题

  • ❌ 每次编译都重新执行,即使代码没有变化
  • ❌ 处理所有文件,即使只修改了一个文件
  • ❌ 大型项目编译速度慢

增量生成器的优势

  • ✅ 缓存未变化的结果
  • ✅ 只处理变化的部分
  • ✅ 显著提高编译速度

性能对比

场景传统生成器增量生成器提升
首次编译1000ms1000ms-
无变化重编译1000ms10ms100倍
小改动重编译1000ms100ms10倍
大型项目(1000个类)30s3s10倍

📚 增量生成器原理

工作流程

编译开始

检查输入是否变化

├─ 未变化 → 使用缓存结果 → 跳过生成 ⚡
└─ 有变化 → 执行转换 → 生成代码 → 更新缓存

编译结束

缓存机制

增量生成器使用 IEquatable<T> 来判断数据是否变化:

csharp
// 使用 record 类型,自动实现 IEquatable
public record ClassInfo(
    string Name,
    string Namespace,
    ImmutableArray<PropertyInfo> Properties
);

// 当两个 ClassInfo 对象的所有属性都相同时,
// 它们被认为是相等的,生成器会使用缓存

缓存判断逻辑

  1. 计算输入数据的哈希值
  2. 与缓存中的哈希值比较
  3. 如果相同,使用缓存结果
  4. 如果不同,重新执行转换

🛠️ 实现增量生成器

回顾: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

用于实现高效的缓存机制,支持值相等性比较。

💡 下一步学习

完成本步骤后,建议:


📊 性能对比实验

实验 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&lt;T&gt;
  • 使用了可变集合(如 List&lt;T&gt;
  • 数据模型包含了不必要的数据

解决方案

csharp
// ✅ 正确:使用 record 和 ImmutableArray
public record ClassInfo(
    string Name,
    ImmutableArray&lt;PropertyInfo&gt; 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");
    
    // ...
}

✅ 检查点

✅ 检查点

完成以下任务后,可以进入下一步:


📝 小测验

测试你的理解:

  1. 为什么增量生成器比传统生成器快?

    查看答案 增量生成器使用缓存机制。当输入数据没有变化时,直接使用缓存的结果,跳过代码生成步骤。只有当输入数据变化时,才重新生成代码。
  2. 为什么要使用 record 类型?

    查看答案 record 自动实现 IEquatable<T>,提供高效的值相等比较。增量生成器使用这个比较来判断数据是否变化,从而决定是否使用缓存。
  3. 为什么要分两阶段过滤?

    查看答案 第一阶段(predicate)只做语法检查,非常快,可以过滤掉大部分不相关的节点。第二阶段(transform)使用语义模型,只处理候选节点,减少开销。

💡 最佳实践总结

✅ 推荐做法

  1. 使用 record 类型:自动实现 IEquatable
  2. 使用 ImmutableArray:不可变集合
  3. 快速语法过滤:在 predicate 中只做语法检查
  4. 最小化数据:只传输必要的信息
  5. 使用 static 方法:避免闭包

❌ 避免做法

  1. 在 predicate 中使用语义模型:太慢
  2. 使用可变集合:破坏缓存
  3. 传输大量数据:增加比较开销
  4. 包含不必要的数据:影响缓存效率

🎯 下一步

恭喜!你已经掌握了增量生成器的核心概念和优化技巧。现在你知道了:

  • ✅ 增量生成器的工作原理
  • ✅ 缓存机制和性能优化
  • ✅ 如何编写高性能的生成器
  • ✅ 常见问题和解决方案

在下一步中,我们将学习更多进阶主题,包括 Builder 模式生成器、诊断报告等。

⏭️ 继续学习


📚 扩展阅读

基于 MIT 许可发布