私はif( a < 901 )
がif( a <= 900 )
より速いと著者が言う本を読んでいます。
この単純な例とまったく同じではありませんが、ループ複素数コードのパフォーマンスがわずかに変更されています。私はそれが本当である場合に備えてこれが生成された機械語で何かをする必要があると思います。
いいえ、それはほとんどのアーキテクチャでは速くはありません。指定しませんでしたが、x86では、すべての整数比較は通常2つのマシン命令で実行されます。
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:
そのため、両者の唯一の違いは、jg
とjge
命令です。両者は同じ時間がかかります。
異なるジャンプ命令が同じ時間がかかることを示すものは何もないというコメントに対処したいと思います。これは答えるのが少し面倒ですが、ここで私が与えることができるものです: インテル命令セットリファレンス では、それらは1つの共通の命令、Jcc
の下にまとめられています(条件が満たされるならジャンプ)。付録C「待ち時間とスループット」の 最適化リファレンスマニュアル の下でも同じグループ化が行われています。
Latency - 命令を形成するすべてのμopの実行を実行コアが完了するのに必要なクロックサイクル数。
Throughput - 発行ポートが再び同じ命令を受け入れられるようになるまで待機するのに必要なクロックサイクル数。多くの命令では、命令のスループットはその待ち時間よりもかなり短くなる可能性があります。
Jcc
の値は次のとおりです。
Latency Throughput
Jcc N/A 0.5
Jcc
の脚注は次のとおりです。
7)条件付きジャンプ命令の選択は、分岐の予測可能性を向上させるために、セクション3.4.1「分岐予測の最適化」の推奨事項に基づく必要があります。分岐が正常に予測されると、
jcc
の待ち時間は事実上ゼロになります。
そのため、Intelのドキュメントには、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
歴史的に(私達は1980年代と1990年代初頭を話しています)、これが真実である some アーキテクチャがありました。根本的な問題は、整数比較が整数減算によって本質的に実行されることです。これは次のような場合を引き起こす。
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つの命令で実行できます。これは、キャリービットが明確であるためです。 only この場合、つまり、
;; 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
したがって、いくつかのマシンでは、「より小さい」比較を使用して may save 1つのマシン命令 を使用します。これは、サブメガヘルツのプロセッサ速度と1:1のCPU対メモリ速度比の時代には関連がありましたが、今日ではほとんど無関係です。
内部整数型について話していると仮定すると、一方が他方よりも速くなる可能性はありません。それらは明らかに意味的に同一です。どちらもコンパイラにまったく同じことをするように依頼します。ひどく壊れたコンパイラだけが、これらのうちの1つに対して劣ったコードを生成するでしょう。
単純な整数型で<
が<=
より速いプラットフォームがある場合、コンパイラは always 定数で<=
を<
に変換する必要があります。 (そのプラットフォームにとって)単に悪いコンパイラにはならないだろうコンパイラ。
どちらも速くはないようです。コンパイラは、各条件に同じマシンコードを異なる値で生成します。
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
浮動小数点コードでは、現代のアーキテクチャでも<=の比較が実際には(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つの重要な点があります。これには、これら2つのビットを1つにまとめるための追加の命令(cror
- 条件レジスタのビット単位のOR)が必要です。 compare_loose
には5つの命令が必要ですが、compare_strict
には4つの命令が必要です。
コンパイラは2番目の関数を次のように最適化できると考えるかもしれません。
int compare_loose(double a, double b) { return ! (a > b); }
ただし、これはNaNを誤って処理します。 NaN1 <= NaN2
とNaN1 > NaN2
は両方ともfalseと評価される必要があります。
その名前のない本の著者は、a > 0
がa >= 1
より速く動くと読んでいて、それが普遍的に正しいと考えているかもしれません。
しかし、それは0
が関係しているからであり(CMP
はアーキテクチャによっては、例えばOR
に置き換えることができるため)、<
のためではありません。
少なくとも、これが真実であれば、コンパイラーはa <= bを!(a> b)に自明に最適化することができます。 。
彼らは同じ速度を持っています。ある特別なアーキテクチャでは彼/彼女が言ったことは正しいかもしれませんが、x86ファミリーでは少なくとも私はそれらが同じであることを知っています。これを実行するために、CPUは減算(a - b)を実行してからフラグレジスタのフラグをチェックします。そのレジスタの2 ビットはZF(0 フラグ)とSF(符号フラグ)と呼ばれ、それは1回のマスク操作でそれをするのでそれは1周期で行われます。
これはCがコンパイルされた基礎となるアーキテクチャに大きく依存します。いくつかのプロセッサおよびアーキテクチャは、異なるサイクル数で実行される、等しい、またはそれ以下、およびそのための明示的な命令を有することがある。
ただし、コンパイラはそれを回避して無関係にする可能性があるため、これはかなり珍しいことです。
アーキテクチャー、コンパイラー、および言語のほとんどの組み合わせでは、それほど速くはありません。
他の答えは x86 アーキテクチャに集中しています、そして、私は _ arm _ アーキテクチャ(あなたの例のアセンブラがそうであるように思われる)を生成されたコードに関して特にコメントするほど十分によくわかりません マイクロ最適化の例 which isは非常にアーキテクチャ固有であり、 最適化であるのと同じくらい反最適化である可能性が高い 。
そのようなものとして、私はこの種の マイクロ最適化 が最良のソフトウェア工学の実践よりもむしろ 貨物のカルト プログラミングの例であることを提案する。
おそらくこれが最適化されているsomeアーキテクチャーがありますが、その逆が真実であるかもしれない少なくとも1つのアーキテクチャーを知っています。由緒ある Transputer アーキテクチャは等しいと以上のマシンコード命令しか持っていなかったので、すべての比較はこれらのプリミティブから構築されなければなりませんでした。
それでも、ほとんどすべての場合において、コンパイラーは実際には比較が他のものよりも有利にならないように評価命令を順序付けることができます。最悪の場合、 オペランドスタック の上の2つの項目を交換するために逆の命令(REV)を追加する必要があるかもしれません。これは実行に1サイクルかかるシングルバイト命令だったので、可能な限り最小のオーバーヘッドでした。
このようなマイクロ最適化が最適化であるか最適化防止であるかは、使用しているアーキテクチャによって異なります。そのため、習慣をつけるのは通常悪い考えです。それ以外の場合は、アーキテクチャ固有の最適化手法を使用しないでください。そうでない場合は、本能的に使用することになります。これは、実際に読んでいる本が主張しているもののようです。
たとえ違いがあっても、違いに気付くことはできないはずです。それに加えて、実際には、条件を有効にするために追加のa + 1
またはa - 1
を行う必要があります。ただし、何らかの魔法の定数を使用するつもりがない限りは絶対に悪い習慣です。
余分な文字はわずかに遅いコード処理をもたらすので、あなたはその行がほとんどのスクリプト言語で正しいと言うことができます。しかし、一番の答えが指摘したように、それはC++では効果がないはずです。そして、スクリプト言語で行われていることはおそらく最適化についてそれほど心配していないでしょう。
この答えを書いたとき、私は一般的に<vs. <=についてのタイトルの質問だけを見ていました。定数a < 901
vs. a <= 900
の特定の例ではありませんでした。多くのコンパイラは、<
と<=
の間で変換することにより、常に定数の大きさを縮小します。 x86即値オペランドの-128..127の1バイトエンコーディングが短いためです。
ARM、特にAArch64の場合、即値としてエンコードできるかどうかは、狭いフィールドをWord内の任意の位置に回転できるかどうかに依存します。したがって、cmp w0, #0x00f000
はエンコード可能ですが、cmp w0, #0x00effff
はエンコードできない可能性があります。そのため、比較時とコンパイル時定数に対するmake-it-smallerルールは、AArch64には必ずしも適用されません。
ほとんどのマシンのアセンブリ言語では、<=
の比較には<
の比較と同じコストがかかります。これは、分岐、ブール値化して0/1整数を作成する、または分岐なしの選択操作(x86 CMOVなど)の述語として使用する場合に適用されます。他の回答は、質問のこの部分のみを扱っています。
しかし、この質問はC++演算子、オプティマイザへのinputについてです。通常、どちらも同等に効率的です。コンパイラは、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++)
...
コンパイラは、all可能な入力値のC++ソースの(定義済みかつ法的に観察可能な)動作を維持する方法でのみ最適化できます 、未定義の動作につながるものを除きます。
(単純なi <= size
でも問題が発生しますが、上限を計算することは、気にしないがコンパイラが考慮しなければならない入力に無限ループの可能性を誤って導入するより現実的な例だと思いました)
この場合、size=0
はupper_bound=UINT_MAX
につながり、i <= UINT_MAX
は常にtrueです。したがって、このループはsize=0
に対して無限であり、プログラマーがおそらくsize = 0を渡すつもりはないのに、コンパイラーはそれを尊重する必要があります。コンパイラーがこの関数を呼び出し側にインライン化して、size = 0が不可能であることを証明できれば、i < size
のように最適化できます。
if(!size) skip the loop;
do{...}while(--size);
のようなAsmは、i
の実際の値がループ内で必要ない場合、for( i<size )
ループを最適化するための通常効率的な方法の1つです( ループが常に「do ... while」スタイル(テールジャンプ)にコンパイルされるのはなぜですか? )。
しかし、それは無限ではありえません:size==0
で入力された場合、2 ^ n回の反復を取得します。 ( forループですべての符号なし整数を反復処理する Cを使用すると、ゼロを含むすべての符号なし整数でループを表現できますが、asmのようにキャリーフラグがなければ簡単ではありません。)
ループカウンターのラップアラウンドが可能になると、現代のコンパイラーはしばしば「あきらめ」、ほとんど積極的に最適化しません。
符号なし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整数を追加して自動ベクトル化し、XMMレジスタの要素で4つのi
値を並行して実行します。
# "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の代わりにadd eax,1
/jnz
がより効率的です。 2ではなく1 uop(sub/jccまたはcmp/jccのマクロ融合後)。両方のループの後のコードはEAXを無条件に書き込むため、ループカウンターの最終値を使用していません。
コンピューターを作成した人がブール論理で悪い場合のみ。あるべきではない。
すべての比較(>=
<=
>
<
)は同じ速度で実行できます。
すべての比較とは、単に減算(差)であり、それが正か負かを確認することです。
(msb
が設定されている場合、数値は負になります)
a >= b
を確認する方法Sub a-b >= 0
a-b
が正かどうかを確認します。
確認方法a <= b
? Sub 0 <= b-a
b-a
が正かどうかを確認します。
確認方法a < b
? Sub a-b < 0
a-b
が負であるかどうかを確認します。
確認方法a > b
? Sub 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
として計算しなかったでしょう。