.NET8 上的 Bing :动态PGO的影响

自从我上次更新大家有关.NET在Bing技术栈中的状态以来已经过去了一年多,尤其是位于核心位置的高性能工作流执行引擎。在这段时间里,这个引擎的应用范围只增不减,特别是随着Microsoft Copilot的发布。虽然我们的工作流引擎起源于Bing,但现在可以说它支撑了许多Microsoft应用程序中搜索和数据栈的相当大一部分。

我们从.NET 8的早期预览版开始进行测试。尽管在.NET 8的核心库中有明显的性能递增好处,但促使我们升级的最大因素是对动态PGO(Profile Guided Optimization,配置文件指导的优化)的显著改进。这一功能自.NET 6起就以预览形式存在,在.NET 8中,改进足够显著,以至于默认启用了这一功能。

之前的帖子:

迁移Bing工作流引擎到 .NET5

.NET 5 升级到 .NET 7,再次为必应带来性能提升

动态PGO

鉴于我们的规模,有时会有一些功能开箱即用就能在几乎所有应用中表现良好,但我们仍会给予额外的考虑。

在进程启动时,这个服务器会加载数千个合作伙伴组件,这些组件包含了我们执行工作流程所需的插件。这大约有2GB的代码,其中很多都需要即时编译(JIT)。当第一个用户查询请求击中服务器时,它需要能够在几百毫秒内提供答案,同时避免因即时编译而导致的暂停。

人们自然会想到,像NGEN和Ready2Run这样的技术是否会有所帮助。我们尝试过预编译代码,并取得了不同程度的成功。最终,我们发现最佳的性能与启动时间最小化的平衡是让JIT自行处理,结合在以往运行中检测到的特别关键的方法列表进行预编译。我们在启动时并行加载其他数据时做这件事。我们还在接受真实用户流量之前,通过系统推送一些测试查询。

动态PGO通过根据需要重新编译某些代码来提高运行时代码的质量。理论上,这可以帮助我们改善延迟,但我们需要彻底测试它对启动和第一个用户查询的影响。关于动态PGO的工作原理,你可以在其他地方阅读到,但简而言之:

动态PGO会在新即时编译的代码中加入一些轻量级指令,以记录性能特征并建立一个重新编译候选队列。可能的优化包括:

  • 内联
  • 方法去虚拟化
  • 循环优化
  • 尾递归移除
  • 优化内存中的代码布局以优化处理器缓存
  • ......还有更多

在测试中,我们看到了两个主要结果:

  1. 稳定状态下性能显著提升。
  2. 我们最大的工作负载在第一个用户查询时有一个小的延迟峰值,这表明要么有些方法根本没有编译,要么是因为分析的影响太大。这使得总延迟超出了最高限制。对于我们较小的工作负载来说,这不是问题。 下面的延迟图显示与基线相比,延迟有一个大幅度的峰值:

(请注意,本文档中的图表已删除了特定的内部指标信息,但形状的变化应能让您大致了解相对变化情况。)

在深入调查和来自 .NET 团队的帮助后,我们发现在我们遇到第一个用户查询之前,重新编译(re-jit)队列的大小已经增长到超过300,000个方法!正如我所说,我们的代码量非常大。最终,当进程运行几个小时后,我们通常会有超过两百万个方法。

作为响应,我们实施了一些小的改动:

  1. 增加了额外的预热查询,给更多方法机会进行即时编译(jit)和重新编译(re-jit)。
  2. 在接受用户流量之前稍作暂停,以便让队列有时间排空(我们起初将其静态配置,但有一个事件可以让您监控 JIT 队列大小)。
  3. 一些针对我们超大场景的自定义 JIT 设置:
sh 复制代码
REM 启用 64 位计数器以稍微减少错误共享
set DOTNET_JitCollect64BitCounts=1

REM 移除启动分层编译的延迟
REM (目标是减少总体上执行带有检测代码的方法所花费的时间)
set DOTNET_TC_CallCountingDelayMs=0

有了这些变化,延迟峰值消失了,现在我们可以享受稳定状态下的性能改进。

性能提升

我们所见到的在多个性能特征上的改进,或许是自从从.NET Framework迁移到.NET 5以来最显著的一次。

我们执行一个查询所消耗的CPU周期数减少了13%。换句话说,我们的效率提高了13%,或者再换一种方式表达:这意味着我们需要购买的机器数量减少了13%,以应对不断增长的需求。下面的图表显示了执行工作流程时总CPU时间的下降。

一个图表显示了查询执行中CPU时间的减少。

受到gen0或gen1垃圾回收影响的查询比例下降了20%(我们几乎从不会有查询受到gen2 GC的影响,因为A. 我们的内存管理策略避免了这一点,B. 我们采取了额外的措施以确保机器在gen2 GC迫在眉睫时不会服务用户------这是极其罕见的)。

这个图表显示了GC对查询影响的相对差异:

图表显示了受垃圾回收影响的查询百分比的下降。许多其它指标也显示出查询执行各个阶段有类似上述图表的改进。一些内部延迟降低了超过25%。查询服务的很大一部分是等待其他后端,因此总体查询服务时间的改进幅度稍微小一些,大约8%------但这仍然非常显著!

总结

总的来说,这次的 .NET 发布对我们来说既稳固又相对容易。我们在延迟上取得了改进,在效率上也有了巨大提升,这将在未来几年为我们节省数百万美元。尽管在我们庞大的代码库和严格的延迟要求下,动态 PGO 需要一些微调,但在运行时性能方面,它确实是一个巨大的胜利。

现在,我需要开始为 .NET 9 做准备了......希望在明年能再次向你们报告!

原文信息

作者:Ben Watson Principal Software Engineer, Bing Platform

链接:devblogs.microsoft.com/dotnet/bing...

相关推荐
摸鱼的春哥5 分钟前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
Victor35622 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack22 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo23 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor35624 分钟前
MongoDB(3)什么是文档(Document)?
后端
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX9 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法9 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate