TL:DR: Sandybridge家族的存储转发在重新加载不需要立即发生时具有更低的延迟。添加无用代码可以加速调试模式循环,因为在-O0
反优化代码中,循环延迟瓶颈几乎总是涉及C变量的存储/重新加载。
其他示例包括:超线程, 调用空函数, 通过指针访问变量。
而且显然也适用于低功耗Goldmont,除非那里有不同的原因导致额外的加载。
对于优化的代码来说,这些都不相关。存储转发延迟的瓶颈偶尔会发生,但是在代码中添加无用的复杂性并不能加速它。
你正在对调试版本进行基准测试,
这基本上是无用的。它们与优化代码有不同的瓶颈,而不是统一的减速。
但显然,一个版本的调试构建运行速度比另一个版本的调试构建慢,这其中肯定有真正的原因。(假设您正确地测量了时间,并且不是CPU频率变化(涡轮/节能)导致墙钟时间差异。)
如果您想深入了解x86性能分析的细节,我们可以尝试解释为什么汇编会以第一种方式执行,以及为什么来自额外C语句的汇编(使用-O0编译为额外的汇编指令)可以使它整体上更快。
这将告诉我们一些关于汇编性能影响的信息,但对优化C没有任何用处。
你没有展示整个内部循环,只有一些循环体,但是gcc -O0
是相当可预测的。每个C语句都单独编译,与其他语句分开,每个语句块之间都要溢出/重新加载所有C变量。这使得您可以在单步调试时更改变量,甚至跳转到函数中的不同行,并且代码仍然有效。以这种方式编译的性能成本是灾难性的。例如,您的循环没有副作用(没有使用任何结果),因此整个三重嵌套循环可以编译为零条指令,在实际构建中无限制地运行得更快。或者更现实地说,即使没有优化或进行主要转换,每次迭代也可以运行1个周期,而不是约6个周期。
瓶颈可能在于循环中对k
的依赖性,需要进行存储/重新加载和add
操作来递增。存储转发延迟通常在大多数CPU上约为5个时钟周期左右。因此,内部循环仅能每6个时钟周期运行一次,即内存目标add
的延迟。
如果您使用的是英特尔CPU,则当重新加载不能立即执行时,存储/重新加载延迟实际上可以更低(更好)。在相关情况下,具有更多独立的加载/存储操作可能会解释这一点。请参见Loop with function call faster than an empty loop。
因此,随着循环中的工作量增加,那个addl $1, -12(%rbp)
将无法保持每6个时钟周期一次的吞吐量,而可能变成每4或5个周期只能迭代一次的瓶颈。
这种效应显然会在Sandybridge和Haswell上发生(不仅仅是Skylake),根据
2013年博客文章的测量结果,因此是您的Broadwell i5-5257U最可能的解释。看来
所有英特尔Sandybridge系列CPU都会发生这种效应。
没有更多有关您的测试硬件、编译器版本(或内部循环的汇编源代码)以及两个版本的绝对和/或相对性能数字,这是我最好的低成本猜测解释。在我的Skylake系统上进行基准测试/分析
gcc -O0
并不足够有趣,无法亲自尝试。下次请包含时间数字。
对于所有不属于循环相关依赖链的工作,存储/重新加载的延迟并不重要,只有吞吐量很重要。现代乱序CPU中的存储队列有效地提供了内存重命名,消除了写后写和写后读冲突,可以重用相同的堆栈内存来写入然后在其他地方读取和写入p
。(有关特定的内存冲突,请参见https://en.wikipedia.org/wiki/Memory_disambiguation#Avoiding_WAR_and_WAW_dependencies,有关延迟与吞吐量以及重用相同寄存器/寄存器重命名,请参见this Q&A)
内部循环的多个迭代可以同时进行,因为内存序缓冲区(MOB)会跟踪每个加载需要从哪个存储中获取数据,而不需要先前的相同位置的存储提交到L1D并离开存储队列。(有关CPU微架构内部的更多信息,请参见英特尔的优化手册和Agner Fog的微架构PDF。MOB是存储缓冲区和加载缓冲区的组合)
这是否意味着添加无用语句会加速实际程序?(启用优化)
一般来说,不会。编译器会将循环变量保存在最内层的寄存器中。并且启用优化后,无用语句实际上会被优化掉。
为gcc -O0
调整源代码是没有用的。请使用-O3
进行测试,或者使用您的项目默认构建脚本提供的其他选项。
此外,存储转发加速是针对英特尔Sandybridge系列的特定优化,除非其他微架构也具有类似的存储转发延迟效果,否则您在其他微架构上如Ryzen上看不到此优化效果。
存储转发延迟在实际(经过优化的)编译器输出中可能会成为问题,特别是如果您没有使用链接时优化(LTO)来让微小函数内联,尤其是那些通过引用传递或返回任何内容的函数(因此必须通过内存而不是寄存器)。缓解这个问题可能需要一些hack,比如如果你真的想在Intel CPU上解决它,可以使用volatile,但可能会在其他CPU上使情况变得更糟。请参见
评论中的讨论。
k
的循环依赖。如果你使用的是Skylake处理器,那么当相关的配对操作之间有更多操作(包括其他存储/加载操作)时,存储/重载延迟实际上可以更低(更好)。 - Peter Cordes