<は<=より速いですか?


1574

であるif( a < 901 )よりも速くif( a <= 900 )

この単純な例とまったく同じではありませんが、ループの複雑なコードではパフォーマンスにわずかな変更があります。これが真実である場合に備えて、これは生成されたマシンコードで何かをしなければならないと思います。


153
この質問の歴史的重要性、回答の質、およびパフォーマンスの他の上位の質問が開かれままであることを考えると、この質問を閉じる必要がある理由はありません(特に投票が現在表示されているため、削除しないでください)。最大でロックする必要があります。また、質問自体が誤った情報/ナイーブであっても、本に出ているという事実は、元の誤った情報が「信頼できる」情報源のどこかに存在することを意味します。したがって、この質問はそれを明らかにするのに役立つという点で建設的です。
Jason C

32
どの本を参照しているのか、教えてくれませんでした。
Jonathon Reinhart 2014

160
タイピング<はタイピングより2倍高速です<=
2016

6
それは8086で本当でした。–
ジョシュア

7
賛成票の数は、過度に最適化しすぎている何百人もの人々がいることを明確に示しています。
m93a

回答:


1704

いいえ、ほとんどのアーキテクチャでは高速にはなりません。指定しませんでしたが、x86では、すべての整数比較は通常、2つの機械語命令で実装されます。

  • 設定するA testまたはcmp命令EFLAGS
  • Jcc比較)タイプ(およびコードレイアウト)に応じた(ジャンプ)命令
    • jne -等しくない場合はジャンプ-> ZF = 0
    • jz -ゼロ(等しい)ならジャンプ-> ZF = 1
    • jg -大きい場合はジャンプ-> ZF = 0 and SF = OF
    • (等...)

(簡潔にするために編集)でコンパイル$ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

コンパイルして:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

そして

    if (a <= b) {
        // Do something 2
    }

コンパイルして:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

そこで、両者の唯一の違いは、jgjge命令。2つは同じ時間かかります。


別のジャンプ命令が同じ時間かかることを示すものは何もないというコメントに対処したいと思います。これは答えるのが少し難しいですが、ここに私が与えることができるものがあります:インテルインストラクションセットリファレンスでは、それらはすべて1つの共通の命令にグループ化されていますJcc(条件が満たされた場合にジャンプします)。同じグループは、付録Cの「最適化リファレンスマニュアル」の「レイテンシとスループットでまとめられています。

レイテンシ —実行コアが命令を形成するすべてのμopの実行を完了するために必要なクロックサイクル数。

スループット —発行ポートが同じ命令を再び自由に受け入れる前に待機するのに必要なクロックサイクル数。多くの命令では、命令のスループットはレイテンシよりも大幅に少なくなる可能性があります

の値は次のJccとおりです。

      Latency   Throughput
Jcc     N/A        0.5

次の脚注でJcc

7)条件付きジャンプ命令の選択は、分岐の予測可能性を向上させるために、セクション3.4.1「分岐予測の最適化」の推奨に基づいている必要があります。ブランチの予測が成功すると、のレイテンシjccは事実上ゼロになります。

したがって、インテルのドキュメントでは、1つのJcc命令を他の命令と異なる方法で処理するものはありません。

命令を実装するために使用される実際の回路について考える場合EFLAGS、条件が満たされているかどうかを判断するために、のさまざまなビットに単純なAND / ORゲートがあると想定できます。そのため、2ビットをテストする命令が、1つだけをテストする命令よりも多少の時間がかかる理由はありません(クロックの周期よりもはるかに短いゲート伝播遅延は無視されます)。


編集:浮動小数点

これは、x87浮動小数点にも当てはまります(上記とほぼ同じコードですが、doubleではなく)int

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret

239
@Dypplは実際jgjnleは同じ指示です7F:-)
Jonathon Reinhart

17
オプティマイザが実際に1つのオプションが他のオプションよりも速い場合は、コードを変更できることは言うまでもありません。
Elazar Leibovich 2012

3
何かが同じ量の命令をもたらすからといって、それらの命令をすべて実行する合計時間が同じになるとは限りません。実際には、より多くの命令をより速く実行できます。サイクルごとの命令は固定数ではなく、命令によって異なります。
jontejj 2013年

