Performance 在编译代码时,添加冗余分配可以加快代码的速度,而无需进行优化

Performance 在编译代码时,添加冗余分配可以加快代码的速度,而无需进行优化,performance,assembly,x86,cpu-architecture,micro-architecture,Performance,Assembly,X86,Cpu Architecture,Micro Architecture,我发现一个有趣的现象: #include<stdio.h> #include<time.h> int main() { int p, q; clock_t s,e; s=clock(); for(int i = 1; i < 1000; i++){ for(int j = 1; j < 1000; j++){ for(int k = 1; k < 1000; k++){

我发现一个有趣的现象:

#include<stdio.h>
#include<time.h>

int main() {
    int p, q;
    clock_t s,e;
    s=clock();
    for(int i = 1; i < 1000; i++){
        for(int j = 1; j < 1000; j++){
            for(int k = 1; k < 1000; k++){
                p = i + j * k;
                q = p;  //Removing this line can increase running time.
            }
        }
    }
    e = clock();
    double t = (double)(e - s) / CLOCKS_PER_SEC;
    printf("%lf\n", t);
    return 0;
}
那么,为什么有了这样的任务,程序运行得更快呢


这很有帮助。在AMD Phenom II X4 810和ARMv7处理器(BCM2835)上的测试显示了相反的结果,支持存储转发加速比是某些英特尔CPU特有的。
并驱使我重写这个问题

这个问题的核心是与处理器架构和组装相关的有趣现象。因此,我认为这可能值得讨论。

TL:DR:Sandybridge家庭商店转发如果不尝试“立即”重新加载,则延迟较低。。添加无用的代码可以加速调试模式循环,因为反优化代码中的循环带来的延迟瓶颈几乎总是会涉及。
行动放缓的其他例子如下:

这些都与优化代码无关。存储转发延迟上的瓶颈偶尔会发生,但给代码添加无用的复杂因素不会加快速度


您正在对调试生成进行基准测试。它们有不同于优化代码的瓶颈,而不是一致的减速。


但是很明显,一个版本的调试版本运行速度比另一个版本的调试版本慢有一个真正的原因。(假设您测量正确,并且不仅仅是CPU频率变化(turbo/节能)导致挂钟时间的差异。)

如果您想深入了解x86性能分析的详细信息,我们可以尝试解释为什么asm会以其最初的方式执行,以及为什么来自额外C语句(使用
-O0
编译为额外asm指令)的asm可以使其总体速度更快这将告诉我们一些有关asm性能影响的信息,但对优化C毫无帮助。

您没有显示整个内部循环,只有部分循环体,但是
gcc-O0
是。每个C语句都是独立于所有其他语句进行编译的,所有C变量都在每个语句的块之间溢出/重新加载。这允许您在单步执行时使用调试器更改变量,甚至跳转到函数中的另一行,使代码仍能工作。以这种方式编译的性能成本是灾难性的。例如,您的循环没有副作用(没有使用任何结果),因此整个三重嵌套循环可以并且将在实际构建中编译为零指令,运行速度将无限快。或者更现实地说,每次迭代运行1个周期,而不是~6个周期,即使没有优化或进行主要转换


瓶颈可能是循环对
k
的依赖性,它有一个存储/重新加载和一个
add
来增加。存储转发延迟通常为。因此,内部循环被限制为每~6个周期运行一次,即内存目标
add
的延迟

如果您使用的是Intel CPU,当重新加载无法立即执行时,存储/重新加载延迟实际上可以更低(更好)。在依赖对之间有更多独立的加载/存储可以解释您的情况。看

因此,随着循环中的工作越来越多,
addl$1,-12(%rbp)
可以在背靠背运行时维持每6个周期一次的吞吐量,这可能只会造成每4或5个周期一次迭代的瓶颈。

根据测量结果,这种影响显然发生在Sandybridge和Haswell(不仅仅是Skylake)身上,所以是的,这也是对Broadwell i5-5257U最可能的解释。似乎这种影响会发生在所有Intel Sandybridge系列CPU上


如果没有关于测试硬件、编译器版本(或内部循环的asm源代码)、以及这两个版本的绝对和/或相对性能数字的更多信息,这是我对解释的最佳猜测。在我的Skylake系统上进行基准测试/评测
gcc-O0
并没有足够的兴趣亲自尝试。下次,包括计时数字


对于不属于循环携带依赖链一部分的所有工作,存储/重新加载的延迟并不重要,只影响吞吐量。现代无序CPU中的存储队列确实有效地提供了内存重命名,避免了在写入
p
后在其他地方读取和写入时重复使用相同的堆栈内存。(有关内存危害的详细信息,以及延迟与吞吐量和重复使用同一寄存器/寄存器重命名的详细信息,请参阅)

内部循环的多次迭代可以同时进行,因为内存顺序缓冲区跟踪每个加载需要从哪个存储中获取数据,而不需要将以前的存储提交到同一位置以提交到L1D并从存储队列中退出。(有关CPU微体系结构内部的更多信息,请参阅英特尔的优化手册和Agner Fog的Microach PDF。)


这是否意味着添加无用语句将加快实际程序的速度?(已启用优化) 一般来说,不,它不。编译器将循环变量保存在最内层循环的寄存器中。而无用的语句实际上会在启用优化的情况下进行优化

gcc-O0
调整源代码是无用的。
使用
-O3
或默认构建脚本为项目使用的任何选项进行测量

此外,此存储转发加速是针对Intel Sandybridge系列的,除非它们也具有类似的存储转发延迟效应,否则在Ryzen等其他微体系结构上不会看到


存储转发延迟可能是真实(优化)编译器输出中的一个问题,特别是如果您没有这样做的话
movl    -44(%rbp), %eax
movl    %eax, -48(%rbp)