.NET 7首次运行for循环性能异常:原因及解决方法咨询
我正在开发一款对性能敏感的应用,正考虑从.NET 6迁移至.NET 7。对比两个版本时发现,.NET 7中for循环首次运行时速度更慢。
测试基于两个代码完全相同的控制台应用,分别基于.NET 6和.NET 7,以Release模式、Any CPU配置运行。
测试代码:
using System.Diagnostics; int size = 1000000; Stopwatch sw = new(); //create array float[] arr = new float[size]; for (int i = 0; i < size; i++) arr[i] = i; Console.WriteLine(AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName); Console.WriteLine($"\nForLoop1"); ForLoop1(); ForLoop1(); ForLoop1(); ForLoop1(); ForLoop1(); Console.WriteLine($"\nForLoopArray"); ForLoopArray(); ForLoopArray(); ForLoopArray(); ForLoopArray(); ForLoopArray(); Console.WriteLine($"\nForLoop2"); ForLoop2(); ForLoop2(); ForLoop2(); ForLoop2(); ForLoop2(); void ForLoop1() { sw.Restart(); int sum = 0; for (int i = 0; i < size; i++) sum++; sw.Stop(); Console.WriteLine($"{sw.ElapsedTicks} ticks ({sum})"); } void ForLoopArray() { sw.Restart(); float sum = 0f; for (int i = 0; i < size; i++) sum += arr[i]; sw.Stop(); Console.WriteLine($"{sw.ElapsedTicks} ticks ({sum})"); } void ForLoop2() { sw.Restart(); int sum = 0; for (int i = 0; i < size; i++) sum++; sw.Stop(); Console.WriteLine($"{sw.ElapsedTicks} ticks ({sum})"); }
.NET 6版本输出:
.NETCoreApp,Version=v6.0 ForLoop1 2989 ticks (1000000) 2846 ticks (1000000) 2851 ticks (1000000) 3180 ticks (1000000) 2841 ticks (1000000) ForLoopArray 8270 ticks (4.9994036E+11) 8443 ticks (4.9994036E+11) 8354 ticks (4.9994036E+11) 8952 ticks (4.9994036E+11) 8458 ticks (4.9994036E+11) ForLoop2 2842 ticks (1000000) 2844 ticks (1000000) 3117 ticks (1000000) 2835 ticks (1000000) 2992 ticks (1000000)
.NET 7版本输出:
.NETCoreApp,Version=v7.0 ForLoop1 19658 ticks (1000000) 2921 ticks (1000000) 2967 ticks (1000000) 3190 ticks (1000000) 3722 ticks (1000000) ForLoopArray 20041 ticks (4.9994036E+11) 8342 ticks (4.9994036E+11) 9212 ticks (4.9994036E+11) 8501 ticks (4.9994036E+11) 9726 ticks (4.9994036E+11) ForLoop2 14016 ticks (1000000) 3008 ticks (1000000) 2885 ticks (1000000) 2882 ticks (1000000) 2888 ticks (1000000)
可见.NET 6的耗时数值稳定,而.NET 7首次运行耗时明显偏高。调整环境变量DOTNET_ReadyToRun和DOTNET_TieredPGO后情况反而更糟,询问该现象的原因及解决方法。
这一现象的核心原因是.NET 7默认启用了Tiered Compilation Level 2(分层编译2级),这是.NET 7对JIT编译策略的优化调整:
- 在Tiered Compilation Level 2模式下,方法首次执行时会先运行快速编译的未优化Tier 0代码,后台异步编译高度优化的Tier 1代码,后续调用会切换到Tier 1版本。你的测试代码首次计时包含了JIT编译Tier 0代码的开销,因此耗时陡增。
- .NET 6默认使用的是Tiered Compilation Level 1,首次执行时会直接编译优化后的代码,因此首次运行耗时更稳定。
- 调整
DOTNET_ReadyToRun和DOTNET_TieredPGO后性能恶化的原因:- ReadyToRun是预编译技术,但它生成的代码优化程度低于JIT编译,若仅通过环境变量强制启用,可能覆盖JIT的优化逻辑,导致首次运行仍需额外处理。
- Tiered PGO需要先收集运行时数据才能生成优化代码,首次运行时不仅要处理编译,还要收集数据,进一步增加了首次执行的开销。
根据你的性能敏感需求,可以通过以下方式缓解首次运行的性能问题:
1. 回退到分层编译Level 1
设置环境变量:
DOTNET_TieredCompilation=1
该配置会让.NET 7采用和.NET 6类似的分层编译策略,首次执行直接编译优化后的代码,消除首次运行的额外编译开销,但会损失Tiered Compilation Level 2带来的后台异步编译的长期优势。
2. 启用循环快速JIT
针对循环场景,可启用专门的优化开关:
DOTNET_TieredCompilation_QuickJitForLoops=1
该配置会让JIT快速编译循环代码的优化版本,既保留Tiered Compilation Level 2的优势,又避免循环首次运行的高耗时。
3. 代码预热
在应用启动阶段,提前调用关键的循环方法(不计时),让JIT完成编译工作,后续正式调用时即可直接使用优化后的代码。例如在测试代码开头添加:
// 预热代码 ForLoop1(); ForLoopArray(); ForLoop2();
4. 合理配置ReadyToRun
若使用ReadyToRun,确保项目发布时启用PublishReadyToRun选项,而非仅通过环境变量调整。ReadyToRun预编译的代码可以减少JIT编译的次数,但需要配合正确的发布配置才能发挥效果。
内容的提问来源于stack exchange,提问作者jrw

