Per*_*ian 4 memory executable caching inline cpu-architecture
在这篇文章https://www.geeksforgeeks.org/inline-functions-cpp/ 中,它指出内联的缺点是:
3) 太多的内联也会降低你的指令缓存命中率,从而降低从缓存内存到主内存的指令提取速度。
内联如何影响指令缓存命中率?
6) 内联函数可能会导致抖动,因为内联可能会增加二进制可执行文件的大小。内存抖动会导致计算机性能下降。
内联如何增加二进制可执行文件的大小?仅仅是因为它增加了代码库长度吗?此外,我不清楚为什么拥有更大的二进制可执行文件会导致抖动,因为两者似乎没有联系。
关于为什么内联会损害 i-cache 命中率或导致抖动的混淆可能在于静态指令计数和动态指令计数之间的区别。内联(几乎总是)减少了后者,但经常增加了前者。
让我们简要地研究一下这些概念。
某些执行跟踪的静态指令计数是二进制映像中出现的唯一指令0的数量。基本上,您只需计算汇编转储中的指令行数。以下 x86 代码片段的静态指令计数为 5(该.top:行是一个标签,不会转换为二进制文件中的任何内容):
mov eci, 10
mov eax, 0
.top:
add eax, eci
dec eci
jnz .top
Run Code Online (Sandbox Code Playgroud)
静态指令计数对于二进制大小和缓存考虑最重要。
静态指令计数也可以简称为“代码大小”,我有时会在下面使用该术语。
在动态指令数,而另一方面,取决于实际运行时的行为,并指令数执行。由于循环和其他分支,同一条静态指令可以被多次计数,并且静态计数中包含的一些指令可能根本不会执行,因此在动态情况下不计数。上面的代码片段的动态指令计数为2 + 30 = 32:前两条指令执行一次,然后循环执行 10 次,每次迭代 3 条指令。
作为一个非常粗略的近似值,动态指令计数对于运行时性能来说是最重要的。
许多优化,例如循环展开、函数克隆、向量化等,都会增加代码大小(静态指令数)以提高运行时性能(通常与动态指令数密切相关)。
内联也是一种优化,尽管对于某些调用站点内联会减少动态和静态指令计数。
内联如何影响指令缓存命中率?
文章提到了太多内联,这里的基本思想是,大量内联通过增加工作集的静态指令数,同时通常会减少其动态指令数来增加代码占用空间。由于典型的指令缓存1缓存静态指令,较大的静态占用空间意味着增加缓存压力并且通常导致更差的缓存命中率。
静态指令计数的增加是因为内联本质上是在每个调用点复制函数体。因此,不是函数体的一份副本和一些调用函数N时间的指令,您最终会N得到函数体的副本。
现在这是关于内联如何工作的一个相当幼稚的模型,因为在内联之后,可能会在特定调用站点的上下文中进行进一步的优化,这可能会大大减少内联代码的大小。在非常小的内联函数或大量后续优化的情况下,内联后得到的代码可能更小,因为剩余的代码(如果有的话)可能比调用函数2 所涉及的开销要小。
尽管如此,基本思想仍然存在:过多的内联会使二进制图像中的代码膨胀。
i-cache 的工作方式取决于某些执行的静态指令计数,或者更具体地说,二进制映像中触及的指令缓存行的数量,这在很大程度上是静态指令计数的相当直接的函数。也就是说,i-cache 缓存二进制图像的区域,因此区域越多,越大,缓存占用空间越大,即使动态指令计数恰好较低。
内联如何增加二进制可执行文件的大小?
它的原理与上面的 i-cache 情况完全相同:更大的静态占用空间意味着需要调入更多不同的页面,这可能会给 VM 系统带来更大的压力。现在我们通常以兆字节为单位来衡量代码大小,而服务器、台式机等上的内存通常以千兆字节为单位,因此过多的内联不太可能对此类系统的颠簸产生有意义的影响。这可能是更小或嵌入式系统的一个问题(尽管后者通常根本没有 MMU)。
0这里的唯一性是指,例如,指令的 IP,而不是编码指令的实际值。您可能会inc eax在二进制文件中的多个位置找到,但从这个意义上说,每个位置都是独一无二的,因为它们出现在不同的位置。
1有例外,例如某些类型的跟踪缓存。
2在 x86 上,必要的开销几乎就是call指令。根据调用站点的不同,还可能存在其他开销,例如将值改组到正确的寄存器中以符合 ABI,以及溢出调用者保存的寄存器。更一般地说,函数调用可能会产生很大的成本,因为编译器必须在函数调用中重置许多假设,例如内存状态。
假设您有一个长达 100 条指令的函数,并且每次调用它都需要 10 条指令来调用它。
这意味着对于 10 次调用,它使用二进制文件中的 100 + 10 * 10 = 200 条指令。
现在让我们说它在它使用的任何地方都内联。这在您的二进制文件中使用了 100*10 = 1000 条指令。
因此,对于第 3 点,这意味着它将在指令缓存中占用更多空间(内联函数的不同调用不会在 i-cache 中“共享”)
对于第 6 点,您的总二进制大小现在更大,并且更大的二进制大小会导致颠簸