web-dev-qa-db-ja.com

memcmp(a、b、4)がuint32比較に対してのみ最適化されるのはなぜですか?

このコードを考えます:

_#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

68
John Zwinck

リトルエンディアンプラットフォーム用のコードを生成する場合、4バイトのmemcmpを単一のDWORD比較と不等式に最適化することは無効です。

memcmpが個々のバイトを比較する場合、プラットフォームに関係なく、低アドレスのバイトから高アドレスのバイトに移動します。

memcmpがゼロを返すためには、4バイトすべてが同一でなければなりません。したがって、比較の順序は重要ではありません。したがって、結果の符号を無視するため、DWORD最適化は有効です。

ただし、memcmpが正の数を返す場合、バイトの順序が重要です。したがって、32ビットDWORD比較を使用して同じ比較を実装するには、特定のエンディアンが必要です。プラットフォームはビッグエンディアンである必要があります。そうしないと、比較の結果が不正確になります。

73
dasblinkenlight

ここでエンディアンネスが問題です。この入力を考慮してください:

a = 01 00 00 03
b = 02 00 00 02

これらの2つの配列を32ビット整数として処理して比較すると、aの方が大きいことがわかります(0x03000001> 0x02000002であるため)。ビッグエンディアンのマシンでは、このテストはおそらく期待どおりに機能します。

24

他の回答/コメントで説明したように、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*_が指しているものに依存します ストリクトエイリアスやアライメントが不明な場合は、memcpyabytesに入れてください。ほとんどのコンパイラは、小さな固定サイズ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_、brsiを含む質問。

_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の場合、実際のすべての作業のコストよりも高くなります。

13
Peter Cordes