内联和指令缓存命中率和抖动

时间:2018-03-17 09:30:39

标签: caching memory inline executable cpu-architecture

在本文https://www.geeksforgeeks.org/inline-functions-cpp/中,它指出内联的缺点是:

  

3)过多的内联也会降低指令缓存命中率,从而降低从缓存内存到主内存的指令获取速度。

内联如何影响指令缓存命中率?

  

6)内联函数可能会导致颠簸,因为内联可能会增加二进制可执行文件的大小。内存中的颠簸会导致计算机性能下降。

内联如何增加二进制可执行文件的大小?它只是增加了代码库的长度吗?此外,我不清楚为什么有一个更大的二进制可执行文件会导致颠簸,因为这两个似乎没有链接。

4 个答案:

答案 0 :(得分:4)

假设你有一个100个指令长的函数,只要它被调用就需要10个指令来调用它。

这意味着对于10次调用,它会在二进制文件中使用100 + 10 * 10 = 200条指令。

现在让我们说它在任何地方内联使用它。这会在二进制文件中使用100 * 10 = 1000条指令。

因此,对于第3点,这意味着它将在指令缓存中占用更多空间(内联函数的不同调用在i-cache中不是'共享')

对于第6点,您的二进制文件总大小现在更大,更大的二进制大小可能导致颠簸

答案 1 :(得分:2)

如果编译器内联所有内容,那么大多数功能都是巨大的。 (虽然您可能只有一个巨大的main函数调用库函数,但在最极端的情况下,程序中的所有函数都将内联到main)。

想象一下,如果所有内容都是宏而不是函数,那么它会在您使用它的任何地方完全扩展。这是内联的源级版本。

大多数功能都有多个呼叫站点。调用函数的代码大小随着args的数量而缩放,但与中到大函数相比通常相当小。因此,在其所有呼叫站点中内联大型函数将增加总代码大小,从而降低I-cache命中率。

但是现在很常见的做法是编写大量的小型包装器/辅助函数,尤其是在C ++中。单个版本的小函数的代码通常不比调用它所需的代码大很多,特别是当你包含函数调用的副作用时(比如clobbering寄存器)。内联小函数通常可以节省代码大小,尤其是在内联后进一步优化时。 (例如,函数计算函数外部的代码也计算的一些相同的东西,因此CSE是可能的。)

因此,对于编译器,是否内联到任何特定调用站点的决定应该基于被调用函数的大小,并且可能是否在循环内调用。 (如果调用站点运行得更频繁,优化掉调用/返回开销更有价值。)配置文件引导优化可以帮助编译器做出更好的决策,通过在热门函数上“花费”更多代码大小,并节省代码大小冷函数(例如,许多函数仅在程序的生命周期内运行一次,而一些热函数占用大部分时间)。

如果编译器没有很好的启发式内联时间,或者你将它们覆盖为方式过于激进,那么是的,I-cache misses就是结果

但是现代编译器具有良好的内联启发式,通常这会使程序显着加快但只有更大。你读过的文章谈到了为什么需要限制。

上面的代码大小推理应该明显表明可执行文件的大小会增加,因为它不会收缩任何数据。许多功能在可执行文件中仍然有一个独立的副本,在各个呼叫站点仍然有内联(和优化)副本。

有一些因素可以缓解I-cache命中率问题。更好的位置(从不跳得太多)让代码预取做得更好。许多程序将大部分时间花在总代码的一小部分上,这些代码在内联后通常仍然适用于I-cache。

但较大的程序(如Firefox或GCC)有很多代码,并且在大型“热”循环中从许多调用站点调用相同的函数。太多内联膨胀,每个热循环的总代码大小会损害它们的I-cache命中率。

  

内存中的颠簸会导致计算机性能下降。

https://en.wikipedia.org/wiki/Thrashing_(computer_science)

在具有多个GiB的RAM的现代计算机上,除非系统上的每个程序都使用极具侵略性的内联编译,否则虚拟内存(分页)的颠簸是不合理的。如今,大多数内存都被数据占用,而不是代码(特别是运行GUI的计算机中的像素图),因此代码必须爆炸几个数量级才能开始在整体内存压力上产生真正的差异。

颠覆I-cache几乎与拥有大量I-cache未命中相同。但是,有可能超越它来破坏缓存代码+数据的更大的统一缓存(L2和L3)。

答案 2 :(得分:2)

关于为什么内联可能会损害i-cache命中率或引起颠簸的混淆可能在于静态指令计数和动态指令计数之间的差异。内联(几乎总是)会减少后者,但往往会增加前者。

让我们简要地研究一下这些概念。

静态指令计数

某些执行跟踪的静态指令计数是二进制映像中出现的唯一 1 指令的数量。基本上,您只需计算汇编转储中的指令行。以下x86代码片段的静态指令数为5(.top:行是一个不会转换为二进制内容的标签):

  mov eci, 10
  mov eax, 0
.top:
  add eax, eci
  dec eci
  jnz .top

静态指令计数对于二进制大小和缓存注意事项非常重要。

