このコードを考えます:
_#include <string.h>
int equal4(const char* a, const char* b)
{
return memcmp(a, b, 4) == 0;
}
int less4(const char* a, const char* b)
{
return memcmp(a, b, 4) < 0;
}
_
X86_64上のGCC 7は、最初のケースの最適化を導入しました(Clangは長い間それを行ってきました):
_ mov eax, DWORD PTR [rsi]
cmp DWORD PTR [rdi], eax
sete al
movzx eax, al
_
しかし、2番目のケースではmemcmp()
を呼び出します:
_ sub rsp, 8
mov edx, 4
call memcmp
add rsp, 8
shr eax, 31
_
同様の最適化を2番目のケースに適用できますか?これに最適なアセンブリは何ですか?また、GCCまたはClangによって実行されていない明確な理由はありますか?
GodboltのCompiler Explorerでそれを参照してください: https://godbolt.org/g/jv8fcf
リトルエンディアンプラットフォーム用のコードを生成する場合、4バイトのmemcmp
を単一のDWORD比較と不等式に最適化することは無効です。
memcmp
が個々のバイトを比較する場合、プラットフォームに関係なく、低アドレスのバイトから高アドレスのバイトに移動します。
memcmp
がゼロを返すためには、4バイトすべてが同一でなければなりません。したがって、比較の順序は重要ではありません。したがって、結果の符号を無視するため、DWORD最適化は有効です。
ただし、memcmp
が正の数を返す場合、バイトの順序が重要です。したがって、32ビットDWORD比較を使用して同じ比較を実装するには、特定のエンディアンが必要です。プラットフォームはビッグエンディアンである必要があります。そうしないと、比較の結果が不正確になります。
ここでエンディアンネスが問題です。この入力を考慮してください:
a = 01 00 00 03
b = 02 00 00 02
これらの2つの配列を32ビット整数として処理して比較すると、a
の方が大きいことがわかります(0x03000001> 0x02000002であるため)。ビッグエンディアンのマシンでは、このテストはおそらく期待どおりに機能します。
他の回答/コメントで説明したように、memcmp(a,b,4) < 0
を使用することは、ビッグエンディアン整数間のunsigned
比較と同等です。リトルエンディアンx86で_== 0
_ほど効率的にインライン化できませんでした。
さらに重要なことは、gcc7/8のこの動作の現在のバージョン memcmp() == 0
または_!= 0
_ のみを探します。 _<
_または_>
_の場合と同じくらい効率的にインライン化できるビッグエンディアンターゲットでも、gccはそれを行いません。 (Godboltの最新のビッグエンディアンコンパイラはPowerPC 64 gcc6.3、およびMIPS/MIPS64 gcc5.4です。mips
はビッグエンディアンMIPS、mipsel
はリトルエンディアンMIPSです。)これは将来のgccで、a = __builtin_assume_align(a, 4)
を使用して、非x86での非整列ロードのパフォーマンス/正確性についてgccが心配する必要がないようにします。 (または、単に_const int32_t*
_の代わりに_const char*
_を使用します。)
EQ/NE以外の場合にgccがmemcmp
をインライン化することを学習した場合、または、ヒューリスティックが余分なコードサイズが価値があるとgccがリトルエンディアンx86でそれを行う場合があります。例えば _-fprofile-use
_ (プロファイルに基づく最適化)でコンパイルすると、ホットループになります。
この場合にコンパイラーに良い仕事をさせたい場合、おそらく_uint32_t
_に割り当て、次のようなエンディアン変換関数を使用する必要がありますntohl
。ただし、実際にインライン化できるものを選択してください。どうやら Windowsには、DLL call にコンパイルされるntohl
があります。ポータブルエンディアンのものについては、その質問に関する他の回答を参照してください。 _portable_endian.h
_ に対する誰かの不完全な試み、そしてこれ それのフォーク 。私はしばらくの間バージョンに取り組んでいましたが、それを終了/テストしたり、投稿したりしませんでした。
ポインターのキャストは、未定義の動作である可能性があります バイトの書き込み方法と_char*
_が指しているものに依存します ストリクトエイリアスやアライメントが不明な場合は、memcpy
をabytes
に入れてください。ほとんどのコンパイラは、小さな固定サイズmemcpy
を最適化するのに適しています。
_// I know the question just wonders why gcc does what it does,
// not asking for how to write it differently.
// Beware of alignment performance or even fault issues outside of x86.
#include <endian.h>
#include <stdint.h>
int equal4_optim(const char* a, const char* b) {
uint32_t abytes = *(const uint32_t*)a;
uint32_t bbytes = *(const uint32_t*)b;
return abytes == bbytes;
}
int less4_optim(const char* a, const char* b) {
uint32_t a_native = be32toh(*(const uint32_t*)a);
uint32_t b_native = be32toh(*(const uint32_t*)b);
return a_native < b_native;
}
_
私はGodboltをチェックしました で、特に古いgccであっても、ビッグエンディアンプラットフォームで、効率的なコード(基本的には下のasmで書いたものと同じ)にコンパイルします。また、memcmp
をインライン化するICC17よりもはるかに優れたコードを作成しますが、バイト比較ループにのみ(_== 0
_の場合でも)。
この手作りのシーケンスは、less4()
の最適な実装であると思います(x86-64 SystemV呼び出し規約では、 rdi
に_const char *a
_、b
にrsi
を含む質問。
_less4:
mov edi, [rdi]
mov esi, [rsi]
bswap edi
bswap esi
# data loaded and byte-swapped to native unsigned integers
xor eax,eax # solves the same problem as gcc's movzx, see below
cmp edi, esi
setb al # eax=1 if *a was Below(unsigned) *b, else 0
ret
_
これらはすべて、K8およびCore2以降のIntelおよびAMD CPUのシングルuop命令です( http://agner.org/optimize/ )。
両方のオペランドをbswapする必要があるため、_== 0
_の場合に比べてコードサイズのコストが余分にかかります。cmp
のメモリオペランドにロードの1つを折り畳むことはできません。 (コードサイズを節約し、マイクロフュージョンのおかげでuopsのおかげです。)これは、2つの余分なbswap
命令の上にあります。
movbe
をサポートするCPUでは、コードサイズを節約できます:_movbe ecx, [rsi]
_は負荷+ bswapです。 Haswellでは2 uopなので、おそらく_mov ecx, [rsi]
_/_bswap ecx
_と同じuopにデコードされます。 Atom/Silvermontでは、ロードポートで正しく処理されるため、uopが少なくなり、コードサイズが小さくなります。
Xor/cmp/setcc(clangが使用する)がcmp/setcc/movzx(gccの典型)よりも優れている理由については、 xor-zeroing回答のsetcc
部分 を参照してください。
これが結果に分岐するコードにインライン化する通常の場合、 setcc + zero-extendは jcc に置き換えられます。コンパイラーは、レジスターにブール戻り値を作成することを最適化します。 これはインライン化のもう1つの利点です。ライブラリmemcmp
は、呼び出し側がテストする整数ブール値戻り値を作成する必要があります x86 ABI /呼び出し規約では、フラグでブール条件を返すことができます。 (それを行うx86以外の呼び出し規約も知りません)。ほとんどのライブラリmemcmp
実装では、長さに応じて戦略を選択することで、また場合によってはアライメントチェックで大きなオーバーヘッドが発生します。それはかなり安いかもしれませんが、サイズ4の場合、実際のすべての作業のコストよりも高くなります。