【问题标题】:How do the .NET JITs optimise generated code layout?.NET JIT 如何优化生成的代码布局?
【发布时间】:2016-10-27 13:53:29
【问题描述】:

早在 2009 年,我在 this answer 上发布了一个关于嵌套 try/catch/finally 块优化的问题。

几年后再次考虑这个问题,似乎这个问题可以扩展到其他控制流,不仅是try/catch/finally,还有if/else

在这些交叉点中的每一个,执行都将遵循一条路径。显然,两者都必须生成代码,但是它们在内存中的放置顺序以及导航它们所需的跳转次数会有所不同。

生成的代码在内存中的布局顺序会影响 CPU 指令缓存的未命中率。让指令流水线停止,等待内存读取,真的会扼杀循环性能。

我不认为循环 (for/foreach/while) 非常适合,除非您期望循环的零迭代次数比它的迭代次数多,因为自然生成顺序看起来很漂亮最佳。

一些问题:

  • 可用的 .NET JIT 以哪些方式优化生成的指令顺序?
  • 这在实践中对通用代码有多大影响?非常适合的案例呢?
  • 开发人员可以做些什么来影响此布局?与被禁止的goto 搞混呢?
  • 使用的特定 JIT 是否会对布局产生很大影响?
  • 内联启发式方法也在这里发挥作用吗?
  • 基本上任何与 JIT 的这方面相关的有趣内容!

一些初步的想法:

catch 块移出线是一件容易的事,因为根据定义,它们应该是例外情况。不确定会发生这种情况。

对于某些循环,我怀疑您可以不平凡地提高性能。但总的来说,我认为这不会有太大的不同。

我不知道 JIT 如何决定生成代码的顺序。在 Linux 上的 C 中,您有 likely(cond) and unlikely(cond) 可以用来告诉编译器哪个分支是要优化的公共路径。我不确定所有编译器都尊重这些宏。

指令排序与分支预测的问题不同,其中 CPU 猜测(自行,afaik)将采用哪个分支以启动管道(过于简单的步骤:解码、获取操作数、执行、写回) 在指令上,在执行步骤确定条件变量的值之前。

我想不出任何方法来影响 C# 语言中的这个顺序。也许你可以通过goto显式地对标签进行一些操作,但这是否可移植,还有其他问题吗?

也许这就是配置文件引导优化的目的。我们现在或计划中是否在 .NET 生态系统中拥有它?也许我会去阅读一下LLILC

【问题讨论】:

  • 好在 JIT 可以使用运行时指标进行优化。循环几次后,它可以找出更好的路径。
  • @Caramiriel 是的,JVM 这样做效果很好。不幸的是,.NET JIT 很差。没有运行时分析。
  • 这不是抖动的工作。移动代码需要使用 mpgo.exe(托管配置文件引导优化)工具和 Ngen.exe 来使用配置文件数据。精确到 +/- 2%,实际上没有人使用它,从 SO 中没有人问过有关它的问题可以看出这一点。我知道它使 I-cache 保持热状态,这很容易做到。它是否改变分支是一个盲目的猜测,微软并没有吹嘘它,也没有在 channel9 视频中详细介绍。回报并不存在,PGO 只会为您带来众所周知的 15% 的改进。
  • 快速跟进。我修改了我的序列化/反序列化库以发出异常(不太可能)的代码正常的直线代码之后。基准测试显示反序列化提高了 14.3%(在此期间进行了大量验证)。不需要任何 PGO 来提升,只是预感。

标签: c# .net optimization code-generation jit


【解决方案1】:

您所说的优化称为代码布局优化,定义如下:

  • 在同一线程中及时执行的那些代码片段在虚拟地址空间中应该是接近的,以便它们适合单个或几个连续的高速缓存行。这样可以减少缓存未命中。
  • 那些在不同线程中及时执行的代码片段在虚拟地址空间中应该是接近的,以便它们适合单个或几个连续的缓存行,只要没有自修改代码。这比前一个优先级低。这样可以减少缓存未命中。
  • 那些频繁执行的代码(热代码)应该靠近虚拟地址空间,以使它们适合尽可能少的虚拟页面。这减少了页面错误和工作集大小。
  • 那些很少执行的代码(冷代码)应该靠近虚拟地址空间,以便它们尽可能少地放入虚拟页面。这减少了页面错误和工作集大小。

现在回答你的问题。

可用的 .NET JIT 以哪些方式优化生成的 指令顺序?

