与其他类型的循环相比,编译器是否为do-while循环生成更好的代码?

Den*_*nis 89 c compiler-construction performance

zlib压缩库中有一个注释(在Chromium项目中用于许多其他项目),这意味着C中的do-while循环在大多数编译器上生成"更好"的代码.这是它出现的代码片段.

do {
} while (*(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         scan < strend);
/* The funny "do {}" generates better code on most compilers */
Run Code Online (Sandbox Code Playgroud)

https://code.google.com/p/chromium/codesearch#chromium/src/third_party/zlib/deflate.c&l=1225

有没有证据表明大多数(或任何)编译器会生成更好(例如更高效)的代码?

更新: 原作者之一Mark Adler在评论中给出了一些背景信息.

Mys*_*ial 108

首先:

do-while环是不一样的一个while-loop或for-loop.

  • whilefor循环可能根本不运行循环体.
  • 一个do-while循环总是在循环体至少一次-它跳过了初始条件检查.

这是合乎逻辑的差异.也就是说,不是每个人都严格遵守这一点.即使在保证它总是至少循环一次的情况下,使用whilefor循环也是很常见的.(特别是在带有foreach循环的语言中.)

因此,为了避免比较苹果和橙子,我将继续假设循环将始终至少运行一次.此外,我不再提及for循环,因为它们本质上是while循环,带有一些循环计数器的语法糖.

所以我会回答这个问题:

如果while保证循环至少循环一次,那么使用do-while循环是否有任何性能提升.


A do-while跳过第一个条件检查.因此,有一个较少的分支和一个较少的条件来评估.

如果检查条件很昂贵,并且您知道保证至少循环一次,那么do-while循环可能会更快.

虽然这被认为是最佳的微优化,但它是编译器不能总是这样做的:特别是当编译器无法证明循环将始终至少输入一次时.


换句话说,一个while循环:

while (condition){
    body
}
Run Code Online (Sandbox Code Playgroud)

实际上与此相同:

if (condition){
    do{
        body
    }while (condition);
}
Run Code Online (Sandbox Code Playgroud)

如果你知道你总是至少循环一次,那if语句是无关紧要的.


同样在汇编级别,这大致是不同循环编译的方式:

do-while循环:

start:
    body
    test
    conditional jump to start
Run Code Online (Sandbox Code Playgroud)

while循环:

    test
    conditional jump to end
start:
    body
    test
    conditional jump to start
end:
Run Code Online (Sandbox Code Playgroud)

请注意,条件已重复.另一种方法是:

    unconditional jump to end
start:
    body
end:
    test
    conditional jump to start
Run Code Online (Sandbox Code Playgroud)

...交换掉重复代码以进行额外的跳转.

无论哪种方式,它仍然比正常do-while循环更糟糕.

也就是说,编译器可以做他们想做的事.如果他们能证明循环总是进入一次,那么它就为你完成了工作.


但是对于问题中的特定示例来说,事情有点奇怪,因为它有一个空循环体.由于没有身体,因此while和之间没有逻辑差异do-while.

FWIW,我在Visual Studio 2012中对此进行了测试:

  • 对于空体,它确实为while和生成相同的代码do-while.因此,当编译器不那么好时,这部分很可能是旧时代的遗留物.

  • 但是由于非空体,VS2012设法避免重复条件代码,但仍会产生额外的条件跳转.

所以具有讽刺意味的是,尽管问题中的示例突出了为什么do-while循环在一般情况下可能更快,但示例本身似乎并没有给现代编译器带来任何好处.

考虑到评论的年龄,我们只能猜测它为何重要.当时的编译器很可能无法识别出身体是空的.(或者如果他们这样做,他们就不会使用这些信息.)

  • @ H2CO3"如果循环只运行一次或两次,那么该循环不值得优化." - 我不敢苟同.它可能在另一个循环中.我自己高度优化的HPC代码是这样的.是的,do-while确实有所作为. (30认同)
  • @ H2CO3我在哪里说我鼓励它?问题是`do-while`循环比while循环更快.我回答这个问题说它可以更快.我没说多少钱.我没有说是否值得.我没有建议任何人开始转换为do-while循环.但是,简单地否认有可能进行优化,即使它是一个小优化,在我看来是对那些关心并对这些事情感兴趣的人的伤害. (29认同)
  • 那么检查条件**少一点**这么大的优势呢?我非常怀疑.运行循环100次,它变得完全无关紧要. (12认同)
  • @ H2CO3但是如果循环只运行一次或两次怎么办?那么重复的条件代码增加了代码大小呢? (7认同)
  • @Mystical如果循环只运行一次或两次,那么该循环不值得优化.增加的代码大小......最多也不是一个可靠的论据.并不要求每个编译器都以您展示的方式实现它.我为自己的玩具语言编写了一个[编译器](https://github.com/H2CO3/Sparkling/blob/master/src/compiler.c#L701-L740),并使用一个实现了while循环的编译.无条件跳转到循环的开头,因此条件的代码只发出一次. (6认同)
  • @Mysticial我没有.当我想要至少循环一次时,我使用`while`,而当我不循环时,我会使用`do-while`,但是`while`的用例数量确实比'do-while`的用例更为真实也许三到四个数量级,我可能会多年没有使用`do-while`.我不准备推测"大多数人"的所作所为.您的信息来源是什么? (2认同)

小智 24

有没有证据表明大多数(或任何)编译器会生成更好(例如更高效)的代码?

除非您在特定平台上查看具有某些特定优化设置实际特定编译器实际生成程序集,否则不多.

这可能值得担心几十年前(编写ZLib时),但现在肯定不会,除非您通过实际分析发现这消除了代码中的瓶颈.

  • 我不认为评价最高的答案会鼓励过早优化.我认为它表明效率的差异是可能的,无论它有多么轻微或微不足道.但人们对事物的解释却不同,有些人可能会认为这是开始使用do-while循环的标志,而不是必要的(我希望不是).无论如何,到目前为止我对所有答案感到高兴.它们提供了有价值的信息,并产生了有趣的讨论. (16认同)
  • 好吧 - 在这里想到"过早优化"这个短语. (9认同)

Lee*_*eor 16

简而言之(tl; dr):

我正在解释OPs代码中的注释略有不同,我认为他们声称观察到的"更好的代码"是由于将实际工作转移到循环"条件"中.然而,我完全同意它是非常特定于编译器的,并且它们所做的比较虽然能够产生稍微不同的代码,但是大部分都是毫无意义的并且可能已经过时,如下所示.


细节:

很难说他的评论是关于这个do {} while产生更好的代码的原始作者的意思,但我想在另一个方向推测而不是在这里提出的 - 我们认为do {} whilewhile {}循环之间的差异非常小(少一个分支作为神秘说道,但是在这段代码中有一些甚至"更有趣"的东西,它将所有的工作都置于这种疯狂的状态,并将内部部分保持为空(do {}).

我在gcc 4.8.1(-O3)上尝试了以下代码,它给出了一个有趣的区别 -

#include "stdio.h" 
int main (){
    char buf[10];
    char *str = "hello";
    char *src = str, *dst = buf;

    char res;
    do {                            // loop 1
        res = (*dst++ = *src++);
    } while (res);
    printf ("%s\n", buf);

    src = str;
    dst = buf;
    do {                            // loop 2
    } while (*dst++ = *src++);
    printf ("%s\n", buf);

    return 0; 
}
Run Code Online (Sandbox Code Playgroud)

编译后 -

00000000004003f0 <main>:
  ... 
; loop 1  
  400400:       48 89 ce                mov    %rcx,%rsi
  400403:       48 83 c0 01             add    $0x1,%rax
  400407:       0f b6 50 ff             movzbl 0xffffffffffffffff(%rax),%edx
  40040b:       48 8d 4e 01             lea    0x1(%rsi),%rcx
  40040f:       84 d2                   test   %dl,%dl
  400411:       88 16                   mov    %dl,(%rsi)
  400413:       75 eb                   jne    400400 <main+0x10>
  ...
;loop 2
  400430:       48 83 c0 01             add    $0x1,%rax
  400434:       0f b6 48 ff             movzbl 0xffffffffffffffff(%rax),%ecx
  400438:       48 83 c2 01             add    $0x1,%rdx
  40043c:       84 c9                   test   %cl,%cl
  40043e:       88 4a ff                mov    %cl,0xffffffffffffffff(%rdx)
  400441:       75 ed                   jne    400430 <main+0x40>
  ...
Run Code Online (Sandbox Code Playgroud)

因此,第一个循环执行7个指令,而第二个循环执行6个指令,即使它们应该执行相同的工作.现在,我无法确定这背后是否有一些编译器智能,可能不是,这只是巧合,但我还没有检查它是如何与该项目可能使用的其他编译器选项交互的.


另一方面,在clang 3.3(-O3)上,两个循环都生成这5个指令代码:

  400520:       8a 88 a0 06 40 00       mov    0x4006a0(%rax),%cl
  400526:       88 4c 04 10             mov    %cl,0x10(%rsp,%rax,1)
  40052a:       48 ff c0                inc    %rax
  40052d:       48 83 f8 05             cmp    $0x5,%rax
  400531:       75 ed                   jne    400520 <main+0x20>
Run Code Online (Sandbox Code Playgroud)

这只是表明编译器是完全不同的,并且推进速度远远超过一些程序员几年前所预期的速度.这也意味着这个评论毫无意义,可能就是因为没有人检查过它是否仍然有意义.


底线 - 如果你想优化到最好的代码(你知道它应该是什么样子),直接在汇编中做,并从等式中剪切"中间人"(编译器),但考虑到更新编译器和更新的HW可能会使此优化过时.在大多数情况下,让编译器为您完成这一级别的工作要好得多,并专注于优化大型工作.

应该做的另一点 - 指令计数(假设这是原始OP代码所用的),对于代码效率来说绝不是一个好的衡量标准.并非所有指令都是相同的,其中一些(例如简单的reg-to-reg移动)非常便宜,因为它们被CPU优化.其他优化实际上可能会损害CPU内部优化,因此最终只有适当的基准测试计数.

  • 看起来你在原始代码中解释的注释与大多数注释不同,这是有道理的.评论说"有趣的做{} ..",但没有说它比较的非搞笑版本.大多数人都知道DO-while和while之间的区别,所以我的猜测是,"有趣DO {}"并不适用于这一点,但对循环展开和/或缺乏额外分配的,当你表现这里. (3认同)

use*_*421 10

while环通常编译为一个do-while循环具有初始分支的条件,即

    bra $1    ; unconditional branch to the condition
$2:
    ; loop body
$1:
    tst <condition> ; the condition
    brt $2    ; branch if condition true
Run Code Online (Sandbox Code Playgroud)

do-while没有初始分支的循环编译是相同的.你可以从中看出,初始分支的成本固有地降低了效率,但是只支付了一次.[比较天真的实现方式,while()每次迭代都需要条件分支和无条件分支.]

话虽如此,它们并不是真正可比的替代方案.将while,循环转换为while循环是很痛苦的,反之亦然.他们做不同的事情.在这种情况下的几个方法调用将完全主宰一切的编译器做do-while对抗while


Yve*_*ust 7

这句话不是关于控制语句的选择(do vs. while),而是关于循环展开!

正如您所看到的,这是一个字符串比较函数(字符串元素可能长度为2个字节),可以使用单个比较而不是快捷方式和表达式中的四个来编写.

后一种实现肯定更快,因为它在每四个元素比较之后对字符串结束条件进行单次检查,而标准编码将涉及每次比较一次检查.换句话说,每4个元素进行5次测试,每4个元素进行8次测试.

无论如何,它只有在字符串长度是4的倍数或者有一个sentinel元素时才会起作用(这样两个字符串保证在strend边界之外不同).风险很大!