コンパイラーは、他のタイプのループと比較して、do-whileループに対してより優れたコードを生成しますか?


89

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

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

更新: 元の著者の1人であるMark Adlerがコメントに少しコンテキストを与えました


7
ちなみに、明確にするために、これはクロムの一部ではありません。URLから推測できるように、これは「サードパーティ」プロジェクトであり、さらに詳しく見ると、このコードが広く使用されている汎用圧縮ライブラリであるZLibからのものであることがわかります。

1
The funny "do {}" generates better code---何より良いですか?面白いwhile()よりも、つまらないより、定期的に行うのですか{}?
n。「代名詞」m。

@ H2CO3明確化に感謝します。原因についてより具体的になるように質問を編集しました。
Dennis

42
そのコメントは、BorlandとSun Cコンパイラの時代に18年以上前に書かれました。今日のコンパイラとの関連性はまったくの偶然です。のdo単なる使用とは対照的に、この特定の使用法はwhile条件付き分岐を回避しないことに注意してください。
Mark Adler

回答:


108

まず第一に:

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

  • whileforループはループ本体をまったく実行しない場合があります。
  • do-whileループは常に少なくとも一度ループ本体を実行します-それは、初期状態のチェックをスキップします。

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

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

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

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


Aは、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回入ることを彼らが証明できれば、それはあなたのために仕事をしました。


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

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

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

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

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

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


12
では、状態を1回少なく確認することは、非常に大きな利点ですか。私はそれを強く疑います。ループを100回実行すると、まったく意味がなくなります。

7
@ H2CO3しかし、ループが1回または2回しか実行されない場合はどうでしょうか。そして、重複した条件コードから増加したコードサイズはどうですか?
Mysticial 2013年

6
@Mysticalループが1回または2回しか実行されない場合、そのループは最適化する価値がありません。そして、コードサイズの増加は...せいぜい、確かな議論ではありません。すべてのコンパイラがあなたが示した方法で実装する必要はありません。私は自分のおもちゃ言語用のコンパイラーを作成しましたが、whileループのコンパイルはループの先頭への無条件ジャンプで実装されているため、条件のコードは1回だけ発行されます。

30
@ H2CO3「ループが1回または2回しか実行されない場合、そのループは最適化する価値がありません。」- 失礼ですが同意できません。別のループ内にある可能性があります。私自身の高度に最適化された大量のHPCコードはこのようなものです。そして、はい、do-whileは違いを生みます。
Mysticial 2013年

29
@ H2CO3私はそれを励ましていたとどこに言ったのですか?質問の質問は、do-whilewhileループよりも速いループです。そして、私はそれがより速くなることができると言って質問に答えました。なんて言ってなかった。それが価値があるかどうかは言わなかった。do-whileループに変換することをだれにも勧めませんでした。しかし、たとえそれが小さなものであっても、最適化の可能性があることを単に否定することは、私の意見では、これらのことに関心を持ち、興味を持っている人にとっては不利益です。
Mysticial 2013年

24

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

特定の最適化設定を使用して、特定のプラットフォーム実際に特定のコンパイラー実際に生成されたアセンブリを調べない限り、それほど多くはありません

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


9
よく言います- premature optimizationここでフレーズが思い浮かびます。
James Snell

@JamesSnell正確に。そして、それこそが最高評価の回答がサポート/奨励するものです。

16
最高評価の回答が時期尚早の最適化を助長するとは思わない。私はそれが効率の違いが可能であることを示していると主張しますが、それがわずかまたは取るに足らないものであるかもしれません。しかし、人々は物事を異なる方法で解釈し、それが必要でないときにdo-whileループの使用を開始する兆候と見なす場合があります(私はそうは思いません)。とにかく、これまでのすべての答えに満足しています。それらは質問に関する貴重な情報を提供し、興味深い議論を生み出しました。
Dennis

16

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

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


詳細:

原作者はこれについての彼のコメントで何を意味するのかと言うのは難しいdo {} while産より良いコードが、私はここで育てられたものよりも、別の方向に推測したい-私たちは、違いと信じているdo {} whilewhile {}、ループはかなり(スリム1本の以下の枝などであります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つの命令を実行し、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の内部最適化に悪影響を与える可能性があるため、最終的には適切なベンチマークのみがカウントされます。


見当移動を節約できるようです。mov %rcx,%rsi:)私はコードを再配置することがどのようにそれを行うことができるかを見ることができます。
Mysticial 2013年

@Mystical、あなたはマイクロ最適化については正しいです。時には、単一の命令を保存するだけでも何の価値もありません(そして、regからRegへの移動は、今日のregの名前変更でほぼ無料になるはずです)。
Leeor 2013年

AMD BulldozerとIntel Ivy Bridgeまでは、移動の名前変更が実装されていたようには見えません。それは驚きです!
Mysticial 2013年

@Mysticial、これらは物理レジスタファイルを実装する最初のプロセッサにほぼ注意してください。古い順不同の設計では、レジスタをリオーダバッファに配置するだけで、それができません。
Leeor 2013年

3
元のコードのコメントを他のコードとは異なる方法で解釈したようですが、それは理にかなっています。コメントは「おもしろい{} ..」と書いてありますが、それがどんなおかしくないバージョンと比較しているかは述べていません。ほとんどの人はdo-whileとwhileの違いを知っているので、「おかしいdo {}」はそれには当てはまらなかったと思いますが、あなたが示したように、ループのアンロールや余分な割り当ての欠如に当てはまります。ここに。
アベル

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

一方、do-whileループのコンパイルは、最初の分岐がなければ同じです。while()これは、最初のブランチのコストによって本質的に効率が低下していることがわかります。[ while,反復ごとに条件付き分岐と無条件分岐の両方を必要とする素朴な実装方法と比較してください。]

そうは言っても、それらは実際に比較可能な代替手段ではありません。whileループをループに、do-whileまたはその逆に変換するのは困難です。彼らは異なることをします。そしてこの場合、いくつかのメソッド呼び出しは、コンパイラが何をするのかに対して何をしたかを完全に支配whileしますdo-while.


7

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

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

この後者の実装は、4つの要素を比較するたびに文字列の終わりの条件を1回チェックするため、確かに高速です。つまり、4要素あたり5つのテストと4要素あたり8つのテストです。

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


それは興味深い観察であり、今まで誰もが見過ごしてきたものです。しかし、コンパイラーはそれに影響を与えませんか?つまり、使用するコンパイラーに関係なく、常により効率的です。それでは、コンパイラに言及するコメントがあるのはなぜですか?
Dennis

@Dennis:コンパイラーによって、生成されるコードを最適化する方法が異なります。一部のユーザーは、ループアンロールを(ある程度まで)行うか、割り当てを最適化します。ここで、コーダーはコンパイラーをループアンローリングに強制し、最適化の少ないコンパイラーがまだ十分に機能するようにします。イヴは彼の仮定については正確だと思いますが、元のコーダーがいなければ、「面白い」発言の背後にある本当の考えが何であったかは少し謎のままです。
アベル

1
@Abelの説明に感謝します。コメントの背後にある(想定される)意味が理解しやすくなりました。イヴは間違いなくコメントの背後にある謎を解くのに最も近くなりましたが、彼が私の質問に最もよく答えたと思うので、Mysticialの答えを受け入れます。コメントがループのタイプに焦点を合わせるのを誤解させているので、私は間違った質問をしていたことがわかりました。
Dennis

0

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

while (Condition)
{
}

そして

do
{
}
while (Condition);

完全に同等です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.