であるif( a < 901 )
よりも速くif( a <= 900 )
。
この単純な例とまったく同じではありませんが、ループの複雑なコードではパフォーマンスにわずかな変更があります。これが真実である場合に備えて、これは生成されたマシンコードで何かをしなければならないと思います。
<
はタイピングより2倍高速です<=
。
であるif( a < 901 )
よりも速くif( a <= 900 )
。
この単純な例とまったく同じではありませんが、ループの複雑なコードではパフォーマンスにわずかな変更があります。これが真実である場合に備えて、これは生成されたマシンコードで何かをしなければならないと思います。
<
はタイピングより2倍高速です<=
。
回答:
いいえ、ほとんどのアーキテクチャでは高速にはなりません。指定しませんでしたが、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
命令。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
jg
にjnle
は同じ指示です7F
:-)
歴史的に(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とメモリの速度比の時代には関係がありましたが、今日ではほとんど関係ありません。
jge
は、ゼロフラグと符号/キャリーフラグの両方をテストするのような命令を実装します。
<=
テストする1つの命令でテストを実装できますnot <
(と同等>=
)。これは、<=
オペランドを交換した場合に必要ですcmp B,A; bcs addr
。それが、このテストがIntelによって省略された理由であり、彼らはそれを冗長であると考え、そのときに冗長な命令を用意することができませんでした:-)
内部整数型について話していると仮定すると、一方が他方よりも高速になる可能性はありません。それらは明らかに意味的に同一です。どちらもコンパイラにまったく同じことをするように要求します。ひどく壊れたコンパイラだけが、これらのいずれかに対して劣ったコードを生成します。
単純な整数型<
よりも高速なプラットフォームがあった場合<=
、コンパイラは常に定数に変換<=
する必要があり<
ます。できなかったコンパイラは、そのプラットフォーム用の悪いコンパイラになるだけです。
<
も<=
速度もありません。あなたは、彼らは一般的に、すでになどデッドコードの最適化、末尾呼び出しの最適化、ループ巻き上げ(および機会に、アンロール)、様々なループの自動並列化を行うことを考えるとこれはコンパイラのための非常に単純な最適化です... なぜ無駄時間は時期尚早の最適化を考え?プロトタイプを実行し、プロファイリングして最も重要な最適化がどこにあるかを特定し、それらの最適化を重要度の順に実行し、進行状況を測定する方法に沿って再度プロファイルします...
(a < C)
への変換が命令セットでエンコードするのをより困難にする場合などです。たとえば、命令セットは比較で-127から128までの符号付き定数をコンパクトな形式で表すことができますが、その範囲外の定数は、より長い、遅いエンコーディング、または別の命令を使用してロードする必要があります。したがって、のような比較では、簡単な変換は行われない場合があります。(a <= C-1)
C
C
(a < -127)
a > 127
していませんa > 128
。必要なものを使用します。同じ真理値表を持っているため、異なるエンコーディングや異なる命令を必要としないと比較a > 127
しa >= 128
ています。一方のエンコーディングは、他方のエンコーディングと同じです。
<=
する必要があるプラットフォームがある場合」という一般的な方法で私はあなたの声明に応えていました<
。私の知る限り、その変換には定数の変更が含まれます。例えば、速いのでa <= 42
コンパイルされます。いくつかのエッジケースでは、新しい定数がより多くのまたはより遅い命令を必要とする可能性があるため、このような変換は実りがありません。もちろん、同等です。コンパイラーは両方の形式を(同じ)最速の方法でエンコードする必要がありますが、それは私が言ったことと矛盾しません。a < 43
<
a > 127
a >= 128
どちらも速くないことがわかります。コンパイラーは、各条件で同じ値の異なるマシンコードを生成します。
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
if(a <=900)
、まったく同じasmを生成することを示す必要があると思います:)
浮動小数点コードの場合、<=の比較は、現代のアーキテクチャでも実際には(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に評価されます。
fucomip
ZFとCFを設定します。
cr
は x86のようなフラグZF
と同等CF
です。(ただし、CRの方が柔軟性があります。)ポスターが話しているのは、結果をGPRに移動することです。これは、PowerPCで2つの命令を受け取りますが、x86には条件付きの移動命令があります。
おそらく、その無名の本の著者はa > 0
、a >= 1
それが普遍的に真実であると考えています。
しかし、それはa 0
が関係しているため(CMP
アーキテクチャによっては、たとえばで置き換えられるOR
ため)、のためではありません<
。
(a >= 1)
よりも遅い実行するために(a > 0)
、前者は自明オプティマイザによって、後者に変換することができるので、..
少なくとも、これが当てはまる場合、コンパイラーはa <= bを!(a> b)に自明に最適化できます。そのため、比較自体が実際に遅い場合でも、最も単純なコンパイラー以外は違いに気付かないでしょう。 。
NOT
は、他の命令(je
vs jne
)によって作成されます
これは、Cがコンパイルされる基本的なアーキテクチャに大きく依存します。一部のプロセッサとアーキテクチャには、異なるサイクル数で実行される以下の明示的な命令がある場合があります。
しかし、コンパイラーがそれを回避して無関係にすることができるので、それはかなり珍しいでしょう。
アーキテクチャー、コンパイラー、および言語のほとんどの組み合わせでは、それは速くはありません。
他の回答はx86アーキテクチャに集中しており、生成されたコードについて具体的にコメントできるARMアーキテクチャ(サンプルアセンブラのようです)はよくわかりませんが、これは非常にアーキテクチャであるマイクロ最適化の例です具体的で、最適化よりも反最適化の可能性があります。
そのため、このようなマイクロ最適化は、ソフトウェアエンジニアリングのベストプラクティスではなく、カーゴカルトプログラミングの例であることをお勧めします。
これが最適化であるいくつかのアーキテクチャがおそらくありますが、私は逆が真であるかもしれない少なくとも1つのアーキテクチャを知っています。由緒あるトランスピューターアーキテクチャには、以上のマシンコード命令しかなかったため、すべての比較はこれらのプリミティブから構築する必要がありました。
それでも、ほとんどすべてのケースで、コンパイラーは、実際には比較が他のどの製品よりも優れているような方法で評価命令を順序付けることができます。ただし、最悪の場合、オペランドスタックの上位2項目をスワップするために、リバース命令(REV)を追加する必要がある場合があります。これは、実行に1サイクルかかる1バイトの命令だったので、オーバーヘッドは可能な限り最小でした。
このようなマイクロ最適化が最適化であるか非最適化であるかは、使用している特定のアーキテクチャに依存するため、通常、アーキテクチャ固有のマイクロ最適化を使用する習慣をつけることは悪い考えです。それを行うことが不適切な場合に使用してください。これは、あなたが読んでいる本がまさに主張しているもののように見えます。
違いがあっても気付かないはずです。さらに、実際には、いくつかの魔法の定数を使用する場合を除いて、追加の条件を実行するa + 1
かa - 1
、条件を立てる必要があります。これは、決して非常に悪い習慣です。
余分な文字はコード処理がわずかに遅くなるため、ほとんどのスクリプト言語では行が正しいと言えます。ただし、トップの回答が指摘したように、C ++では効果がなく、スクリプト言語で行われていることは、おそらく最適化についてはそれほど心配されていません。
この回答を書いたとき、私は一般的に<vs. <=に関するタイトルの質問だけを見ていました。定数a < 901
vs.の具体的な例ではありませんでした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_MAX
、i <= 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のようにキャリーフラグがないと簡単ではありません。)
ループカウンターのラップアラウンドが可能である場合、最近のコンパイラーは、しばしば「あきらめ」、ほとんど積極的に最適化しません。
符号なし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を書き込むため、ループカウンターの最終値を使用していません。
<
または<=
。ただし、ZFが設定されている場合(ecx == 0)、またはCFが設定されている場合(EAX == 1のビット3)、test ecx,ecx
/ bt eax, 3
/ jbe
はジャンプします。これにより、ほとんどのCPUで部分的なフラグストールが発生します。フラグを書き込む最後の命令から来る。Sandybridgeファミリでは、実際にはストールせず、マージしているuopを挿入する必要があるだけです。 cmp
/ test
すべてのフラグを書き込みますが、bt
ZFは変更しません。felixcloutier.com/x86/bt
コンピュータを作成した人々がブール論理に悪ければ。彼らがすべきではないもの。
すべての比較(>=
<=
>
<
)を同じ速度で実行できます。
すべての比較が何であるかは、単に減算(違い)であり、それが正か負かを確認することです。
(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