编译器是否为其他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不同。
-
while
和for
循环可能根本不运行循环体。 -
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循环总是进入一次,那么它已经为你做了工作。
但是对于这个问题中的特定例子来说,事情有点奇怪,因为它有一个空的循环体。 既然没有身体,那么在while
和do-while
之间没有逻辑上的区别。
FWIW,我在Visual Studio 2012中testing了这个:
-
对于空体,它实际上为
while
和do-while
生成相同的代码。 所以这个部分可能是编译器不如以往的那些旧时代的残余。 -
但是对于一个非空的实体,VS2012设法避免了条件代码的重复,但是仍然会产生额外的条件跳转。
所以很讽刺的是,虽然这个问题的例子强调了为什么在一般情况下do-while
循环会更快,但是这个例子本身并没有给现代编译器带来任何好处。
考虑到评论的年龄,我们只能猜测它为什么重要。 当时的编制人员很可能无法认识到尸体是空的。 (或者如果他们这样做,他们不会使用这些信息。)
有没有证据表明大多数(或任何)编译器会生成更好的(如更高效的)代码?
没有多less,除非您查看在特定平台上实际生成的实际特定编译器的汇编,并使用一些特定的优化设置。
几十年前(当ZLib已经写完了),这可能是值得担心的,但是现在肯定不是现在,除非你通过真正的分析发现,这消除了你的代码的瓶颈。
简而言之(tl; dr):
我在解释OPs代码中的注释有点不同,我认为他们声称观察到的“更好的代码”是由于将实际的工作转移到了“条件”循环中。 我完全同意,这是非常特定的编译器,他们所做的比较,而能够产生一个稍微不同的代码,是多余的,可能已经过时,如下所示。
细节:
很难说,他的评论关于这个do {} while
原来的作者是什么意思, do {} while
生产更好的代码,但我想推测在这里提出的另一个方向 – 我们相信do {} while
和while {}
循环相当渺茫(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);
是完全等同的。