【问题标题】:For loop performance difference, and compiler optimization对于循环性能差异,以及编译器优化
【发布时间】:2014-08-29 21:18:41
【问题描述】:

我选择了 David 的答案,因为他是唯一一个在没有优化标志的情况下针对 for 循环中的差异提出解决方案的人。其他答案演示了设置优化标志时会发生什么。


Jerry Coffin 的回答解释了为本示例设置优化标志时发生的情况。仍然没有答案的是为什么 superCalculationA 比 superCalculationB 运行得慢,当 B 为每次迭代执行一个额外的内存引用和一个加法时。 Nemo 的帖子显示了汇编器的输出。根据 Matteo Italia 的建议,我在运行 Ubuntu 12.04 64 位的 PC 上使用 -S 标志确认了此编译,2.9GHz Sandy Bridge (i5-2310)。


当我偶然发现以下案例时,我正在试验 for 循环的性能。

我有以下代码以两种不同的方式进行相同的计算。

#include <cstdint>
#include <chrono>
#include <cstdio>

using std::uint64_t;

uint64_t superCalculationA(int init, int end)
{
    uint64_t total = 0;
    for (int i = init; i < end; i++)
        total += i;
    return total;
}

uint64_t superCalculationB(int init, int todo)
{
    uint64_t total = 0;
    for (int i = init; i < init + todo; i++)
        total += i;
    return total;
}