22
@jontejj私はそのことをよく知っています。私の答えまで読んだ?私は状態と同じについては何もしませんでしたの命令を、私は、彼らは基本的にまったく同じにコンパイルされていると述べたinstrutcions 1つのジャンプ命令が1つのフラグを見ていると、他のジャンプ命令は二つのフラグを見ている以外、。私はそれらが意味的に同一であることを示すのに十分以上の証拠を与えてきたと思います。
Jonathon Reinhart 2013年

2
@jontejjあなたは非常に良い点を作ります。この答えが得られる限りの可視性を得るために、おそらく少し整理しておく必要があります。フィードバックをお寄せいただきありがとうございます。
Jonathon Reinhart 2013年

593

歴史的に(1980年代と1990年代初頭を話している)、これが当てはまるアーキテクチャがいくつかあった。根本的な問題は、整数の比較が本質的に整数の減算を介して実装されていることです。これにより、以下のケースが発生します。

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

これでA < B、減算を正しく行うために減算がハイビットを借用する必要がある場合は、手作業で加算および減算するときに持ち運びおよび借用を行うのと同じです。この「借用」ビットは通常キャリービットと呼ばれ、分岐命令でテストできます。第2のビットは、呼び出されたゼロ・ビットを減算は同じ等式を暗示ゼロであった場合に設定されるであろう。

通常、少なくとも2つの条件付き分岐命令があり、1つはキャリービットで分岐し、もう1つはゼロビットで分岐します。

さて、問題の核心をつかむために、前の表を拡張して、キャリーとゼロビットの結果を含めます。

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

したがって、の分岐の実装A < Bは1つの命令で実行できます。キャリービットはこの場合にのみクリアされるため、つまり、

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

ただし、以下の比較を実行する場合は、ゼロフラグの追加チェックを実行して、等しい場合を検出する必要があります。

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

したがって、一部のマシンでは、「小なり」比較を使用すると、1つのマシン命令を節約できる場合があります。これは、サブメガヘルツのプロセッサ速度と1:1のCPUとメモリの速度比の時代には関係がありましたが、今日ではほとんど関係ありません。


10
さらに、x86のようなアーキテクチャjgeは、ゼロフラグと符号/キャリーフラグの両方をテストするのような命令を実装します。
greyfade 2012

3
それが特定のアーキテクチャに当てはまる場合でも。どのコンパイラー作成者も気づかず、低速を高速に置き換える最適化を追加した確率は何ですか?
Jon Hanna

8
これは8080にも当てはまります。ゼロにジャンプしてマイナスにジャンプするように指示されていますが、両方を同時にテストできるものはありません。

4
これは、Motorola 68HC11 / 12にも拡張される6502および65816プロセッサフ​​ァミリにも当てはまります。
ルーカス

31
8080でも、オペランドを交換して<=テストする1つの命令でテストを実装できますnot <(と同等>=)。これは、<=オペランドを交換した場合に必要ですcmp B,A; bcs addr。それが、このテストがIntelによって省略された理由であり、彼らはそれを冗長であると考え、そのときに冗長な命令を用意することができませんでした:-)
Gunther Piez

92

内部整数型について話していると仮定すると、一方が他方よりも高速になる可能性はありません。それらは明らかに意味的に同一です。どちらもコンパイラにまったく同じことをするように要求します。ひどく壊れたコンパイラだけが、これらのいずれかに対して劣ったコードを生成します。

単純な整数型<よりも高速なプラットフォームがあった場合<=、コンパイラは常に定数に変換<=する必要があり<ます。できなかったコンパイラは、そのプラットフォーム用の悪いコンパイラになるだけです。


6
+1同意する また、コンパイラーがどの速度になるかを決定するまで、速度<<=速度もありません。あなたは、彼らは一般的に、すでになどデッドコードの最適化、末尾呼び出しの最適化、ループ巻き上げ(および機会に、アンロール)、様々なループの自動並列化を行うことを考えるとこれはコンパイラのための非常に単純な最適化です... なぜ無駄時間は時期尚早の最適化を考え?プロトタイプを実行し、プロファイリングして最も重要な最適化がどこにあるかを特定し、それらの最適化を重要度の順に実行し、進行状況を測定する方法に沿って再度プロファイルします...
自閉症

1つの定数値を持つ比較が<=で遅くなる可能性があるいくつかのエッジケースがまだあります。たとえば、(ある定数の)から(a < C)への変換が命令セットでエンコードするのをより困難にする場合などです。たとえば、命令セットは比較で-127から128までの符号付き定数をコンパクトな形式で表すことができますが、その範囲外の定数は、より長い、遅いエンコーディング、または別の命令を使用してロードする必要があります。したがって、のような比較では、簡単な変換は行われない場合があります。(a <= C-1)CC(a < -127)
BeeOnRope

@BeeOnRope問題は、定数が異なるために異なる操作を実行するとパフォーマンスに影響するかどうかではなく、異なる定数を使用して同じ操作を表現するパフォーマンスに影響するかどうかでした。ですから、選択の余地がないので比較a > 127していませんa > 128。必要なものを使用します。同じ真理値表を持っているため、異なるエンコーディングや異なる命令を必要としないと比較a > 127a >= 128ています。一方のエンコーディングは、他方のエンコーディングと同じです。
David Schwartz

「<=の方が遅い]コンパイラーは常に定数に変換<=する必要があるプラットフォームがある場合」という一般的な方法で私はあなたの声明に応えていました<。私の知る限り、その変換には定数の変更が含まれます。例えば、速いのでa <= 42コンパイルされます。いくつかのエッジケースでは、新しい定数がより多くのまたはより遅い命令を必要とする可能性があるため、このような変換は実りがありません。もちろん、同等です。コンパイラーは両方の形式を(同じ)最速の方法でエンコードする必要がありますが、それは私が言ったことと矛盾しません。a < 43<a > 127a >= 128
BeeOnRope 2016年

67

どちらも速くないことがわかります。コンパイラーは、各条件で同じ値の異なるマシンコードを生成します。

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

私の例ifは、Linux上のx86_64プラットフォーム上のGCCからのものです。

コンパイラー作成者はかなり賢い人であり、彼らはこれらのことや私たちのほとんどが当然だと思っている他の多くのことを考えています。

定数でない場合、どちらの場合も同じマシンコードが生成されることに気付きました。

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3

9
これはx86に固有のものであることに注意してください。
Michael Petrotta 2012

10
それを使用してif(a <=900)、まったく同じasmを生成することを示す必要があると思います:)
Lipis

2
@AdrianCornish申し訳ありません。私はそれを編集しました。多かれ少なかれ同じです..しかし、もし秒を<= 900に変更すると、asmコードはまったく同じになります:)今はほとんど同じですが、あなたは知っている.. OCDの場合:)
Lipis '27

3
@Boann if(true)になり、完全に排除される可能性があります。
Qsario 2012

5
この最適化は定数比較にのみ適用されることを指摘した人はいません。保証します2つの変数を比較する場合、このように行われないます。
Jonathon Reinhart、2012

51

浮動小数点コードの場合、<=の比較は、現代のアーキテクチャでも実際には(1つの命令で)遅くなる可能性があります。これが最初の関数です:

int compare_strict(double a, double b) { return a < b; }

PowerPCでは、最初にこれが浮動小数点比較(cr条件レジスターを更新)を実行し、次に条件レジスターをGPRに移動し、「小なり比較」ビットを所定の位置にシフトしてから戻ります。4つの指示が必要です。

代わりにこの関数を考えてみましょう:

int compare_loose(double a, double b) { return a <= b; }

これには上記と同じ作業が必要ですcompare_strictが、今は2つの興味深い点があります。「より小さい」と「等しい」です。これには、crorこれら2つのビットを1つに結合するための追加の命令(-条件レジスタのビットごとのOR)が必要です。したがってcompare_loose、5つの命令compare_strictが必要ですが、4つ必要です。

コンパイラーが2番目の関数を次のように最適化できると思うかもしれません。

int compare_loose(double a, double b) { return ! (a > b); }

ただし、これはNaNを誤って処理します。 NaN1 <= NaN2そしてNaN1 > NaN2両方の必要性をfalseに評価されます。


幸い、x86(x87)ではこのように機能しません。fucomipZFとCFを設定します。
Jonathon Reinhart 2012

3
@JonathonReinhart:PowerPCが何をしているか誤解していると思います-条件レジスターcr x86のようなフラグZFと同等CFです。(ただし、CRの方が柔軟性があります。)ポスターが話しているのは、結果をGPRに移動することです。これは、PowerPCで2つの命令を受け取りますが、x86には条件付きの移動命令があります。
ディートリッヒエップ2012

@DietrichEppステートメントの後に追加するつもりだったのは、EFLAGSの値に基づいてすぐにジャンプできることです。明確ではないため申し訳ありません。
Jonathon Reinhart、2012

1
@JonathonReinhart:はい、CRの値に基づいてすぐにジャンプすることもできます。答えはジャンプについてではなく、そこから追加の指示が出されます。
ディートリッヒエップ2012

34

おそらく、その無名の本の著者はa > 0a >= 1それが普遍的に真実であると考えています。

しかし、それはa 0が関係しているため(CMPアーキテクチャによっては、たとえばで置き換えられるORため)、のためではありません<


1
以下のために確かに、「デバッグ」ビルドでは、それは悪いコンパイラを取る(a >= 1)よりも遅い実行するために(a > 0)、前者は自明オプティマイザによって、後者に変換することができるので、..
BeeOnRope

2
@BeeOnRopeオプティマイザが最適化できる複雑な事柄と、そうしなかった簡単な事柄に時々驚かされます。
glglgl 2016年

1
確かに、そして、それが重要である非常に少数の関数については、常にasm出力をチェックする価値があります。とはいえ、上記の変換は非常に基本的であり、単純なコンパイラでも数十年にわたって実行されてきました。
BeeOnRope 2016年

32

少なくとも、これが当てはまる場合、コンパイラーはa <= bを!(a> b)に自明に最適化できます。そのため、比較自体が実際に遅い場合でも、最も単純なコンパイラー以外は違いに気付かないでしょう。 。


!(a> b)が<= bの最適化バージョンである理由 !(a> b)2つの操作が1つになっていないか?
Abhishek Singh

6
@AbhishekSingh NOTは、他の命令(jevs jne)によって作成されます
Pavel Gatnar 2015

15

彼らは同じ速度を持っています。たぶんいくつかの特別なアーキテクチャでは彼/彼女が言ったことは正しいですが、x86ファミリでは少なくとも私はそれらが同じであることを知っています。これを行うために、CPUは減算(a-b)を実行してから、フラグレジスタのフラグをチェックします。そのレジスタの2ビットはZF(ゼロフラグ)およびSF(符号フラグ)と呼ばれ、1回のマスク操作で実行されるため、1サイクルで実行されます。


14

これは、Cがコンパイルされる基本的なアーキテクチャに大きく依存します。一部のプロセッサとアーキテクチャには、異なるサイクル数で実行される以下の明示的な命令がある場合があります。

しかし、コンパイラーがそれを回避して無関係にすることができるので、それはかなり珍しいでしょう。


1
サイクルに違いがあった場合。1)検出されない。2)その価値があるコンパイラーは、コードの意味を変更せずに、低速フォームから高速フォームへの変換をすでに行っています。したがって、作成された結果の命令は同じになります。
マーティンヨーク

完全に同意します。それはいずれにしても、かなり些細で愚かな違いです。確かに、プラットフォームにとらわれないはずの本で言及すべきことは何もない。
Telgin、2012

@lttlrck:わかりました。しばらく(ばかげた)私を取りました。いいえ、測定を不可能にする他の多くのことが起こっているため、それらは検出できません。プロセッサストール/キャッシュミス/シグナル/プロセススワッピング。したがって、通常のOSの状況では、単一サイクルレベルの状況は物理的に測定できません。測定からそのすべての干渉を排除できる場合(オンボードメモリを搭載し、OSを搭載していないチップで実行する場合)、心配するタイマーの細分性はありますが、理論的には、十分に長く実行すると、何かが見える可能性があります。
マーティンヨーク

12

TL; DR回答

アーキテクチャー、コンパイラー、および言語のほとんどの組み合わせでは、それは速くはありません。

完全な答え

他の回答はx86アーキテクチャに集中しており、生成されたコードについて具体的にコメントできるARMアーキテクチャ(サンプルアセンブラのようです)はよくわかりませんが、これ非常にアーキテクチャであるマイクロ最適化の例です具体的で、最適化よりも反最適化の可能性があります

そのため、このようなマイクロ最適化は、ソフトウェアエンジニアリングのベストプラクティスではなく、カーゴカルトプログラミングの例であることをお勧めします。

これが最適化であるいくつかのアーキテクチャがおそらくありますが、私は逆が真であるかもしれない少なくとも1つのアーキテクチャを知っています。由緒あるトランスピューターアーキテクチャには以上のマシンコード命令しかなかったため、すべての比較はこれらのプリミティブから構築する必要がありました。

