【问题标题】:Inlining and Instruction Cache Hit Rates and Thrashing内联和指令缓存命中率和抖动
【发布时间】:2018-03-17 09:30:38
【问题描述】:

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

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

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

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

内联如何增加二进制可执行文件的大小?仅仅是它增加了代码库长度吗?此外,我不清楚为什么拥有更大的二进制可执行文件会导致抖动,因为两者似乎没有联系。

【问题讨论】:

  • 为什么投反对票??????这是一个完全合理的问题
  • 不是我的反对意见。但是 SO 解决了相当具体的问题,一般不提供“我不明白,向我解释”之类的答案。你从好心的人那里得到了一些人的反对意见,但有人可能认为你可以在发布问题之前自己花更多的精力研究这个话题。
  • 由于大量代码大小而导致交换空间/虚拟内存的抖动在现代台式机/笔记本电脑上是不合理的,除非内联数量非常不合理,产生数百或数千兆字节的可执行文件,或者如果整个系统是用一个疯狂的编译器编译的,所以所有的程序都很大,你有几个同时运行,而你“只有”几个 GiB 的物理 RAM。
  • @Jurassic - 否决票可能是因为 geeksforgeeks 已知包含很多废话,并且不是学习的好地方。在这种情况下,一些信息看起来像是 25 年前 Turbo-C++ 文档中的引用。 正确使用内联可以使代码更小更快。
  • @Jurassic - 我不是 DV(我是 UVed),但总的来说,你有一大群人讨厌基于“标准没有说”或“你不知道”的实施或性能相关问题“不需要知道”或“只需编写简单的代码,编译器就会对其进行排序”或“它是特定于实现的”或“编译器比你更聪明”或“它是否需要快”之类的。很多这样的人都在 C/C++ 标签上闲逛,如果你在那里问那种类型的问题,你就会得到即时的 DV。一个提示就是不要使用这些标签,而是包含其他标签。

标签: caching memory inline executable cpu-architecture


【解决方案1】:

关于为什么内联会损害 i-cache 命中率或导致抖动的困惑可能在于静态指令数和动态指令数之间的差异。内联(几乎总是)会减少后者,但通常会增加前者。

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

静态指令计数

某些执行跟踪的静态指令计数是二进制映像中出现的唯一指令的数量0。基本上,您只需计算程序集转储中的指令行数。以下 x86 代码的 sn-p 的静态指令计数为 5(.top: 行是一个标签,不会转换为二进制文件中的任何内容):

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

静态指令计数对于二进制大小和缓存考虑最为重要。

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

动态指令计数

另一方面,动态指令计数取决于实际的运行时行为,并且是执行的指令数。由于循环和其他分支,相同的静态指令可以被多次计数,并且静态计数中包含的某些指令可能根本不会执行,因此在动态情况下不计数。如上的sn-p,动态指令计数为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,以及溢出调用者保存的寄存器。更一般地说,函数调用可能会产生很大的成本,因为编译器必须在函数调用中重置其许多假设,例如内存状态。

【讨论】:

    【解决方案2】:

    假设你有一个 100 条指令长的函数,每次调用它都需要 10 条指令。

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

    现在让我们说它在所有使用的地方都是内联的。这会在您的二进制文件中使用 100*10 = 1000 条指令。

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

    对于第 6 点,您的总二进制大小现在更大,而更大的二进制大小可能会导致抖动

    【讨论】:

    • 这些计算不正确。当一个函数被内联时,它的代码会与它被内联的函数的代码一起优化。因此,得到的指令总数可能与单个函数的指令数之和有很大不同。
    • 我不明白为什么更大的二进制大小会导致抖动。甚至看起来都没有关系,因为我看不到二进制大小和内存管理是如何关联的
    • 这里的二进制大小也与 i-cache 工作集或静态指令占用有关。基本上,您的 i-cache 大小有限,因此执行更多总代码(以使用的缓存行衡量)意味着您将在各种缓存级别上承受更大的压力。这是因为内联基本上是关于复制代码。除了“i-cache”被“page cache”或“virtual memory/swap”或类似的东西替换之外,抖动是一样的。不过,基于我们所讨论的相对较小的尺寸,我认为不太可能会颠簸到磁盘。
    【解决方案3】:

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

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


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

    但如今,编写大量小型包装器/帮助器函数的常见做法,尤其是在 C++ 中。小函数的独立版本的代码通常不会比调用它所需的代码大多少,尤其是当您包含函数调用的副作用时(例如破坏寄存器)。内联小函数通常可以节省代码大小,尤其是在内联后可以进行进一步优化时。 (例如,函数计算一些函数外代码也计算的相同内容,因此CSE 是可能的)。

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

    如果编译器对何时内联没有很好的启发式方法,或者您将它们重写为方式过于激进,那么是的,I-cache 未命中将是结果。

    但现代编译器确实具有良好的内联启发式算法,通常这会使程序明显更快,但只是更大一点。您阅读的文章是在讨论为什么需要限制。


    上述代码大小的推理应该清楚地表明可执行文件大小增加了,因为它不会缩小任何数据。许多函数仍将在可执行文件中具有独立副本,并在各个调用站点具有内联(和优化)副本。

    有几个因素可以缓解 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)进行处理。

    【讨论】:

    • 内联还可以增加重用距离并降低使用频率,降低代码在共享级别缓存中的可能性。这在某种程度上与容量效应无关;如前所述,数据使用可以支配容量。 (使用共享库在多个线程甚至进程之间共享往往会增加这种效果。)
    【解决方案4】:

    一般而言,由于调用站点被更大的代码片段替换,内联往往会增加发出的代码大小。因此,可能需要更多的内存空间来保存代码,这可能会导致抖动。我会更详细地讨论这个问题。

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

    如果不实际运行代码并衡量其性能,通常很难静态描述内联对性能的影响。

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

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

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

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

    【讨论】:

    • 是的,投反对票应该附上原因
    • @BeeOnRope 我确实想知道为什么增加代码大小通常会降低 i-cache 命中率
    猜你喜欢
    • 2017-04-21
    • 2012-05-23
    • 1970-01-01
    • 1970-01-01
    • 2017-01-14
    • 1970-01-01
    • 2013-01-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多