编译器是否为其他types的循环产生了更好的do-while循环代码?

在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 */ 

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

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

更新:原作者之一马克·阿德勒 ( Mark Adler)在评论中给了一些背景知识 。

首先:

do-while循环与while -loop或for -loop不同。

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

所以这是合乎逻辑的区别。 也就是说,并不是每个人都严格遵守这一点。 即使在保证循环至less循环一次的情况下for循环也是常见的。 (特别是在具有foreach循环的语言中。)

所以为了避免比较苹果和橙子,我会继续假设循环总是至less运行一次。 此外,我不会再提到循环,因为它们本质上是循环,循环计数器有一些语法糖。

所以我会回答这个问题:

如果一个while循环保证至less循环一次,那么使用do-while循环会有什么性能增益。


一个do-while跳过了第一个条件检查。 所以有一个分支less一个条件来评估。

如果检查的条件是昂贵的,而且你知道你至less要循环一次,那么一个do-while循环可能会更快。

虽然这被认为是微型优化,但它是编译器不能总是这样做的:特别是当编译器无法certificate循环总是至lessinput一次时。


换句话说,一个while循环:

 while (condition){ body } 

实际上是这样的:

 if (condition){ do{ body }while (condition); } 

如果你知道你总是会循环至less一次,那么if语句是无关的。


同样在汇编级别,这大概是如何将不同的循环编译为:

do-while循环:

 start: body test conditional jump to start 

while循环:

  test conditional jump to end start: body test conditional jump to start end: 

请注意,该条件已被复制。 另一种方法是:

  unconditional jump to end start: body end: test conditional jump to start 

…它将重复的代码换成额外的跳转。

无论哪种方式,它仍然比正常的do-while循环更差。

也就是说,编译器可以做他们想做的事情。 如果他们可以certificate循环总是进入一次,那么它已经为你做了工作。


但是对于这个问题中的特定例子来说,事情有点奇怪,因为它有一个空的循环体。 既然没有身体,那么在whiledo-while之间没有逻辑上的区别。

FWIW,我在Visual Studio 2012中testing了这个:

  • 对于空体,它实际上为whiledo-while生成相同的代码。 所以这个部分可能是编译器不如以往的那些旧时代的残余。

  • 但是对于一个非空的实体,VS2012设法避免了条件代码的重复,但是仍然会产生额外的条件跳转。

所以很讽刺的是,虽然这个问题的例子强调了为什么在一般情况下do-while循环会更快,但是这个例子本身并没有给现代编译器带来任何好处。

考虑到评论的年龄,我们只能猜测它为什么重要。 当时的编制人员很可能无法认识到尸体是空的。 (或者如果他们这样做,他们不会使用这些信息。)

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

没有多less,除非您查看在特定平台实际生成的实际特定编译器的汇编,并使用一些特定的优化设置。

几十年前(当ZLib已经写完了),这可能是值得担心的,但是现在肯定不是现在,除非你通过真正的分析发现,这消除了你的代码的瓶颈。

简而言之(tl; dr):

我在解释OPs代码中的注释有点不同,我认为他们声称观察到的“更好的代码”是由于将实际的工作转移到了“条件”循环中。 我完全同意,这是非常特定的编译器,他们所做的比较,而能够产生一个稍微不同的代码,是多余的,可能已经过时,如下所示。


细节:

很难说,他的评论关于这个do {} while原来的作者是什么意思, do {} while生产更好的代码,但我想推测在这里提出的另一个方向 – 我们相信do {} whilewhile {}循环相当渺茫(Mystical所说的一个分支),但是在这段代码中有一些更“有趣”的东西,那就是把所有的工作都放在这个疯狂的条件下,并且保持内部部分为空( 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; } 

编译之后 –

 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> ... 

所以第一个循环做了7个指令,而第二个循环做了6个,尽pipe他们应该做同样的工作。 现在,我不能确定是否有一些编译器智能背后,可能不是,这只是巧合,但我没有检查它与这个项目可能使用的其他编译器选项交互。


另一方面,在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> 

这只是表明编译器是完全不同的,并且比几年前一些程序员可能预期的要快得多。 这也意味着这个评论是相当无意义的,可能是因为从来没有人检查过它是否有意义。


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

另一点应该做的 – 指令计数(假设这是原来的OP代码之后),绝不是一个好的代码效率测量。 并不是所有的指令都是相同的,其中一些(例如简单的reg-to-reg移动)实际上便宜,因为它们被CPU优化。 其他优化可能实际上损害了CPU内部优化,所以最终只有适当的基准testing才算得上。

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

  bra $1 ; unconditional branch to the condition $2: ; loop body $1: tst <condition> ; the condition brt $2 ; branch if condition true 

do-while循环的编译在没有初始分支的情况下是相同的。 您可以从中看到,由初始分支的成本本身效率较低,但是只支付一次。 [比较实际的实现方式while,每次迭代都需要一个条件分支和一个无条件分支。]

话虽如此,它们并不是真正的可比较的select。 将while循环转换成do-while循环是相当痛苦的, 反之亦然。 他们做不同的事情。 而在这种情况下,几个方法调用将完全支配编译器所做的任何事情, while不是do-while.

这句话不是关于控制语句(do与while)的select,而是关于循环展开!

正如你所看到的,这是一个string比较函数(string元素可能是2个字节长),可以用一个比较而不是四个快捷键和expression式来写。

后面的实现肯定会更快,因为它在每四个元素比较之后对string结束条件进行单个检查,而标准编码将涉及每个比较一次检查。 换句话说,每4个元素有5个testing,而每4个元素有8个testing。

无论如何,它只有在string长度是四的倍数或者有一个标记元素(这样两个string保证不同于过去的strend边界)时才起作用。 相当危险!

在这种情况下,这个讨论while和do的效率是完全没有意义的,因为没有body。

 while (Condition) { } 

 do { } while (Condition); 

是完全等同的。