C++ 为什么for循环体中的一个基本算术运算比两个算术运算执行得慢?

C++ 为什么for循环体中的一个基本算术运算比两个算术运算执行得慢?,c++,performance,assembly,cpu-architecture,google-benchmark,C++,Performance,Assembly,Cpu Architecture,Google Benchmark,当我尝试测量算术运算的执行时间时,我遇到了非常奇怪的行为。包含for循环且循环体中有一个算术运算的代码块执行速度始终低于相同的代码块,但for循环体中有两个算术运算。以下是我最后测试的代码: #include <iostream> #include <chrono> #define NUM_ITERATIONS 100000000 int main() { // Block 1: one operation in loop body {

当我尝试测量算术运算的执行时间时,我遇到了非常奇怪的行为。包含
for
循环且循环体中有一个算术运算的代码块执行速度始终低于相同的代码块,但
for
循环体中有两个算术运算。以下是我最后测试的代码:

#include <iostream>
#include <chrono>

#define NUM_ITERATIONS 100000000

int main()
{
    // Block 1: one operation in loop body
    {
        int64_t x = 0, y = 0;
        auto start = std::chrono::high_resolution_clock::now();

        for (long i = 0; i < NUM_ITERATIONS; i++) {x+=31;}

        auto end = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> diff = end-start;
        std::cout << diff.count() << " seconds. x,y = " << x << "," << y << std::endl;
    }

    // Block 2: two operations in loop body
    {
        int64_t x = 0, y = 0;
        auto start = std::chrono::high_resolution_clock::now();

        for (long i = 0; i < NUM_ITERATIONS; i++) {x+=17; y-=37;}

        auto end = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> diff = end-start;
        std::cout << diff.count() << " seconds. x,y = " << x << "," << y << std::endl;
    }

    return 0;
}
#包括
#包括
#定义NUM_迭代100000000
int main()
{
//块1:循环体中的一个操作
{
int64_t x=0,y=0;
自动启动=标准::时钟::高分辨率时钟::现在();
对于(长i=0;istd::coutETA:这是一个猜测,彼得·科尔德斯(Peter Cordes)就其不正确的原因进行了很好的论证。请投票表决彼得的答案

我把答案留在这里是因为有些人发现这些信息很有用。虽然这不能正确解释OP中的行为,但它突出了一些问题,这些问题使得在现代处理器上测量特定指令的速度变得不可行(而且毫无意义)


有根据的猜测:

这是流水线、关闭内核部分电源和

现代处理器采用流水线方式,使多条指令可以同时执行。这是可能的,因为处理器实际上是在微操作上工作的,而不是我们通常认为是机器语言的汇编级指令。处理器“调度”通过将微操作分派到芯片的不同部分,同时跟踪指令之间的依赖关系

假设运行代码的核心有两个算术/逻辑单元(ALU)。一条反复重复的算术指令只需要一个ALU。使用两个ALU没有帮助,因为下一个操作取决于当前ALU的完成情况,所以第二个ALU只能等待

但是在您的两个表达式测试中,表达式是独立的。要计算
y
的下一个值,您不必等待
x
上的当前操作完成。现在,由于省电功能,第二个ALU可能会首先断电。内核可能会运行几次迭代,然后才意识到它可以使用第二个ALU。此时,它可以为第二个ALU通电,两个表达式循环的大部分运行速度将与一个表达式循环的运行速度一样快。因此,您可能希望两个示例所用的时间大致相同

最后,许多现代处理器使用动态频率缩放。当处理器检测到它运行不努力时,它实际上会稍微降低时钟以节省电源。但是当它被大量使用时(并且芯片的当前温度允许),它可能会将实际时钟速度提高到其额定速度

我假设这是通过启发式实现的。在第二个ALU断电的情况下,启发式可能会决定不值得增加时钟。在两个ALU通电并以最高速度运行的情况下,它可能会决定增加时钟。因此,两个表达式的情况下,应该已经与一个表达式的速度一样快在这种情况下,它实际上以更高的平均时钟频率运行,使它能够在略短的时间内完成两倍的工作


根据您的数字,差异约为14%。我的Windows机器在3.75 GHz左右空闲,如果我在Visual Studio中构建解决方案来稍微推动一下,时钟将上升到4.25 GHz左右(查看任务管理器中的“性能”选项卡).这是13%的时钟速度差异,因此我们的估计是正确的。

@PeterCordes在许多假设中证明了这个答案是错误的,但作为对这个问题的一些盲目研究尝试,它仍然是有用的

我设置了一些快速基准测试,认为它可能与代码内存对齐有关,这真是一个疯狂的想法

但似乎@Adrian McCarthy在动态频率缩放方面做得对

无论如何,基准测试告诉我们,插入一些NOP可能会有助于解决这个问题,在块1中x+=31之后插入15个NOP将导致与块2几乎相同的性能。单指令循环体中插入15个NOP如何提高性能,真是令人惊讶

我也尝试过,最聪明的编译器可能会扔掉一些插入这样NOP的代码内存,但事实似乎并非如此。

编辑:多亏了@PeterCordes,我们清楚地看到优化从来没有像上面基准测试中预期的那样有效(因为全局变量需要添加访问内存的指令),新的基准测试清楚地表明,对于堆栈变量,块1和块2的性能是相同的。但是NOPs仍然可以帮助单线程应用程序使用循环访问全局变量,在这种情况下,您可能不应该使用全局变量,而只需在循环后将全局变量分配给局部变量


编辑2:由于快速基准宏使变量访问不稳定,从而阻止了重要的优化,因此实际上优化从未起作用。加载变量一次是合乎逻辑的,因为我们只是在循环中修改它,因此它是不稳定的或禁用的优化。因此,这个答案基本上是错误的,但至少它显示了NOP可以加速未优化的代码执行,如果它在现实世界中有任何意义(有更好的方法比如Buffter计数器)。< /P> < P>我将代码分解成C++和汇编。我只想测试循环,所以我没有返回总和。。我在Windows上运行,调用约定是
rcx,rdx,r8,r9,
循环计数在
rcx
中。代码正在添加imm
        .code
        public  test1
        align   16
test1   proc
        sub     rsp,16
        mov     qword ptr[rsp+0],0
        mov     qword ptr[rsp+8],0
tst10:  add     qword ptr[rsp+8],17
        dec     rcx
        jnz     tst10
        add     rsp,16
        ret     
test1   endp

        public  test2
        align 16
test2   proc
        sub     rsp,16
        mov     qword ptr[rsp+0],0
        mov     qword ptr[rsp+8],0
tst20:  add     qword ptr[rsp+0],17
        add     qword ptr[rsp+8],-37
        dec     rcx
        jnz     tst20
        add     rsp,16
        ret     
test2   endp

        end
        .code
        public  test1
        align   16
test1   proc
        xor     rdx,rdx
        xor     r8,r8
        xor     r9,r9
        xor     r10,r10
        xor     r11,r11
tst10:  add     rdx,17
        dec     rcx
        jnz     tst10
        ret     
test1   endp

        public  test2
        align 16
test2   proc
        xor     rdx,rdx
        xor     r8,r8
        xor     r9,r9
        xor     r10,r10
        xor     r11,r11
tst20:  add     rdx,17
        add     r8,-37
        dec     rcx
        jnz     tst20
        ret     
test2   endp

        public  test3
        align 16
test3   proc
        xor     rdx,rdx
        xor     r8,r8
        xor     r9,r9
        xor     r10,r10
        xor     r11,r11
tst30:  add     rdx,17
        add     r8,-37
        add     r9,47
        dec     rcx
        jnz     tst30
        ret     
test3   endp

        public  test4
        align 16
test4   proc
        xor     rdx,rdx
        xor     r8,r8
        xor     r9,r9
        xor     r10,r10
        xor     r11,r11
tst40:  add     rdx,17
        add     r8,-37
        add     r9,47
        add     r10,-17
        dec     rcx
        jnz     tst40
        ret     
test4   endp

        end
#define NUM_ITERATIONS 1000000000ll
#define X_INC 17
#define Y_INC -31
for (long i = 0; i < NUM_ITERATIONS; i++) { x+=X_INC; }
    mov     QWORD PTR [rbp-32], 0
.L13:
    cmp     QWORD PTR [rbp-32], 999999999
    jg      .L12
    add     QWORD PTR [rbp-24], 17
    add     QWORD PTR [rbp-32], 1
    jmp     .L13
.L12:
for (long i = 0; i < NUM_ITERATIONS; i++) {x+=X_INC; y+=Y_INC;}
    mov     QWORD PTR [rbp-80], 0
.L21:
    cmp     QWORD PTR [rbp-80], 999999999
    jg      .L20
    add     QWORD PTR [rbp-64], 17
    sub     QWORD PTR [rbp-72], 31
    add     QWORD PTR [rbp-80], 1
    jmp     .L21
.L20:
while (x < (X_INC * NUM_ITERATIONS)) { x+=X_INC; }
.L15:
    movabs  rax, 16999999999
    cmp     QWORD PTR [rbp-40], rax
    jg      .L14
    add     QWORD PTR [rbp-40], 17
    jmp     .L15
.L14:
register long i;
for (i = 0; i < NUM_ITERATIONS; i++) { x+=X_INC; }
    mov     ebx, 0
.L17:
    cmp     rbx, 999999999
    jg      .L16
    add     QWORD PTR [rbp-48], 17
    add     rbx, 1
    jmp     .L17
.L16:
orig_main:
   ...
        call    std::chrono::_V2::system_clock::now()       # demangled C++ symbol name
        mov     rbp, rax                                    # save the return value = start
        call    std::chrono::_V2::system_clock::now()
        # end in RAX
#include <benchmark/benchmark.h>

static void TargetFunc(benchmark::State& state) {
   uint64_t x2 = 0, y2 = 0;
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    benchmark::DoNotOptimize(x2 += 31);
    benchmark::DoNotOptimize(y2 += 31);
  }
}
// Register the function as a benchmark
BENCHMARK(TargetFunc);
# just the main loop, from gcc10.1 -O3 
.L7:                         # do{
        add     rax, 31        # x2 += 31
        add     rdx, 31        # y2 += 31
        sub     rbx, 1
        jne     .L7          # }while(--count != 0)
static void TargetFunc(benchmark::State& state) {
   uint64_t x2 = 0, y2 = 0;
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
      x2 += 16;
      y2 += 17;
    asm volatile("" : "+r"(x2), "+r"(y2));
  }
}