基本C/C++解释

3
我想检查计算机视觉算法的性能,于是我写了这个基本的代码片段来测试哪个循环更快。但是我对结果没有任何解释。通常我得到的结果显示双重循环比简单循环快3倍。如果我交换两个循环,我得到相同的结果,这意味着第二个循环总是被优化了...那么编译器做了什么样的优化呢?
抱歉,我知道这可能是一个愚蠢的问题...
ulong k = 0;
auto start = std::chrono::high_resolution_clock::now();
for( uint i = 0; i < 1000000; ++i )
{
    k++;
}
auto diff = std::chrono::high_resolution_clock::now() - start;
auto t1 = std::chrono::duration_cast<std::chrono::nanoseconds>(diff);

k = 0;
start = std::chrono::high_resolution_clock::now();
for( uint i = 0; i < 1000; ++i )
{
    for( uint j = 0; j < 1000; ++j )
    {
        k++;
    }
}
diff = std::chrono::high_resolution_clock::now() - start;
auto t2 = std::chrono::duration_cast<std::chrono::nanoseconds>(diff);

CL_PRINT( "Simple: ", t1.count() );
CL_PRINT( "Double: ", t2.count() );

如果我交换这两个循环,得到的结果是相同的,这意味着第二个循环总是被优化了...

请注意,CL_PRINT 只是用于调试目的的宏。 还要注意,我使用以下选项编译代码:-O3 -msse4.1


13
你是否阅读生成的代码以确保所有循环都完整地“存活”下来了?在-O3优化级别下,如果编译器发现变量k的值从未被使用,它可能会重构代码。 - unwind
4
“重组...显著”的说法,是“消除无用代码”这一说法的巧妙委婉说法。 - sehe
1
是的,这就是为什么实际上允许打印此代码的原因:for(;;); printf("hello"); - 永久循环没有副作用,因此编译器可以重新排序或完全删除它。尝试使您的k成为易失性变量,以便不允许这样做。 - ltjax
我这样做确实只用了几毫秒时间,这很正常,因为我在一个3Ghz的处理器上进行了三个操作。但是静态地看,第二个操作仍然更快... 我将使用不同选项生成的代码进行比较,以便更深入地了解为什么会出现这种情况。 - Athanase
3
这是一个合理的问题,我不明白为什么会有负评。编译器允许优化掉代码的唯一原因是它能够证明结果没有任何“可见效果”(经过的时间不算),而且两个循环在这个标准下是完全一致的。所以我怀疑答案是不合理的:你遇到了优化启发式表现愚蠢的地方。我怀疑特别是编译器没有“意识到”第一个循环后面的 k = 0; 意味着这个计算不需要了。 - j_random_hacker
显示剩余3条评论
1个回答

2
这里的答案是确切时间会有所不同。当我在我的电脑上运行这段代码时,第一次循环有时会出现1000,而其他时候第二次循环会出现1000。这只是计时器滴答声的“运气”。如果你有一个更准确的计时器,它可能会显示基于读取计时器所需时间或其他因素的差异。
$ ./a.out
k = 1000000
k = 1000000
Simple: 0
Double: 1000
$ ./a.out
k = 1000000
k = 1000000
Simple: 1000
Double: 0
$ ./a.out
k = 1000000
k = 1000000
Simple: 1000
Double: 0
$ ./a.out
k = 1000000
k = 1000000
Simple: 1000
Double: 0

很容易看出两个循环都已被优化:

main:
.LFB1474:
.cfi_startproc
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
pushq   %rbx
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
subq    $8, %rsp
.cfi_def_cfa_offset 32
call    _ZNSt6chrono12system_clock3nowEv
movq    %rax, %rbx
call    _ZNSt6chrono12system_clock3nowEv
movl    $.LC0, %esi
**subq  %rbx, %rax**
movl    $_ZSt4cout, %edi
imulq   $1000, %rax, %rbp
call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
**movl  $1000000, %esi**
movq    %rax, %rdi
call    _ZNSo9_M_insertImEERSoT_
movq    %rax, %rdi
call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
call    _ZNSt6chrono12system_clock3nowEv
movq    %rax, %rbx
call    _ZNSt6chrono12system_clock3nowEv
movl    $.LC0, %esi
**subq  %rbx, %rax**
movl    $_ZSt4cout, %edi
imulq   $1000, %rax, %rbx
call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
**movl  $1000000, %esi**
movq    %rax, %rdi
call    _ZNSo9_M_insertImEERSoT_
movq    %rax, %rdi
call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
movl    $.LC1, %esi
movl    $_ZSt4cout, %edi
call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movq    %rbp, %rsi
movq    %rax, %rdi
call    _ZNSo9_M_insertIlEERSoT_
movq    %rax, %rdi
call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
movl    $.LC2, %esi
movl    $_ZSt4cout, %edi
call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movq    %rbx, %rsi
movq    %rax, %rdi
call    _ZNSo9_M_insertIlEERSoT_
movq    %rax, %rdi
call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
addq    $8, %rsp
.cfi_def_cfa_offset 24
xorl    %eax, %eax
popq    %rbx
.cfi_def_cfa_offset 16
popq    %rbp
.cfi_def_cfa_offset 8
ret

您可以清楚地看到K的常量被插入到流中作为常量,并且在“before”和“after”的时间被记录之后进行减法运算,两者之间没有(太多)代码。 (有趣的部分用** ... **标记 - 当然,在代码中它不会变成粗体)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接