int main()
{
    const uint64_t answer = 500000110500000000;

    std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
    double elapsed;

    std::printf("=====================================================\n");

    start = std::chrono::high_resolution_clock::now();
    uint64_t ret1 = superCalculationA(111, 1000000111);
    end = std::chrono::high_resolution_clock::now();
    elapsed = (end - start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    start = std::chrono::high_resolution_clock::now();
    uint64_t ret2 = superCalculationB(111, 1000000000);
    end = std::chrono::high_resolution_clock::now();
    elapsed = (end - start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    if (ret1 == answer)
    {
        std::printf("The first method, i.e. superCalculationA, succeeded.\n");
    }
    if (ret2 == answer)
    {
        std::printf("The second method, i.e. superCalculationB, succeeded.\n");
    }

    std::printf("=====================================================\n");

    return 0;
}

编译这段代码

g++ main.cpp -o 输出 --std=c++11

导致以下结果:

=====================================================
Elapsed time: 2.859 s | 2859.441 ms | 2859440.968 us
Elapsed time: 2.204 s | 2204.059 ms | 2204059.262 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

我的第一个问题是:为什么第二个循环的运行速度比第一个快 23%?

另一方面,如果我用

编译代码

g++ main.cpp -o 输出 --std=c++11 -O1

结果进步了很多,

=====================================================
Elapsed time: 0.318 s | 317.773 ms | 317773.142 us
Elapsed time: 0.314 s | 314.429 ms | 314429.393 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

时间差几乎消失了。

但是当我设置 -O2 标志时我简直不敢相信自己的眼睛,

g++ main.cpp -o 输出 --std=c++11 -O2

得到了这个:

=====================================================
Elapsed time: 0.000 s | 0.000 ms | 0.328 us
Elapsed time: 0.000 s | 0.000 ms | 0.208 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

那么,我的第二个问题是:当我设置 -O1 和 -O2 标志导致这种巨大的性能提升时,编译器在做什么?

我检查了Optimized Option - Using the GNU Compiler Collection (GCC),但这并没有说明问题。


顺便说一句,我正在用 g++ (GCC) 4.9.1 编译这段代码。


编辑以确认 Basile Starynkevitch 的假设

我编辑了代码,现在main 看起来像这样:

int main(int argc, char **argv)
{
    int start = atoi(argv[1]);
    int end   = atoi(argv[2]);
    int delta = end - start + 1;

    std::chrono::time_point<std::chrono::high_resolution_clock> t_start, t_end;
    double elapsed;

    std::printf("=====================================================\n");

    t_start = std::chrono::high_resolution_clock::now();
    uint64_t ret1 = superCalculationB(start, delta);
    t_end = std::chrono::high_resolution_clock::now();
    elapsed = (t_end - t_start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    t_start = std::chrono::high_resolution_clock::now();
    uint64_t ret2 = superCalculationA(start, end);
    t_end = std::chrono::high_resolution_clock::now();
    elapsed = (t_end - t_start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    std::printf("Results were %s\n", (ret1 == ret2) ? "the same!" : "different!");
    std::printf("=====================================================\n");

    return 0;
}

这些修改确实增加了-O1-O2 的计算时间。现在两者都给了我大约 620 毫秒。 这证明 -O2 确实在编译时进行了一些计算

我仍然不明白这些标志在提高性能方面做了什么,-Ofast 做得更好,大约 320 毫秒。

还请注意,我更改了调用函数 A 和 B 的顺序来测试 Jerry Coffin 的假设。在没有优化器标志的情况下编译这段代码仍然给我大约 2.2 秒的 B 和 2.8 秒的 A。所以我认为这不是缓存的事情。只是强调我不是谈论第一种情况下的优化(没有标志的情况),我只是想知道是什么让秒循环比第一种运行得更快。

【问题讨论】:

  • 运行 g++-S 并检查程序集。
  • 如果没有开启优化(你的第一种情况),比较时间是没有意义的,因为生成的代码几乎是直接将你的代码翻译成汇编。通过优化,编译器几乎可以肯定在这种情况下完全消除您的循环。
  • 我猜想-O2 GCC 在编译时进行大部分计算。 SuperCalculationA & SuperCalculationB 的参数应该是可变的,例如通过程序参数给出(例如,main 中的 int init = atoi(argv[1]); int end = atoi(argv[2]);
  • 我必须纠正自己:查看-O0 的程序集并不能说明问题。发出的程序集显然是一个幼稚的 C-> 程序集翻译,但是,尽管在非常相似的代码中做了 more 事情(并访问堆栈上的另一个位置),但事实证明 superCalculationB 更快(由探查器确认)。结果甚至在for 循环中重复两次计算。
  • @jcmonteiro 感谢您选择我的答案。我做了更多的功课,现在认为我有一个更可靠的解释,没有任何谜团。请查看我修改后的答案。

标签: c++ performance gcc


【解决方案1】:

我的直接猜测是第二个更快,不是因为你对循环所做的更改,而是因为它是第二个,所以缓存在运行时已经准备好。

为了验证理论,我重新安排了您的代码以颠倒调用这两个计算的顺序:

#include <cstdint>
#include <chrono>
#include <cstdio>

using std::uint64_t;

uint64_t superCalculationA(int init, int end)
{
    uint64_t total = 0;
    for (int i = init; i < end; i++)
        total += i;
    return total;
}

uint64_t superCalculationB(int init, int todo)
{
    uint64_t total = 0;
    for (int i = init; i < init + todo; i++)
        total += i;
    return total;
}

int main()
{
    const uint64_t answer = 500000110500000000;

    std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
    double elapsed;

    std::printf("=====================================================\n");

    start = std::chrono::high_resolution_clock::now();
    uint64_t ret2 = superCalculationB(111, 1000000000);
    end = std::chrono::high_resolution_clock::now();
    elapsed = (end - start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    start = std::chrono::high_resolution_clock::now();
    uint64_t ret1 = superCalculationA(111, 1000000111);
    end = std::chrono::high_resolution_clock::now();
    elapsed = (end - start).count() * ((double) std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution_clock::period::den);
    std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);

    if (ret1 == answer)
    {
        std::printf("The first method, i.e. superCalculationA, succeeded.\n");
    }
    if (ret2 == answer)
    {
        std::printf("The second method, i.e. superCalculationB, succeeded.\n");
    }

    std::printf("=====================================================\n");

    return 0;
}

我得到的结果是:

=====================================================
Elapsed time: 0.286 s | 286.000 ms | 286000.000 us
Elapsed time: 0.271 s | 271.000 ms | 271000.000 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

所以,当版本 A 首先运行时,它会变慢。版本 B 第一次运行时,速度较慢。

为了确认,在对版本 A 或 B 进行计时之前,我添加了一个额外的调用 superCalculationB。之后,我尝试运行该程序 3 次。对于这三个运行,我会判断结果是平局(版本 A 快了一次,版本 B 快了两次,但都没有可靠地获胜,也没有足够大的优势来有意义)。

这并不能证明它实际上是这样的缓存情况,但确实有力地表明这是函数被调用的顺序问题,而不是代码本身的差异。

就编译器为使代码更快所做的工作而言:它所做的主要工作是展开循环的几次迭代。如果我们手动展开几次迭代,我们可以获得几乎相同的效果:

uint64_t superCalculationC(int init, int end)
{
    int f_end = end - ((end - init) & 7);

    int i;
    uint64_t total = 0;
    for (i = init; i < f_end; i += 8) {
        total += i;
        total += i + 1;
        total += i + 2;
        total += i + 3;
        total += i + 4;
        total += i + 5;
        total += i + 6;
        total += i + 7;
    }

    for (; i < end; i++)
        total += i;

    return total;
}

它有一个特性,有些人可能会觉得很奇怪:使用 -O2 编译时实际上比使用 -O3 编译时更快。使用 -O2 编译时,它也比使用 -O3 编译时的其他两个快约五倍。

与编译器的循环展开相比,速度提升约 5 倍的主要原因是我们展开循环的方式与编译器有所不同(并且更智能,IMO)。我们计算f_end 来告诉我们展开的循环应该执行多少次。我们执行这些迭代,然后我们在最后执行一个单独的循环来“清理”任何奇怪的迭代。

编译器生成的代码大致相当于这样的代码:

for (i = init; i < end; i += 8) {
    total += i;
    if (i + 1 >= end) break;
    total += i + 1;
    if (i + 2 >= end) break;
    total += i + 2;
    // ...
}

虽然这比完全没有展开循环时快得多,但从主循环中消除这些额外检查并为任何奇数迭代执行单独的循环仍然要快得多。

鉴于这样一个微不足道的循环体被执行了如此多的次数,您还可以通过展开循环的更多迭代来进一步提高速度(使用 -O2 编译时)。展开 16 次迭代后,它的速度大约是上面展开 8 次迭代的代码的两倍:

uint64_t superCalculationC(int init, int end)
{
    int first_end = end - ((end - init) & 0xf);

    int i;
    uint64_t total = 0;
    for (i = init; i < first_end; i += 16) {
        total += i + 0;
        total += i + 1;
        total += i + 2;

        // code for `i+3` through `i+13` goes here

        total += i + 14;
        total += i + 15;
    }

    for (; i < end; i++)
        total += i;

    return total;
}

我没有尝试探索展开这个特定循环的收益极限,但展开 32 次迭代几乎又使速度翻了一番。根据您使用的处理器,您可能通过展开 64 次迭代获得一些小的收益,但我猜我们开始接近极限 - 在某些时候,性能提升可能会平稳,然后(如果您展开更多迭代)可能会下降,很可能会急剧下降。

总结:使用-O3,编译器展开循环的多次迭代。这在这种情况下非常有效,主要是因为我们有 许多 次执行几乎最微不足道的可能循环体。手动展开循环比让编译器来做更有效——我们可以更智能地展开循环,而且我们可以简单地展开比编译器更多的迭代。额外的智能可以给我们带来大约 5:1 的改进,额外的迭代可以让我们提高 4:1 左右1(代价是代码更长,可读性略低)。

最后警告:与优化一样,您的里程可能会有所不同。编译器和/或处理器的差异意味着您得到的结果可能至少与我有所不同。在大多数情况下,我希望我的手动展开循环比其他两个快得多,但具体快多少可能会有所不同。


1. 但请注意,这是将带有 -O2 的手动展开循环与带有 -O3 的原始循环进行比较。当使用 -O3 编译时,手动展开的循环运行得更慢。

【讨论】:

  • 感谢您的回复,杰瑞。您是否使用一些优化器标志编译此代码?我仔细检查了我的代码,没有 -O0 标志的编译给了我这些结果,A 为 2.8,B 为 2.2,即使我更改了调用的顺序。
  • 是的,我使用 -O2 和 -O3 进行了尝试(以及 MS VC++,使用 -O2b2 -GL)。不同的总时间,但两个编译器的基本结果相同:哪个例程先运行,也运行得更慢)。
  • 使用这些标志编译时,它们确实给出了相同的结果。我的观点是,在没有它们的情况下编译时,例程 B 的运行速度比 A 快。尝试删除所有优化器标志。
  • 在禁用优化的情况下查看优化是人类可能进行的最无意义的练习。只是为了咧嘴笑,我确实用 -O2 和 -O3 进行了汇编。在这两种情况下,编译器都会为两个版本的代码创建相同代码。执行速度的任何差异纯粹是您如何进行计时的伪影。
  • 我认为你没有理解这个问题。我问是什么导致了禁用优化的时间差异以及优化做了什么来减少计算时间。
【解决方案2】:

检查汇编输出确实是阐明这些事情的唯一方法。

编译器优化会做很多事情,包括不严格“符合标准”的事情(尽管据我所知,-O1-O2 并非如此) - 例如检查,@ 987654325@开关。

我发现这很有帮助:http://gcc.godbolt.org/,以及您的演示代码here

【讨论】:

  • 谢谢你的回答,鲁桑。不错的网站,我以后一定会用的。就组装而言,我仍在学习,所以如果你能指出当我打开标志 -O1 和 -Ofast 时发生的事情,我将不胜感激。请参考我添加 argc 和 argv 的编辑 main 函数。
【解决方案3】:

-O2

解释-O2的结果很简单,看代码从godbolt改成-O2

main:
pushq   %rbx
movl    $.LC2, %edi
call    puts
call    std::chrono::_V2::system_clock::now()
movq    %rax, %rbx
call    std::chrono::_V2::system_clock::now()
pxor    %xmm0, %xmm0
subq    %rbx, %rax
movsd   .LC4(%rip), %xmm2
movl    $.LC6, %edi
movsd   .LC5(%rip), %xmm1
cvtsi2sdq   %rax, %xmm0
movl    $3, %eax
mulsd   .LC3(%rip), %xmm0
mulsd   %xmm0, %xmm2
mulsd   %xmm0, %xmm1
call    printf
call    std::chrono::_V2::system_clock::now()
movq    %rax, %rbx
call    std::chrono::_V2::system_clock::now()
pxor    %xmm0, %xmm0
subq    %rbx, %rax
movsd   .LC4(%rip), %xmm2
movl    $.LC6, %edi
movsd   .LC5(%rip), %xmm1
cvtsi2sdq   %rax, %xmm0
movl    $3, %eax
mulsd   .LC3(%rip), %xmm0
mulsd   %xmm0, %xmm2
mulsd   %xmm0, %xmm1
call    printf
movl    $.LC7, %edi
call    puts
movl    $.LC8, %edi
call    puts
movl    $.LC2, %edi
call    puts
xorl    %eax, %eax
popq    %rbx
ret

没有调用这两个函数,也没有比较结果。

现在为什么会这样?它当然是优化的力量,程序太简单了……

首先应用内联的力量,之后编译器可以看到所有参数实际上都是文字值(111、1000000111、1000000000、500000110500000000),因此是常量。

发现init + todo是一个循环不变量,并用end替换它们,将B循环之前的end定义为end = init + todo = 111 + 1000000000 = 1000000111

现在已知这两个循环都只包含编译时值。它们进一步完全相同:

uint64_t total = 0;
for (int i = 111; i < 1000000111; i++)
    total += i;
return total;

编译器看到它是一个求和,total 是累加器,它是一个等于 stride 1 的总和,所以编译器使最终循环展开,即 all,但它知道这种形式有总和

改写高斯公式 s=n*(n+1)

111+1000000110
110+1000000109
...
1000000109+110
1000000110+111=1000000221

循环 = 1000000111-111 = 1E9

一半,因为我们得到了寻找的两倍

1000000221 * 1E9 / 2 = 500000110500000000

这是寻找 500000110500000000 的结果

现在它的结果是一个编译时间常数,它可以将它与想要的结果进行比较,并注意它总是为真,因此它可以删除它。

注明的时间是您 PC 上 system_clock 的最短时间。

-O0

-O0 的计时更困难,很可能是函数和跳转缺少对齐的伪影,µops 缓存和循环缓冲区都喜欢 32 字节的对齐。如果你添加一些,你可以测试一下

asm("nop");

在 A 的循环之前,2-3 可能会成功。 Storeforwards 也喜欢它们的值自然一致。

【讨论】:

  • 这应该是 -O2 案例的最佳答案。
【解决方案4】:

编辑:在了解了更多关于处理器流水线中的依赖关系之后,我修改了我的答案,删除了一些不必要的细节,并提供了对减速的更具体的解释。


看来 -O0 情况下的性能差异是由于处理器流水线造成的。

首先,从 Nemo 的回答中复制的程序集(用于 -O0 构建),内联了一些我自己的 cmets:

superCalculationA(int, int):
    pushq   %rbp
    movq    %rsp, %rbp
    movl    %edi, -20(%rbp)    # init
    movl    %esi, -24(%rbp)    # end
    movq    $0, -8(%rbp)       # total = 0
    movl    -20(%rbp), %eax    # copy init to register rax
    movl    %eax, -12(%rbp)    # i = [rax]
    jmp .L7
.L8:
    movl    -12(%rbp), %eax    # copy i to register rax
    cltq
    addq    %rax, -8(%rbp)     # total += [rax]
    addl    $1, -12(%rbp)      # i++
.L7:
    movl    -12(%rbp), %eax    # copy i to register rax
    cmpl    -24(%rbp), %eax    # [rax] < end
    jl  .L8
    movq    -8(%rbp), %rax
    popq    %rbp
    ret

superCalculationB(int, int):
    pushq   %rbp
    movq    %rsp, %rbp
    movl    %edi, -20(%rbp)    # init
    movl    %esi, -24(%rbp)    # todo
    movq    $0, -8(%rbp)       # total = 0
    movl    -20(%rbp), %eax    # copy init to register rax
    movl    %eax, -12(%rbp)    # i = [rax]
    jmp .L11
.L12:
    movl    -12(%rbp), %eax    # copy i to register rax
    cltq
    addq    %rax, -8(%rbp)     # total += [rax]
    addl    $1, -12(%rbp)      # i++
.L11:
    movl    -20(%rbp), %edx    # copy init to register rdx
    movl    -24(%rbp), %eax    # copy todo to register rax
    addl    %edx, %eax         # [rax] += [rdx]  (so [rax] = init+todo)
    cmpl    -12(%rbp), %eax    # i < [rax]
    jg  .L12
    movq    -8(%rbp), %rax
    popq    %rbp
    ret

在这两个函数中,堆栈布局如下所示:

Addr Content

24   end/todo
20   init
16   <empty>
12   i
08   total
04   
00   <base pointer>

(注意total 是一个 64 位的 int,因此占用了两个 4 字节的插槽。)

这些是superCalculationA()的关键行:

    addl    $1, -12(%rbp)      # i++
.L7:
    movl    -12(%rbp), %eax    # copy i to register rax
    cmpl    -24(%rbp), %eax    # [rax] < end

堆栈地址-12(%rbp)(保存i的值)在addl指令中写入,然后在下一条指令中立即读取。在写入完成之前,读取指令无法开始。这表示管道中的一个块,导致superCalculationA()superCalculationB() 慢。

您可能会好奇为什么superCalculationB() 没有这个相同的管道块。它实际上只是 gcc 如何在 -O0 中编译代码的产物,并不代表任何从根本上有趣的东西。基本上,在superCalculationA() 中,比较i&lt;end 是通过从寄存器 中读取i 来执行的,而在superCalculationB() 中,比较i&lt;init+todo 是通过从寄存器中读取i 来执行的堆栈

为了证明这只是一个神器,让我们替换

for (int i = init; i < end; i++)

for (int i = init; end > i; i++)

superCalculateA()。然后生成的程序集看起来相同,只是对关键行进行了以下更改:

    addl    $1, -12(%rbp)      # i++
.L7:
    movl    -24(%rbp), %eax    # copy end to register rax
    cmpl    -12(%rbp), %eax    # i < [rax]

现在从堆栈中读取i,并且管道块消失了。以下是进行此更改后的性能数据:

=====================================================
Elapsed time: 2.296 s | 2295.812 ms | 2295812.000 us
Elapsed time: 2.368 s | 2367.634 ms | 2367634.000 us
The first method, i.e. superCalculationA, succeeded.
The second method, i.e. superCalculationB, succeeded.
=====================================================

应该注意,这实际上是一个玩具示例,因为我们使用 -O0 进行编译。在现实世界中,我们使用 -O2 或 -O3 进行编译。在这种情况下,编译器会以这样一种方式对指令进行排序,以尽量减少流水线块,我们不必担心是写i&lt;end还是end&gt;i

【讨论】:

    【解决方案5】:

    (这并不完全是一个答案,但它确实包含更多数据,包括一些与 Jerry Coffin 的冲突的数据。)

    有趣的问题是,为什么未优化的例程执行如此不同且违反直觉。 -O2-O3 的案例解释起来比较简单,其他人也这样做了。

    为了完整性,here is the assembly(感谢@Rutan Kax)为 GCC 4.9.1 生成的superCalculationAsuperCalculationB

    superCalculationA(int, int):
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %edi, -20(%rbp)
        movl    %esi, -24(%rbp)
        movq    $0, -8(%rbp)
        movl    -20(%rbp), %eax
        movl    %eax, -12(%rbp)
        jmp .L7
    .L8:
        movl    -12(%rbp), %eax
        cltq
        addq    %rax, -8(%rbp)
        addl    $1, -12(%rbp)
    .L7:
        movl    -12(%rbp), %eax
        cmpl    -24(%rbp), %eax
        jl  .L8
        movq    -8(%rbp), %rax
        popq    %rbp
        ret
    
    superCalculationB(int, int):
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %edi, -20(%rbp)
        movl    %esi, -24(%rbp)
        movq    $0, -8(%rbp)
        movl    -20(%rbp), %eax
        movl    %eax, -12(%rbp)
        jmp .L11
    .L12:
        movl    -12(%rbp), %eax
        cltq
        addq    %rax, -8(%rbp)
        addl    $1, -12(%rbp)
    .L11:
        movl    -20(%rbp), %edx
        movl    -24(%rbp), %eax
        addl    %edx, %eax
        cmpl    -12(%rbp), %eax
        jg  .L12
        movq    -8(%rbp), %rax
        popq    %rbp
        ret
    

    在我看来,B 确实在做更多的工作。

    我的测试平台是运行 Red Hat Enterprise 6 Update 3 的 2.9GHz Sandy Bridge EP 处理器 (E5-2690)。我的编译器是 GCC 4.9.1,并生成上述程序集。

    为了确保 Turbo Boost 和相关的 CPU 调频技术不会干扰测量,我运行了:

    pkill cpuspeed # if you have it running
    grep MHz /proc/cpuinfo # to see where you start
    modprobe acpi_cpufreq # if you do not have it loaded
    cd /sys/devices/system/cpu 
    for cpuN in cpu[0-9]* ; do
        echo userspace > $cpuN/cpufreq/scaling_governor
        echo 2000000 > $cpuN/cpufreq/scaling_setspeed
    done
    grep MHz /proc/cpuinfo # to see if it worked
    

    这会将 CPU 频率固定为 2.0 GHz 并禁用 Turbo Boost。

    Jerry 观察到这两个例程的运行速度取决于他执行它们的顺序。 我无法重现该结果。 对我而言,superCalculationB 的运行速度始终比superCalculationA 快 25-30%,无论 Turbo Boost 或时钟速度设置如何。这包括以任意顺序多次运行它们。例如,在 2.0GHz 时,superCalculationA 始终需要略高于 4500 毫秒,superCalculationB 始终需要略低于 3600 毫秒。

    我还没有看到任何理论可以开始解释这一点。

    【讨论】:

    • 感谢您的回复,尼莫。我也在使用 GCC 4.9.1,它产生的程序集与你的程序集相同。我在这个程序集输出中徘徊,看看我是否能找到发生了什么。对我来说,superCalculationB 似乎也在做更多的工作。
    • B 正在执行 A 没有的一次加载和一次添加。这给出了 6 个内存引用而不是 5 个,并且这比使用 -O2 (例如)慢得多的大部分原因是由于对内存的持续引用而不是在寄存器中保存值。很难看出多 20% 的内存引用(或 17%,取决于观点)如何导致 25% 的速度差异。
    • @JerryCoffin:B 也比 A 25%。因此,内存引用增加 20% 会导致速度增加 25%。存储转发确实意味着内存引用并不总是内存引用......但是,我仍然不知道这里发生了什么。
    【解决方案6】:

    处理器很复杂。执行时间取决于很多事情,其中​​很多是您无法控制的。只是几种可能性:

    一个。您的计算机可能没有恒定的时钟速度。可能是时钟速度通常设置得相当低,以避免浪费能源/电池寿命/产生过多的热量。当您的程序开始运行时,操作系统会发现需要电源并提高时钟速度。为了验证,改变调用的顺序——如果执行的第二个循环总是比第一个快,这可能就是原因。

    b.确切的执行速度,尤其是对于像您这样的紧密循环,取决于指令在内存中的对齐方式。如果循环完全包含在一个高速缓存行而不是两个高速缓存行中,或者在两个高速缓存行而不是三个高速缓存行中,则某些处理器可能会更快地运行一个循环。一些编译器会添加 nop 指令来对齐缓存行上的循环以对此进行优化,大多数编译器不会。很可能其中一个循环完全靠运气对齐得更好,因此运行得更快。

    c。确切的执行速度可能取决于分派指令的确切顺序。由于代码的细微差异可能取决于处理器,因此稍微不同的代码可能以不同的速度运行,并且无论如何编译器可能很难考虑。

    d。有一些证据表明,英特尔处理器可能存在人为短循环的问题,而这种问题只有在人工基准测试中才会发生。您的代码非常接近“人工”。在其他线程中讨论过一些案例,其中非常短的循环运行速度出乎意料地慢,并且添加指令使它们运行得更快。

    【讨论】:

    • 我增加了两个循环中的指令数量,A逐渐变得比B快。我认为你在“d”中提出的可能性。是答案。您能否提供有关此主题的更多信息,以便我接受您的回答?
    【解决方案7】:

    第一个问题的答案:

    1- 执行一次 for 循环后它变得更快,但我不确定是否只是根据我的实验结果进行评论。(实验 1 更改它们的名称(B->A,A->B)实验 2 运行一个函数在时间检查之前有 for 循环,实验 3 在时间检查之前启动一个 for 循环)

    2- 第一个程序应该运行得更快,原因是第二个函数在第一个函数执行 1 次操作时执行 2 次操作。

    我在这里留下更新的代码来解释我的答案。

    第二个问题的答案:

    我不确定,但我可能会想到两种方法,

    它可以以某种方式形式化你的函数并摆脱循环,因为区别 可以通过这种方式销毁(比如“return end-init”或“return todo”我不知道,我不确定)

    它有 -fauto_inc_dec 并且它可以产生影响,因为这些函数都是关于增量和减量的。

    我希望它可以帮助。

    #include <cstdint>
    #include <ctime>
    #include <cstdio>
    
    using std::uint64_t;
    
    uint64_t superCalculationA(int init, int end)
    {
        uint64_t total = 0;
        for (int i = init; i < end; i++)
            total += i;
        return total;
    }
    uint64_t superCalculationB(int init, int todo)
    {
        uint64_t total = 0;
        for (int i = init; i < init+todo; i++)
            total += i;
        return total;
    }
    int add(int a1,int a2){printf("multiple times added\n");return a1+a2;}
    uint64_t superCalculationC(int init, int todo)
    {
        uint64_t total = 0;
        for (int i = init; i < add(init , todo); i++)
            total += i;
        return total;
    }
    
    int main()
    {
        const uint64_t answer = 500000110500000000;
    
        std::clock_t start=clock();
        double elapsed;
    
        std::printf("=====================================================\n");
    
        superCalculationA(111, 1000000111);
    
        start = clock();
        uint64_t ret1 = superCalculationA(111, 1000000111);
        elapsed = ((std::clock()-start)*1.0/CLOCKS_PER_SEC);
        std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed,    1e+6*elapsed);
    
        start = clock();
        uint64_t ret2 = superCalculationB(111, 1000000000);
        elapsed = ((std::clock()-start)*1.0/CLOCKS_PER_SEC);
        std::printf("Elapsed time: %.3f s | %.3f ms | %.3f us\n", elapsed, 1e+3*elapsed, 1e+6*elapsed);
    
        if (ret1 == answer)
        {
            std::printf("The first method, i.e. superCalculationA, succeeded.\n");
        }
        if (ret2 == answer)
        {
            std::printf("The second method, i.e. superCalculationB, succeeded.\n");
        }
    
        std::printf("=====================================================\n");
    
        return 0;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-12-11
      • 1970-01-01
      • 2017-11-05
      • 2015-09-30
      • 1970-01-01
      • 2011-12-03
      • 1970-01-01
      • 2010-12-27
      相关资源
      最近更新 更多