用于循环性能差异和编译器优化

时间:2014-08-29 21:18:42

标签: c++ performance gcc


我选择了David的答案,因为他是唯一一个在没有优化标志的情况下解决for-loops差异的人。其他答案说明了在设置优化标志时会发生什么。


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


当我偶然发现以下情况时,我正在试验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 output --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 output --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 output --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的假设。在没有优化器标志的情况下编译这个代码仍然让我在B中大约2.2秒,在A中大约2.8秒。所以我认为它不是缓存的东西。只是强化我在第一种情况下谈论优化(没有标志的那种),我只是想知道是什么让秒循环运行得比第一种更快。

7 个答案:

答案 0 :(得分:10)

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

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

#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的额外调用。之后,我尝试了三次运行程序。对于这三次运行,我判断结果是平局的(版本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 (代价是稍微长一些,可读性稍差的代码)。

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


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

答案 1 :(得分:6)

检查装配输出真的是照亮这些东西的唯一方法。

编译器优化会做很多事情,包括不严格符合标准的事情&#34; (但据我所知,-O1-O2不是这种情况) - 例如检查,-Ofast切换。

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

答案 2 :(得分:5)

-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

没有调用2个函数,因此没有比较结果。

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

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

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

现在已知两个循环仅包含编译时值。它们完全相同:

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

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

重写高斯的formel s = n *(n + 1)

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

loops = 1000000111-111 = 1E9

一半,因为我们得到了所需的双倍

1000000221 * 1E9 / 2 = 500000110500000000

这是查找500000110500000000

的结果

现在它的结果是编译时常量,它可以将它与想要的结果进行比较,并注意它总是是真的,所以它可以删除它。

指出的时间是PC上system_clock的最短时间。

-O0

-O0的时序更加困难,很可能是函数和跳转缺失对齐的伪像,μops缓存和loopbuffer都喜欢32字节的对齐。如果添加一些

,可以测试一下
asm("nop");
在A的循环前面,2-3可能会成功。 Storeforwards也喜欢他们的价值观自然对齐。

答案 3 :(得分:2)

(这不是一个答案,但确实包含更多数据,包括与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观察到这两个例程的运行速度更快或更慢,具体取决于执行它们的顺序。 我无法重现该结果。对我而言,无论Turbo Boost或时钟速度设置如何,superCalculationB始终比superCalculationA快25-30%。这包括以任意顺序多次运行它们。例如,在2.0 GHz时superCalculationA始终需要超过4500毫秒,而superCalculationB始终需要不到3600毫秒。

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

答案 4 :(得分:2)

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

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

湾确切的执行速度,特别是对于像你这样的紧密循环,取决于指令在内存中的对齐方式。如果一些处理器完全包含在一个缓存行而不是两个缓存行中,或者在两个缓存行而不是三个缓存行中,则它们可以更快地运行循环。一些编译器会添加nop指令来对齐缓存行上的循环以针对此进行优化,大多数情况下不会。很可能其中一个循环通过纯粹的运气更好地对齐,因此运行得更快。

℃。确切的执行速度可能取决于分派指令的确切顺序。由于代码中可能与处理器相关的细微差别,略有不同的代码可能以不同的速度运行,并且无论如何编译器可能难以考虑。

d。有证据表明英特尔处理器可能存在人为短路的问题,这可能仅在人工基准测试中发生。你的代码非常接近“人为”。在其他线程中已经讨论过非常短的循环意外运行缓慢的情况,并且添加指令使它们运行得更快。

答案 5 :(得分:2)

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


-O0情况下的性能差异似乎是由于处理器流水线操作。

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

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<end是通过从注册中读取i来执行的,而在superCalculationB()中,比较i<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<end还是end>i

答案 6 :(得分:0)

第一个问题的答案:

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

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;
}