-ve 値を取ることが期待されない整数の場合、unsigned int または int を使用できます。コンパイラの観点または純粋に CPU サイクルの観点から、 x86_64 に違いはありますか?
5 に答える
場合によります。それを使って何をしているかint
、また基礎となるハードウェアのプロパティに応じて、どちらの方向にも進む可能性があります。
わかりやすい例としてはunsigned int
、整数除算があります。C/C++ では、整数除算はzeroに向かって丸められることになっていますが、x86 でのマシン整数除算は負の無限大に向かって丸められます。また、整数除算 (シフトなど) のさまざまな「最適化された」置換も、通常、負の無限大に向かって丸められます。そのため、標準要件を満たすために、コンパイラは追加のマシン命令で符号付き整数除算の結果を調整する必要があります。符号なし整数除算の場合、この問題は発生しません。これが、通常、整数除算が符号付き型よりも符号なし型の方がはるかに高速に動作する理由です。
たとえば、次の単純な式を考えてみましょう
rand() / 2
MSVC コンパイラによってこの式用に生成されたコードは、通常、次のようになります。
call rand
cdq
sub eax,edx
sar eax,1
単一のシフト命令 ( sar
) の代わりに、ここでは多数の命令が表示されていることに注意してください。つまり、 2 つの追加命令 (および)sar
が先行しています。これらの追加の命令は、(C 言語の観点から) 「正しい」結果を強制的に生成するために除算を「調整」するためのものです。コンパイラは、値が常に正になることを認識していないため、これらの命令を常に無条件に生成する必要があることに注意してください。有用なことは何もしないため、CPU サイクルが浪費されます。cdq
sub
のコードを見ないでください
(unsigned) rand() / 2
それはただです
call rand
shr eax,1
この場合、1 回のシフトで問題が解決したため、天文学的に高速なコード (除算のみ) が得られました。
一方、整数演算と FPU 浮動小数点演算を混在させる場合、FPU 命令セットには符号付き整数値をロード/格納するための即時命令が含まれていますが、符号なし整数値に対する命令は含まれていないため、符号付き整数型の方が高速に動作する可能性があります。
これを説明するために、次の単純な関数を使用できます
double zero() { return rand(); }
生成されたコードは通常、非常に単純です。
call rand
mov dword ptr [esp],eax
fild dword ptr [esp]
しかし、関数を次のように変更すると
double zero() { return (unsigned) rand(); }
生成されたコードは次のように変わります
call rand
test eax,eax
mov dword ptr [esp],eax
fild dword ptr [esp]
jge zero+17h
fadd qword ptr [__real@41f0000000000000 (4020F8h)]
FPU 命令セットは符号なし整数型では機能しないため、このコードは著しく大きくなります。そのため、符号なし値をロードした後に追加の調整が必要になります (これが条件式のfadd
動作です)。
どちらの方法でも機能することを示すために使用できる他のコンテキストと例があります。繰り返しますが、それはすべて依存しています。しかし、一般的に、これらはすべて、プログラムのパフォーマンスの全体像には関係ありません。私は通常、符号なしの量を表すために符号なしの型を使用することを好みます。私のコードでは、整数型の 99% が符号なしです。しかし、パフォーマンスの向上のためではなく、純粋に概念的な理由でこれを行います。
コンパイラはオーバーフローの可能性を無視し、適切と思われる方法で演算を単純化/再配置できるため、ほとんどの場合、符号付き型は本質的に最適化可能です。一方、符号なしの型は本質的により安全です。結果が常に明確に定義されているためです (単純にそうあるべきだと思っていなくても)。
符号なし型の方が最適化に適している 1 つのケースは、除算/剰余を 2 の累乗で記述している場合です。符号なしの型の場合、これはビットシフトおよびビット単位の AND に直接変換されます。符号付きの型の場合、値が正であることがわかっていることをコンパイラが確立できない限り、負の数によるオフバイワンの問題を補うために追加のコードを生成する必要があります (C によれば、-3/2 は -1 ですが、代数的およびビットごとの演算では -2 です)。
ほぼ間違いなく違いはありませんが、コンパイラは、数サイクルを削減するために、型の符号性を操作することがありますが、正直なところ、全体的な変更はおそらく無視できるものです。
たとえば、 があり、次のようint x
に書きたいとします。
if(x >= 10 && x < 200) { /* ... */ }
あなた (あるいはコンパイラ) は、これを少し変換して、比較を 1 つ減らすことができます。
if((unsigned int)(x - 10) < 190) { /* ... */ }
int
これは2 の補数で表される仮定を行っているため、(x - 10)
が より小さい場合、 はとして見たときに巨大な値に0
なります。たとえば、典型的な x86 システムでは、テスト対象よりも明らかに大きいです。unsigned int
(unsigned int)-1 == 0xffffffff
190
これはせいぜいマイクロ最適化であり、コンパイラーに任せるのが最善です。代わりに、意味を表現するコードを記述し、遅すぎる場合はプロファイリングして、実際に賢くする必要がある場所を決定する必要があります。
ALU の観点からは、符号付きまたは符号なしの値を (またはその他の) 追加しても、どちらもビットのグループで表されるため、違いはありません。0100 + 1011
は常にです1111
が、それがまたはの場合は選択します。
@Markに同意します。他の人がコードを理解できるように、最適なデータ型を選択する必要があります。4 + (-5) = -1
4 + 11 = 15
CPUやコンパイラに関しては、それほど大きな違いはないと思います。考えられるケースの 1 つは、数値が決して負にならないことをコンパイラが認識できるようにし、コードを最適化する場合です。
ただし、人間がコードを読んで、問題の変数のドメインを知るのに役立ちます。