在 Visual Studio 2026 中,我们推出了 Copilot Profiler Agent,这是一款新的人工智能驱动的助手,可帮助您分析和优化代码中的性能瓶颈。通过将 GitHub Copilot 的功能与 Visual Studio 的性能分析器相结合,您现在可以用自然语言询问有关性能的问题,深入了解热点路径,并快速发现优化机会。让我们来看一个真实的例子,了解这款工具如何帮助您实现有意义的性能改进。

对实际项目进行基准测试

  为了展示 Copilot Profiler Agent 的功能,让我们对一个广受欢迎的开源项目 CsvHelper 进行优化。您可以按照以下步骤操作:克隆我的代码仓库分支,然后通过“git checkout 435ff7c”命令切换到我修复之前的版本,我们将在下文详细介绍该修复。

  在我之前的一篇博客文章中,我添加了一个 CsvHelper.Benchmarks 项目,其中包含一个用于读取 CSV 记录的基准测试。这次我想看看我们是否可以优化 CSV 记录的写入。通常,我会通过为想要优化的代码创建基准测试来开始这项研究,不过虽然我们仍然会这样做,但我们可以让 Copilot 来承担这些繁重的工作。在 Copilot 聊天窗口中,我可以问@Profiler “帮我为 #WriteRecords 方法编写一个基准测试”。@Profiler 让我们直接与 Copilot Profiler Agent 对话,而 #WriteRecords 则明确告诉它我们要进行基准测试的方法。

1

  从这里开始,Copilot 着手创建我们的新基准测试,它会询问我们是否可以安装分析器的 NuGet 包,以便在运行基准测试时从中提取信息。它还会根据找到的任何现有基准测试来构建新的基准测试模型,因此生成的基准测试与我们已经编写的非常相似,从而保持与存储库风格的一致性。最后,它会启动构建过程,以确保一切正常。

2

  完成后,它会提供一些有用的后续提示来启动调查。我们可以点击其中一个来展开调查,不过我想对基准测试做些细微的修改。

3

  我对基准测试做了些调整,增加了几个供我们写入的字段,这里具体是 2 个整数字段和 2 个字符串字段。在为这篇博客撰写内容之前,我最初让 Copilot 来做这件事时,它每次都是写入一个新的内存流,而不是同一个内存流。写入同一个内存流或许是更好的做法,这次算你赢了 Copilot,但在我给 CsvHelper 提交的最初的拉取请求中,我并没有这么做,不过应该也没什么问题。

public class BenchmarkWriteCsv 
{ 
    private const int entryCount = 10000; 
    private readonly List records = new(entryCount); 

    public class Simple 
    { 
        public int Id1 { get; set; } 
        public int Id2 { get; set; } 
        public string Name1 { get; set; } 
        public string Name2 { get; set; } 
    } 

    [GlobalSetup] 
    public void GlobalSetup() 
    { 
        var random = new Random(42); 
        var chars = new char[10]; 

        string getRandomString() 
        { 
            for (int i = 0; i < 10; ++i) 
                chars[i] = (char)random.Next('a', 'z' + 1); 
            return new string(chars); 
        } 

        for (int i = 0; i < entryCount; ++i) 
        { 
            records.Add(new Simple 
            { 
                Id1 = random.Next(), 
                Id2 = random.Next(), 
                Name1 = getRandomString(), 
                Name2 = getRandomString(), 
            }); 
        } 
    } 

    [Benchmark] 
    public void WriteRecords() 
    { 
        using var stream = new MemoryStream(); 
        using var streamWriter = new StreamWriter(stream); 
        using var writer = new CsvHelper.CsvWriter(streamWriter, CultureInfo.InvariantCulture); 
        writer.WriteRecords(records); 
        streamWriter.Flush(); 
    } 
} 

深入了解基准测试

  现在开始分析,我既可以让 Profiler Agent 运行基准测试,也可以直接点击后续提示“@Profiler Run the benchmark and analyze results”。从这里开始,Copilot 会编辑我的主方法,乍一看可能有些奇怪,但查看所做的更改后,我发现它为了使用 BenchmarkSwitcher 进行了必要的修改,这样就能选择要运行的基准测试了:        

static void Main(string[] args)
{
    // Use assembly-wide discovery so all benchmarks in this assembly are run,
    // including the newly added BenchmarkWriteRecords.
    _ = BenchmarkSwitcher.FromAssembly(typeof(BenchmarkEnumerateRecords).Assembly).Run(args);
}

        然后它启动了一次基准测试运行,完成后会给我一个诊断会话,我可以在其中开始调查。