静态指令计数也可简称为"代码大小"我有时会在下面使用这个术语。

动态指令计数

另一方面,动态指令计数取决于实际的运行时行为,并且是执行的指令数。由于循环和其他分支,相同的静态指令可以被多次计数,并且静态计数中包括的一些指令可能根本不会执行,因此在动态情况下不计数。上面的代码片段的动态指令计数为2 + 30 = 32:前两个指令执行一次,然后循环执行10次,每次迭代3个指令。

作为一种非常粗略的近似,动态指令计数对运行时性能至关重要。

权衡

许多优化(例如循环展开,函数克隆,向量化等)会增加代码大小(静态指令计数),以提高运行时性能(通常与动态指令计数密切相关)。

内联也是一种优化,尽管对于某些调用网站来说,内联会减少动态静态指令数。

  

内联如何影响指令缓存命中率?

文章提到了太多内联,这里的基本思想是通过增加工作集的静态指令数来大量内联增加代码占用空间通常减少其动态指令数。由于典型的指令缓存 1 缓存静态指令,因此较大的静态占用空间意味着缓存压力增加,并且通常会导致缓存命中率降低。

增加的静态指令计数是因为内联基本上复制了每个调用站点的函数体。因此,不是函数体的一个副本以及调用函数N次的一些指令,而是最终得到函数体的N个副本。

现在这是一个相当天真的内联模型,自 内联之后的内联工作原理如何,可能会在特定调用的上下文中进行进一步的优化-site,这可能会大大减少内联代码的大小。在非常小的内联函数或大量后续优化的情况下,内联后生成的代码甚至可能更小,因为剩余代码(如果有的话)可能小于调用函数 2

尽管如此,基本思想仍然存在:过多的内联可能会使二进制映像中的代码膨胀。

i-cache的工作方式取决于某些执行的静态指令计数,或者更具体地说是二进制映像中触及的指令缓存行数,这在很大程度上是一个相当直接的函数。静态指令计数。也就是说,i-cache缓存二进制映像的区域,因此区域越多,它们越大,缓存占用空间越大,即使动态指令数量恰好更低。

  

内联如何增加二进制可执行文件的大小?

它的原理与上面的i-cache情况完全相同:更大的静态占用空间意味着需要分页更多不同的页面,这可能会对VM系统造成更大的压力。现在我们通常以兆字节为单位来测量代码大小,而服务器,台式机等上的内存通常以千兆字节为单位进行测量,因此过度内联对于此类系统的颠簸有很大帮助。它可能是一个更小或嵌入式系统的问题(尽管后者通常根本没有MMU)。

0 这里 unique 例如指的是指令的IP,而不是指编码指令的实际值。您可能会在二进制文件的多个位置找到inc eax,但在这个意义上每个都是唯一的,因为它们出现在不同的位置。

1 有一些例外,例如某些类型的跟踪缓存。

2 在x86上,必要的开销几乎就是call指令。根据呼叫站点的不同,可能还有其他开销,例如将值混洗到正确的寄存器中以遵守ABI,以及溢出调用者保存的寄存器。更一般地说,函数调用的成本可能很高,因为编译器必须在函数调用中重置许多假设,例如内存状态。

答案 3 :(得分:1)

一般来说,由于呼叫站点被更大的代码片段替换,内联趋向于增加发出的代码大小。因此,可能需要更多的存储空间来保存代码,这可能导致颠簸。我将更详细地讨论这个问题。

  

内联如何影响指令缓存命中率?

内联对性能的影响很难在没有实际运行代码和测量其性能的情况下进行静态表征。

是的,内联可能会影响代码大小,并且通常会使发出的本机代码更大。让我们考虑以下情况:

  • 在两种情况下(有或没有内联),在特定时间段内执行的代码都适合内存层次结构的特定级别(比如L1I)。因此,关于该特定级别的表现不会改变。
  • 在没有内联的情况下,在特定时间段内执行的代码适合内存层次结构的特定级别,但不适合内联。这可能对性能产生的影响取决于执行的位置。基本上,如果最热的代码片段首先在该内存级别内,那么该级别的未命中率可能会略微增加。现代处理器的特征,例如推测执行,无序执行,预取可以隐藏或减少额外未命中的惩罚。值得注意的是,内联确实改善了代码的局部性,尽管代码大小增加,但这可能导致对性能的净积极影响。当频繁执行在呼叫站点内联的代码时尤其如此。已经开发了部分内联技术以仅内联被认为是热的功能部分。
  • 在两种情况下,在特定时间段内执行的代码都不适合内存层次结构的特定级别。因此,关于该特定级别的表现不会改变。
  

此外,我不清楚为什么要有更大的二进制可执行文件   文件会导致颠簸,因为两者似乎没有联系。

考虑资源受限系统上的主内存级别。即使代码大小仅增加5%也会导致主内存抖动,从而导致性能显着下降。在其他资源丰富的系统(桌面,工作站,服务器)上,当热指令的总大小太大而无法容纳在一个或多个缓存中时,抖动通常仅发生在缓存中。