[解決済み] なぜGCCはほとんど同じCコードに対して根本的に異なるアセンブリを生成するのでしょうか?
質問
最適化された
ftol
関数に非常に奇妙な挙動を発見しました。
GCC 4.6.1
. まず、コードをお見せしましょう(わかりやすいように、差分をマークしました)。
fast_trunc_one、C:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent; /* diff */
} else {
r = mantissa >> exponent; /* diff */
}
return (r ^ -sign) + sign; /* diff */
}
fast_trunc_two、C:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent) ^ -sign; /* diff */
} else {
r = (mantissa >> exponent) ^ -sign; /* diff */
}
return r + sign; /* diff */
}
同じに見えるでしょ?しかし、GCCはそうは思いません。でコンパイルした後
gcc -O3 -S -Wall -o test.s test.c
これがアセンブリの出力です。
fast_trunc_one が、生成されました。
_fast_trunc_one:
LFB0:
.cfi_startproc
movl 4(%esp), %eax
movl $150, %ecx
movl %eax, %edx
andl $8388607, %edx
sarl $23, %eax
orl $8388608, %edx
andl $255, %eax
subl %eax, %ecx
movl %edx, %eax
sarl %cl, %eax
testl %ecx, %ecx
js L5
rep
ret
.p2align 4,,7
L5:
negl %ecx
movl %edx, %eax
sall %cl, %eax
ret
.cfi_endproc
fast_trunc_two、生成されました。
_fast_trunc_two:
LFB1:
.cfi_startproc
pushl %ebx
.cfi_def_cfa_offset 8
.cfi_offset 3, -8
movl 8(%esp), %eax
movl $150, %ecx
movl %eax, %ebx
movl %eax, %edx
sarl $23, %ebx
andl $8388607, %edx
andl $255, %ebx
orl $8388608, %edx
andl $-2147483648, %eax
subl %ebx, %ecx
js L9
sarl %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_remember_state
.cfi_def_cfa_offset 4
.cfi_restore 3
ret
.p2align 4,,7
L9:
.cfi_restore_state
negl %ecx
sall %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_restore 3
.cfi_def_cfa_offset 4
ret
.cfi_endproc
それは
極端な
の違いがあります。これは実際にプロフィールにも表れています。
fast_trunc_one
は、約30%高速化されています。
fast_trunc_two
. さて、私の疑問は、何が原因なのか、ということです。
解決方法は?
OPの編集と同期するように更新
コードをいじってみて、最初のケースでGCCがどのように最適化するかを確認することができました。
なぜこれほどまでに違うのかを理解する前に、まずGCCがどのように最適化するのかを理解する必要があります。
fast_trunc_one()
.
信じるか信じないかは別として。
fast_trunc_one()
がこれに最適化されている。
int fast_trunc_one(int i) {
int mantissa, exponent;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
if (exponent < 0) {
return (mantissa << -exponent); /* diff */
} else {
return (mantissa >> exponent); /* diff */
}
}
これは、元の
fast_trunc_one()
- レジスタの名前も何もかも
がないことに注意してください。
xor
のアセンブリにある
fast_trunc_one()
. それでわかったんだ。
どうですか?
ステップ1:
sign = -sign
まず、「Subject」内の
sign
という変数があります。このため
sign = i & 0x80000000;
が取り得る値は2つだけです。
sign
を取ることができる。
-
sign = 0
-
sign = 0x80000000
では、どちらの場合もそうだと認識してください。
sign == -sign
. したがって、元のコードをこう変えると。
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent;
} else {
r = mantissa >> exponent;
}
return (r ^ sign) + sign;
}
と全く同じアセンブリが生成されます。
fast_trunc_one()
. アセンブリは割愛しますが、レジスタ名など全く同じです。
ステップ2:
数学的な還元を行う。
x + (y ^ x) = y
sign
は2つの値のうちどちらか一方しか取ることができません。
0
または
0x80000000
.
-
いつ
x = 0
であればx + (y ^ x) = y
が成立すれば、トリビアルが成立する。 -
による足し算・引き算
0x80000000
は同じです。符号ビットを反転させるのです。したがってx + (y ^ x) = y
も成立します。x = 0x80000000
.
したがって
x + (y ^ x)
は次のようになります。
y
. そして、コードはこのように単純化されます。
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent);
} else {
r = (mantissa >> exponent);
}
return r;
}
この場合も、レジスタ名など、まったく同じアセンブリにコンパイルされます。
この上のバージョンは最終的にこのように縮小されます。
int fast_trunc_one(int i) {
int mantissa, exponent;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
if (exponent < 0) {
return (mantissa << -exponent); /* diff */
} else {
return (mantissa >> exponent); /* diff */
}
}
これは、GCCがアセンブリに生成するものとほぼ同じです。
では、なぜコンパイラは以下のような最適化をしないのでしょうか?
fast_trunc_two()
を同じものにするのでしょうか?
の重要な部分は
fast_trunc_one()
は
x + (y ^ x) = y
の最適化を行います。で
fast_trunc_two()
は
x + (y ^ x)
の式がブランチをまたいで分割されています。
この最適化を行わないようにGCCを混乱させるのに十分かもしれないと思います。(それは
^ -sign
をブランチから取り出し、それをマージして
r + sign
を最後に追加します)。
例えば、これは、以下のものと同じアセンブリを生成します。
fast_trunc_one()
:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = ((mantissa << -exponent) ^ -sign) + sign; /* diff */
} else {
r = ((mantissa >> exponent) ^ -sign) + sign; /* diff */
}
return r; /* diff */
}
関連
-
libc++abi.dylib: NSException タイプの捕捉されない例外で終了するエラー
-
コンパイルエラー:可変長オブジェクトが初期化されていない可能性があります。
-
[解決済み] Cプリプロセッサはなぜ "linux "という単語を定数 "1 "と解釈するのですか?
-
[解決済み] Collatz予想の検証を行うC++のコードは、なぜ手書きのアセンブリよりも高速に動作するのでしょうか?
-
[解決済み] C言語でのブーリアン値の使用
-
[解決済み] プログラム終了前にmallocの後にfreeをしないと本当に何が起こるのか?
-
[解決済み] C言語でファイルが存在するかどうかを確認する最も良い方法は何ですか?
-
[解決済み] C言語のi++と++iに性能差はあるのでしょうか?
-
[解決済み] なぜGCCは、速度の代わりにサイズに最適化すると、15-20%速いコードを生成するのですか?
-
[解決済み] 2次元配列の反復処理において、ループの順序がパフォーマンスに影響するのはなぜですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[C] レポートエラー 代入の左オペランドとしてlvalueが必要
-
Solve Dev-c++ [エラー] 'for' ループの初期宣言は、C99 または C11 モードでのみ許可されます。
-
[解決済み] Xcode - 警告。C99 では関数の暗黙の宣言は無効です。
-
[解決済み] c または c++ 用のシンプルな 2 次元クロスプラットフォームグラフィックスライブラリ?[クローズド]
-
[解決済み] C言語のコードで「:-!」とは何ですか?
-
[解決済み] 難読化Cコードコンテスト2006。sykes2.cの解説をお願いします。
-
[解決済み] C言語でファイルが存在するかどうかを確認する最も良い方法は何ですか?
-
[解決済み] C言語のi++と++iに性能差はあるのでしょうか?
-
[解決済み] アセンブリがCより速いのはどんなとき?[クローズド]
-
[解決済み] なぜ16進数には0xがつくのですか?