使用 Copilot Profiler Agent 来查找瓶颈

  现在到了令人兴奋的部分。运行基准测试后,Profiler Agent 会分析跟踪信息,并突出显示时间的消耗位置。我可以向 Profiler Agent 询问有关跟踪的问题,让它解释代码为什么运行缓慢,或者某些优化为何会有帮助。它已经指出,大部分时间都花在委托编译和调用上,这是针对 CSV 记录中的每个字段进行的。对于一个有 4 个字段、被写入 10,000 次的记录来说,这意味着会有 40,000 次委托调用。每次调用都有开销,而这在分析器中显示为一个热点路径。

6

  我可以问 Profiler Agent:“我怎样才能减少委托调用的开销?”或者“为什么委托调用很慢?”,而它会像一位耐心的老师一样解释相关概念并提出修复建议。

实施修复方案

  我点击 @Profiler Optimize library to produce a single compiled write delegate (reduce multicast invokes),看看会得到什么结果。 Profiler Agent 会对 ObjectRecordWriter 进行编辑,我可以在聊天窗口中点击它来查看所做更改的差异。

  查看当前的实现,代码构建了一个委托列表,每个字段对应一个委托:

var delegates = new List<Action>();

foreach (var memberMap in members)
{
    // ... field writing logic ...

    delegates.Add(Expression.Lambda<Action>(writeFieldMethodCall, recordParameter).Compile());
}

var action = CombineDelegates(delegates) ?? new Action((T parameter) => { });
return action;

        问题在于 CombineDelegates 会创建一个多播委托,该委托会依次单独调用每个独立的委托。相反,Profiler Agent 建议我们在编译前使用 Expression.Block 来组合所有表达式:

var expressions = new List<Expression>(members.Count);

foreach (var memberMap in members)
{
    // ... field writing logic ...

    expressions.Add(writeFieldMethodCall);
}

if (expressions.Count == 0)
{
    return new Action<T>((T parameter) => { });
}

// Combine all field writes into a single block
var block = Expression.Block(expressions);
return Expression.Lambda<Action<T>>(block, recordParameter).Compile();

        这一改动虽小却很精妙:我们没有创建多个委托并按顺序调用它们,而是创建了一个包含所有字段写入操作的单个块表达式,然后对其进行一次编译。现在,当我们为每条记录调用委托时,所有字段都会在一次调用中完成写入,不存在额外的委托开销。

衡量影响

  做出这一更改后,Copilot 会自动重新运行基准测试以衡量改进效果。结果显示,在此次使用分析器的运行中,性能大约提升了 24%。我们之前为 CsvHelper 准备的分阶段拉取请求显示性能提升了约 15%。CPU 分析器证实,我们已经消除了委托调用的开销,对于每条有 4 个字段的 10,000 条记录,之前需要进行 40,000 次委托调用,而现在只需要 10,000 次委托调用。

9

  对于一个已经经过大量优化的库来说,这是一场意义重大的胜利。对于那些编写包含许多字段的大型 CSV 文件的应用程序而言,这一改进直接意味着 CPU 时间的减少和处理速度的提升。而且,由于 CsvHelper 的下载量高达数百万次,这项优化惠及了大量用户。在此基础上,我继续推进并提交了拉取请求,不过 Copilot 贴心地提供了更多关于类型转换和 ShouldQuote 逻辑的后续提示,以便我能进一步提升性能。

Copilot Profiler Agent 的价值

  这个工作流程之所以强大,是因为它将 Visual Studio Profiler 提供的精确性能数据与 Copilot 的分析和代码生成能力相结合。您无需手动深入研究 CPU 跟踪并试图理解热点路径的含义,而是可以提出自然语言问题,获取可执行的见解,并快速测试想法。

  该 Agent 不仅会告诉您哪些部分运行缓慢,还会帮助您理解其缓慢的原因,并提出具体的修复方法。在这种情况下,它识别出委托调用的开销是瓶颈,并建议采用 Expression.Block 优化,这正是解决该问题的正确方案。它甚至还重新运行了基准测试来确认该优化的效果!

让我们知道您的想法

  我们已经展示了 Copilot Profiler Agent 如何帮助您处理实际项目,通过自然语言查询识别性能瓶颈,并在数据支持下做出有意义的改进。当您能够就性能数据提出问题并获得智能答案时,测量/更改/测量的循环会变得快得多。我们很想听听您的想法!

原文链接:https://devblogs.microsoft.com/visualstudio/delegate-the-analysis-not-the-performance/

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