それでも、ほとんどすべてのケースで、コンパイラーは、実際には比較が他のどの製品よりも優れているような方法で評価命令を順序付けることができます。ただし、最悪の場合、オペランドスタックの上位2項目をスワップするために、リバース命令(REV)を追加する必要がある場合があります。これは、実行に1サイクルかかる1バイトの命令だったので、オーバーヘッドは可能な限り最小でした。

このようなマイクロ最適化が最適化である非最適化であるかは、使用している特定のアーキテクチャに依存するため、通常、アーキテクチャ固有のマイクロ最適化を使用する習慣をつけることは悪い考えです。それを行うことが不適切な場合に使用してください。これは、あなたが読んでいる本がまさに主張しているもののように見えます。


6

違いがあっても気付かないはずです。さらに、実際には、いくつかの魔法の定数を使用する場合を除いて、追加の条件を実行するa + 1a - 1、条件を立てる必要があります。これは、決して非常に悪い習慣です。


1
悪い習慣は何ですか?カウンターをインクリメントまたはデクリメントしますか?では、インデックス表記をどのように保存しますか?
jcolebrand 2012

5
彼は、2つの変数型の比較を行っている場合を意味します。もちろん、ループなどに値を設定するのは簡単です。しかし、x <= yであり、yが不明である場合、x <y + 1に「最適化」するのは遅くなります
JustinDanielson

@JustinDanielsonは同意しました。醜い、紛らわしいなどは言うまでもなく
Jonathon Reinhart

4

余分な文字はコード処理がわずかに遅くなるため、ほとんどのスクリプト言語では行が正しいと言えます。ただし、トップの回答が指摘したように、C ++では効果がなく、スクリプト言語で行われていることは、おそらく最適化についてはそれほど心配されていません。


ややそう思わない。競争力のあるプログラミングでは、スクリプト言語が問題に対する最も迅速な解決策を提供することがよくありますが、正しい解決策を得るためには、正しい手法(読み取り:最適化)を適用する必要があります。
タイラークロンプトン、2012

3

この回答を書いたとき、私は一般的に<vs. <=に関するタイトルの質問だけを見ていました。定数a < 901vs.の具体的な例ではありませんでしたa <= 900。多くのコンパイラは、<との間で変換することにより、常に定数の大きさを縮小します<=。たとえば、x86の即値オペランドは-128..127の1バイトエンコーディングが短いためです。

ARM、特にAArch64の場合、即時としてエンコードできるかどうかは、狭いフィールドを単語の任意の位置に回転できるかどうかにかかっています。したがってcmp w0, #0x00f000、エンコード可能ですが、エンコードcmp w0, #0x00effffできない場合があります。したがって、比較とコンパイル時の定数のmake-it-smallerルールは、AArch64に常に適用されるわけではありません。


<対<=一般的に、ランタイム変数条件を含む

ほとんどのマシンのアセンブリ言語では、の比較<=はの比較と同じコストです。<ます。これは、分岐する場合、ブール化して0/1整数を作成する場合、または分岐なしの選択操作(x86 CMOVなど)の述語として使用する場合に適用されます。他の回答は、質問のこの部分のみを扱っています。

しかし、この質問は、オプティマイザへの入力であるC ++演算子についてです。 通常、どちらも同等に効率的です。コンパイラーは常にasmで実装する比較を変換できるため、この本のアドバイスはまったく偽物に聞こえます。しかし、少なくとも1つの例外があり、<=、コンパイラを最適化できないものを誤って作成する可能性があります。

ループ条件として、コンパイラがループが無限ではないことを証明できないようにする場合、<=質的に異なる場合があり<ます。 これにより、大きな違いが生じ、自動ベクトル化が無効になります。

