web-dev-qa-db-ja.com

コンパイラーは、他のタイプのループと比較して、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 、元の作者の1人、- コンテキストを少し提供 コメント。

89
Dennis

まず第一に:

do-whileループは、while- loopまたはfor- loopと同じではありません。

  • whileおよびforループは、ループ本体をまったく実行しない場合があります。
  • do-whileループは、常に少なくとも1回はループ本体を実行します-初期条件チェックをスキップします。

これが論理的な違いです。とはいえ、誰もがこれを厳守するわけではありません。常に少なくとも1回ループすることが保証されている場合でも、whileまたはforループが使用されることはよくあります。 (特に foreach ループのある言語では。)

したがって、リンゴとオレンジの比較を避けるために、ループは常に少なくとも1回は実行されると想定して進めます。さらに、forループは本質的にwhileループであるため、ループカウンター用の少しの構文糖を使用しているため、再度言及しません。

だから私は質問に答えます:

whileループが少なくとも1回ループすることが保証されている場合、代わりにdo-whileループを使用することでパフォーマンスが向上します。


do-whileは、最初の条件チェックをスキップします。したがって、評価するブランチと条件が1つ少なくなります。

条件のチェックにコストがかかり、少なくとも1回はループすることが保証されている場合は、do-whileループの方が高速です。

これは、せいぜいマイクロ最適化と見なされますが、コンパイラーが常に実行できるわけではありません。具体的には、コンパイラーがループが常に少なくとも1回入ることを証明できない場合。


つまり、whileループ:

while (condition){
    body
}

事実上これと同じです:

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

少なくとも1回はループすることがわかっている場合、そのifステートメントは無関係です。


同様に、アセンブリレベルでは、これは大まかに、さまざまなループが次のようにコンパイルされる方法です。

do-whileループ:

start:
    body
    test
    conditional jump to start

while-loop:

    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ループよりもさらに悪いです。

とはいえ、コンパイラーは必要なことを実行できます。そして、ループが常に1回入ることを証明できる場合、それはあなたのために仕事をしました。


しかし、問題の特定の例はループ本体が空であるため、少し奇妙です。本体がないため、whiledo-whileの間に論理的な違いはありません。

FWIW、私はこれをVisual Studio 2012でテストしました:

  • 空の本体では、実際にはwhiledo-whileに対して同じコードが生成されます。したがって、その部分は、コンパイラがそれほど優れていなかった昔の名残である可能性があります。

  • しかし、空ではないボディを使用すると、VS2012は条件コードの重複を回避できますが、追加の条件ジャンプを生成します。

皮肉なことに、質問の例ではdo-whileループが一般的なケースでより高速である理由を強調していますが、例自体は最新のコンパイラーには何のメリットもないようです。

コメントの古さを考えると、なぜそれが問題になるのかを推測することしかできません。当時のコンパイラーは、本体が空であることを認識できなかった可能性があります。 (または、使用した場合、情報を使用しませんでした。)

108
Mysticial

ほとんどの(または任意の)コンパイラがより良い(たとえば、より効率的な)コードを生成するという証拠はありますか?

実際の生成された実際の特定のコンパイラのアセンブリ特定のプラットフォームをいくつかの特定の最適化で生成した場合を除き、それほど多くはありません。設定

これはおそらく数十年前(ZLibが作成されたとき)に心配する価値がありましたが、実際のプロファイリングによってコードからボトルネックが取り除かれていることが判明しない限り、確かに今日ではありません。

24
user529758

一言で言えば(tl; dr):

私はOPのコードのコメントを少し異なって解釈しています。彼らが観察したと主張する「より良いコード」は、実際の作業をループの「条件」に移動したためだと思います。ただし、これはコンパイラ固有のものであり、わずかに異なるコードを生成することはできますが、以下に示すように、ほとんど無意味でおそらく時代遅れであることに完全に同意します。


詳細:

このdo {} whileに関するコメントで元の作者が何を意味するかを言うのは難しいですが、ここで提起されたものとは別の方向に推測したいと思います。do {} whileの違いはそしてwhile {}ループはかなりスリムです(Mysticalが言ったようにブランチが1つ少なくなります)が、このコードにはさらに「おかしな」ものがあり、すべての作業がこのクレイジーな状態の内部に置かれ、内部部分が空のままです(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つの命令を実行し、2番目のループは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>

これは、コンパイラーがまったく異なり、数年前に一部のプログラマーが予想したよりもはるかに速い速度で進んでいることを示しています。また、このコメントはかなり意味がなく、おそらく意味があるかどうかを誰も確認したことがないため、おそらくそこにもあります。


結論-可能な限り最高のコードに最適化したい場合(そしてコードがどのように見えるかわかっている場合)、それをアセンブリーで直接実行し、方程式から「中間者」(コンパイラー)をカットしますが、その新しいものを考慮に入れますコンパイラと新しいHWは、この最適化を廃止する可能性があります。ほとんどの場合、コンパイラーにそのレベルの作業を任せて、大きなものの最適化に集中する方がはるかに優れています。

もう1つ注意すべき点は、命令カウント(これが元のOPのコードの後に​​あったものと想定)は、コード効率の良い測定値とは言えません。すべての命令が同じように作成されたわけではなく、それらの一部(たとえば、単純なregからregへの移動)は、CPUによって最適化されるため、本当に安価です。他の最適化は実際にはCPUの内部最適化に悪影響を与える可能性があるため、最終的には適切なベンチマークのみがカウントされます。

16
Leeor

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()は、最初の分岐のコストによって本質的に効率が低下することがわかります。 [反復ごとに条件付き分岐と無条件分岐の両方が必要な_while,_を実装する素朴な方法と比較してください。]

そうは言っても、それらは実際に匹敵する代替手段ではありません。 whileループを_do-while_ループに変換し、逆も同様です。それらは異なることをします。この場合、いくつかのメソッド呼び出しは、_do-while._とは異なり、コンパイラがwhileで行った処理を完全に支配します。

10
user207421

発言は、制御ステートメント(do対while)の選択ではなく、ループのアンロールです!!!

ご覧のとおり、これは文字列比較関数(おそらく2バイト長の文字列要素)であり、ショートカットと式で4回ではなく1回の比較で記述することができます。

この後者の実装は、4つの要素の比較ごとに文字列の終了条件の単一のチェックを実行するため、確かに高速ですが、標準のコーディングでは、比較ごとに1つのチェックが含まれます。つまり、4要素あたり5つのテストと4要素あたり8つのテストです。

とにかく、文字列の長さが4の倍数であるか、センチネル要素がある場合にのみ機能します(そのため、2つの文字列はstrend境界を越えて異なることが保証されます)。かなり危険!

7
Yves Daoust

ボディがないため、この場合のwhile対do効率のこの議論は完全に無意味です。

while (Condition)
{
}

そして

do
{
}
while (Condition);

完全に同等です。

0
Yves Daoust