在其展开循环文章中,维基百科有以下声明:增加的程序代码大小可能不受欢迎,特别是对于嵌入式应用程序。还可能导致指令高速缓存未命中率增加,从而对性能产生不利影响。为什么会这样呢?此外,由于大量死代码导致程序代码大小变大并不会增加缓存未命中率,因为死代码不会被执行。
代码通常整块地读入缓存,称为缓存行,它的大小可能是64、128或256个字节。如果你有一个256个字节的缓存行,其中四分之三都是无用代码,那么你并没有很好地利用你的缓存内存。另一方面,如果你有一兆字节完全未使用的代码,那并不会影响缓存效率。一些编译器将使用启发式算法或软件开发人员提供的提示来确定哪些代码可能非常少使用,并安排代码,使得一个缓存行将被完全填充使用的代码或完全填充未使用的代码。与无用代码不同,由于循环展开而膨胀的使用代码确实会增加缓存未命中。
维基百科关于循环展开的文章列出了该技术可能的几个缺点,其中两个与代码大小有关: 增加程序代码大小,这可能是不可取的,特别是对于嵌入式应用。也可能导致指令高速缓存未命中增加,从而对性能产生负面影响。 如果循环体中的代码涉及函数调用,则可能无法将展开与内联结合使用,因为代码大小的增加可能过多。因此,在这两种优化之间可能需要权衡。 循环展开会增加静态代码大小,因为它会复制在循环中的部分代码。希望通过这样做可以减少需要执行的循环迭代次数,从而减少动态指令计数。对于一个小的循环体,增加循环体内的指令数量可能不会对指令缓存命中率产生负面影响。但随着循环体变得更大,增加的代码大小可能会导致指令缓存中其他有用的行被替换,从而降低整体命中率。这主要是因为执行的代码量更大。当循环体包含函数调用且编译器必须确定是否内联函数时,循环展开变得特别复杂。或者如果循环是一个可能在其他地方内联的函数的一部分。内联和循环展开都有可能减少动态指令计数,但它们也会增加静态代码大小。因此,编译器通常使用启发式算法来选择如何组合这些优化,因为问题无法以最佳方式解决(至少不能在合理的时间内)。正如其他答案中提到的,对齐问题可能也会导致更多的指令缓存未命中。然而,循环展开可能导致更多的缓存未命中的主要原因是它增加了实际在循环体中执行的代码量(以及额外的序言和尾声代码)。
展开循环可以使它们更快,但代价是额外的指令高速缓存未命中。而当我们遇到指令高速缓存未命中时,无法调度任何有用的代码。那么,为什么我们还要展开循环呢(如果我们有足够的内存)?因为如果我将循环展开4次会导致在1000次迭代中运行速度提升25%,那么即使在循环后支付1个高速缓存未命中的代价也仍然更快。所以你必须权衡使用更多代码所带来的相对成本和额外的高速缓存未命中的成本。额外的代码也会导致一些额外的TLB未命中,但这个论点仍然适用。