符号なしオーバーフローは、符号付きオーバーフロー(UB)とは異なり、base-2ラップアラウンドとして明確に定義されています。署名されたループカウンターは、通常、署名されたオーバーフローUBに基づいて最適化されるコンパイラーが発生しない場合、++i <= sizeこれから安全です。常に最終的にfalseになります。(すべてのCプログラマが未定義の動作について知っておくべきこと

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

コンパイラーは、未定義の動作につながるものを除いて、可能なすべての入力値に対してC ++ソースの(定義された、法的に観察可能な)動作を維持する方法でのみ最適化できます

(単純なものでi <= sizeも問題が発生しますが、上限の計算は、気にしていないがコンパイラーが考慮しなければならない入力の無限ループの可能性を誤って導入するより現実的な例だと思いました。)

この場合、size=0はにつながりupper_bound=UINT_MAXi <= UINT_MAX常にtrueです。したがって、このループはに対して無限でありsize=0プログラマとしてのあなたがおそらくsize = 0を渡すつもりがない場合でも、コンパイラはそれを尊重する必要があります。コンパイラーがこの関数を呼び出し側にインライン化できる場合、size = 0が不可能であることを証明できれば、すばらしいですが、のように最適化できi < sizeます。

asm like if(!size) skip the loop; do{...}while(--size);は、for( i<size )ループ内で実際の値がi必要ない場合に、ループを最適化するための通常効率的な方法の1つです(なぜ、ループは常に「do ... while」スタイル(テールジャンプ)にコンパイルされるのですか?)。

しかし、これは{}間無限にすることはできません。で入力した場合size==0、2 ^ n回の反復が行われます。(forループ C ですべての符号なし整数を反復すると、0を含むすべての符号なし整数でループを表現できますが、asmのようにキャリーフラグがないと簡単ではありません。)

ループカウンターのラップアラウンドが可能である場合、最近のコンパイラーは、しばしば「あきらめ」、ほとんど積極的に最適化しません。

例:1からnまでの整数の合計

符号なしi <= nを使用sum(1 .. n)すると、ガウスのn * (n+1) / 2式に基づいて閉形式でループを最適化するclangのイディオム認識が無効になります

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

Godboltコンパイラエクスプローラーのclang7.0およびgcc8.2からのx86-64 asm

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

しかし、単純なバージョンの場合、clangからばかげたループを取得するだけです。

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCCはどちらの方法でも閉形式を使用しないため、ループ条件の選択が実際に害を及ぼすことはありません。SIMD整数加算で自動ベクトル化しi、XMMレジスタの要素で4つの値を並列に実行します。

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.

また、非常に小さいn、または無限ループの場合に使用すると思われる単純なスカラーループもあります。

ところで、これらのループはどちらも、ループオーバーヘッドの命令(およびSandybridgeファミリCPUのuop)を無駄にします。 sub eax,1/ cmp / jccのjnz代わりにadd eax,1/がより効率的です。2ではなく1 uop(sub / jccまたはcmp / jccのマクロ融合後)。両方のループの後のコードは無条件にEAXを書き込むため、ループカウンターの最終値を使用していません。


素晴らしい人為的な例。EFLAGSの使用が原因で、順序どおりに実行されない可能性があるという他のコメントはありますか?それは純粋に理論的なものですか、それとも実際にJBがJBEよりも優れたパイプラインにつながる可能性がありますか?
rustyx

@rustyx:別の答えの下のどこかにコメントしましたか?コンパイラーは、部分的なフラグのストールを引き起こすコードを出力することはなく、C <または<=。ただし、ZFが設定されている場合(ecx == 0)、またはCFが設定されている場合(EAX == 1のビット3)、test ecx,ecx/ bt eax, 3/ jbeはジャンプします。これにより、ほとんどのCPUで部分的なフラグストールが発生します。フラグを書き込む最後の命令から来る。Sandybridgeファミリでは、実際にはストールせず、マージしているuopを挿入する必要があるだけです。 cmp/ testすべてのフラグを書き込みますが、btZFは変更しません。felixcloutier.com/x86/bt
Peter Cordes

2

コンピュータを作成した人々がブール論理に悪ければ。彼らがすべきではないもの。

すべての比較(>= <= > <)を同じ速度で実行できます。

すべての比較が何であるかは、単に減算(違い)であり、それが正か負かを確認することです。
msbが設定されている場合、数値は負です)

確認方法はa >= b?サブa-b >= 0チェックa-bが正であるかどうか。
確認方法はa <= b?サブ0 <= b-aチェックb-aが正であるかどうか。
確認方法はa < b?サブa-b < 0チェックa-bが負の場合。
確認方法はa > b?サブ0 > b-aチェックb-aが負の場合。

簡単に言うと、コンピュータは、指定されたopの内部でこれを実行できます。

a >= b== msb(a-b)==0
a <= b== msb(b-a)==0
a > b== msb(b-a)==1
a < b==msb(a-b)==1

そしてもちろん、コンピュータは実際に==0その==1どちらかを行う必要はありません。なぜなら
==0それmsbは回路から反転するだけです。

とにかく、彼らは間違いなく笑a >= bとして計算されなかったでしょうa>b || a==b

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