“指令顺序”真的是一个很笼统的名词。许多优化会影响指令顺序。我假设您指的是代码布局。

JITters 的设计应该花费最少的时间来编译代码,同时生成高质量的代码。为了实现这一点,他们只执行最重要的优化,因此真的值得花时间去做。代码布局优化不是其中之一,因为没有分析,它可能没有好处。虽然 JITter 当然可以执行分析和动态优化,但通常有一种首选方式。

这在实践中对通用代码有多大影响?什么 关于完全适合的案例?

代码布局优化本身通常可以将整体性能提高 -1%(负数)到 4%,这足以让编译器编写者满意。我想补充一点,它通过减少缓存未命中来间接降低能耗。指令缓存的未命中率通常可降低高达 35%。

开发人员可以做些什么来影响这个布局吗?什么 关于使用被禁止的 goto 进行修改?

是的,有很多方法。我想提一下一般推荐的mpgo.exe。请不要将 goto 用于此目的。这是禁止的。

所使用的特定 JIT 是否会对布局产生很大影响?

没有。

内联启发式方法也在这里发挥作用吗?

内联确实可以改善函数调用方面的代码布局。这是最重要的优化之一,所有 .NET JIT 都会执行它。

将 catch 块移出线是一件容易的事,因为它们应该这样做 根据定义是例外情况。不确定会发生这种情况。

是的,它可能很“容易”,但潜在的好处是什么? catch 块通常很小(包含对处理异常的函数的调用)。处理这种特殊的代码布局情况似乎并不乐观。如果您真的在乎,请使用mpgo.exe

我不知道 JIT 如何决定生成代码的顺序。在 C 上 Linux 你有可能(cond)和不太可能(cond),你可以用来 告诉编译器哪个分支是要优化的公共路径。

使用 PGO 比使用 likely(cond)unlikely(cond) 更可取,原因有两个:

  1. 程序员在代码中放置可能(cond) 和不太可能(cond) 时可能会无意中出错。它实际上发生了很多。在尝试手动优化代码时犯下大错是很常见的。
  2. 在整个代码中添加可能(cond) 和不太可能(cond) 会降低它在未来的可维护性。每次更改源代码时,您都必须确保这些提示有效。在大型代码库中,这可能是(或者更确切地说是)一场噩梦。

指令排序与分支问题不同 预测...

假设您在谈论代码布局,是的,它们是不同的。但是代码布局优化通常由真正包含分支统计信息的配置文件指导。硬件分支预测当然完全不同。

也许我会去阅读有关 LLILC 的文章。

虽然使用 mpgo.exe 是执行此优化的主流方式,但您也可以使用 LLILC,因为 LLVM 也支持配置文件引导优化。但我认为你不需要走这么远。

【讨论】:

  • 非常感谢您的详细回答。是的,我指的是您所说的代码布局(我假设指令排序处于更精细的粒度级别)。就我而言,我认为 PGO 不会工作,因为我正在使用 Reflection.Emit 生成序列化/反序列化代码。正如我在 cmets 中提到的,通过将 catch 块移出热路径 (14.3%) 以进行反序列化,我观察到的性能改进比您引用的要大得多。但是,我没有移动接球块,而是移动了最终抛出的块。对于具有多次抛出的大型方法,这可能是有意义的。
  • @DrewNoakes 我提到的性能改进来自现实世界的大型程序,这种单一的优化并不能真正起到多大作用。但我相信您可以设计出展示更大改进的基准,但希望能对您的特定现实世界场景进行建模。实际上,当前形式的 mpgo.exe 不适用于Reflection.Emit。我还想补充一点,ProfileOptimization 目前无法处理Reflection.Emit。如果您向我提供有关您如何/何时/什么/谁使用Reflection.Emit 的更多详细信息,我可能会建议一个相当简单的解决方案。
  • Here's the commit 在其中我介绍了“收集”所有在DynamicMethod 的 IL 流末尾发出的抛出代码块的更改。因此,假设反序列化成功发生,代码采用一条非常线性的路径,没有太多的跳转。也就是说,这些延迟抛出块的分支不被采用。我应该注意到,我是在提出这个问题后做出的承诺。
  • @DrewNoakes 谢谢。我去看看。
  • @DrewNoakes 你能告诉我你在哪个微架构和缓存组织上获得了 14.3% 的加速吗?
猜你喜欢
  • 2021-06-23
  • 1970-01-01
  • 2010-10-19
  • 1970-01-01
  • 1970-01-01
  • 2021-12-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多