私はこのプログラムをx64コンピューターでコンパイルしようとしました:
#include <cstring>
int main(int argc, char* argv[])
{
return ::std::strcmp(argv[0],
"really really really really really really really really really"
"really really really really really really really really really"
"really really really really really really really really really"
"really really really really really really really really really"
"really really really really really really really really really"
"really really really really really really really really really"
"really really really really really really really really really"
"really really really really really really really really really"
"really really really really really really really long string"
);
}
私はそれを次のようにコンパイルしました:
g++ -std=c++11 -msse2 -O3 -g a.cpp -o a
しかし、結果の分解は次のようになります。
0x0000000000400480 <+0>: mov (%rsi),%rsi
0x0000000000400483 <+3>: mov $0x400628,%edi
0x0000000000400488 <+8>: mov $0x22d,%ecx
0x000000000040048d <+13>: repz cmpsb %es:(%rdi),%ds:(%rsi)
0x000000000040048f <+15>: seta %al
0x0000000000400492 <+18>: setb %dl
0x0000000000400495 <+21>: sub %edx,%eax
0x0000000000400497 <+23>: movsbl %al,%eax
0x000000000040049a <+26>: retq
SIMDが使用されないのはなぜですか?たとえば、一度に16文字を比較することが考えられます。独自のSIMD strcmp
を作成する必要がありますか、それとも何らかの理由で無意味なアイデアですか?
SSE2実装では、文字列の最後でメモリアクセスが発生しないことをコンパイラがどのように確認する必要がありますか?最初に長さを知る必要があり、これには文字列をスキャンして終了ゼロバイトを探す必要があります。
文字列の長さをスキャンする場合、strcmp関数のほとんどの作業はすでに完了しています。したがって、SSE2を使用するメリットはありません。
ただし、インテルはSSE4.2命令セットに文字列処理の命令を追加しました。これらは終端のゼロバイト問題を処理します。彼らの素晴らしい記事については、このブログ投稿を読んでください:
この場合のGCCは組み込みのstrcmp
を使用しています。 glibcのバージョンを使用する場合は、-fno-builtin
を使用します。ただし、GCCの組み込みバージョンのstrcmp
またはglibcのstrcmp
の実装が効率的であると想定しないでください。 GCCの組み込みmemcpy
とglibcのmemcpy
は効率が悪い であることを経験から知っています。
Agner Fog's asmlib をご覧になることをお勧めします。彼はアセンブリーのいくつかの標準ライブラリー関数を最適化しました。ファイルstrcmp64.asm
を参照してください。これには2つのバージョンがあります。SSE4.2のないCPUの汎用バージョンとSSE4.2のあるCPUのバージョンです。これはSSE4.2バージョンのメインループです
compareloop:
add rax, 16 ; increment offset
movdqu xmm1, [rs1+rax] ; read 16 bytes of string 1
pcmpistri xmm1, [rs2+rax], 00011000B ; unsigned bytes, equal each, invert. returns index in ecx
jnbe compareloop ; jump if not carry flag and not zero flag
一般的なバージョンでは、彼は書いています
これは非常にシンプルなソリューションです。 SSE2や複雑なものを使用してもあまりメリットはありません
ジェネリックバージョンのメインループは次のとおりです。
_compareloop:
mov al, [ss1]
cmp al, [ss2]
jne _notequal
test al, al
jz _equal
inc ss1
inc ss2
jmp _compareloop
GCCの組み込みstrcmp
、GLIBCのstrcmp
、およびasmlib strcmp
のパフォーマンスを比較します。逆アセンブルを見て、組み込みコードを取得していることを確認する必要があります。たとえば、GCCのmemcpy
は、8192より大きいサイズの組み込みバージョンを使用しません。
編集:文字列の長さに関して、AgnerのSSE4.2バージョンは、文字列の長さを超えて最大15バイトを読み取ります。何も書かれていないため、これが問題になることはめったにないと彼は主張する。スタック割り当て配列の場合は問題ありません。静的に割り当てられた配列の場合、メモリページの境界で問題になる可能性があります。これを回避するには、.dataセクションの後に.bssセクションに16バイトを追加します。詳細については、asmlibのマニュアルのセクション1.7文字列の説明と安全上の注意を参照してください。
Cの標準ライブラリが設計されたとき、大量のデータを処理するときに最も効率的だったstring.h
メソッドの実装は、少量の場合はかなり効率的であり、その逆も同様です。一部の文字列比較シナリオでは、SIMD命令の高度な使用により「単純な実装」よりも優れたパフォーマンスが得られる場合がありますが、実際の多くのシナリオでは、比較される文字列は最初の数文字が異なります。このような状況では、単純な実装は、「より高度な」アプローチが比較の実行方法を決定するのに費やすよりも短い時間で結果をもたらす可能性があります。 SIMDコードが一度に16バイトを処理し、不一致または文字列の終わりの条件が検出されたときに停止できる場合でも、スキャンされた最後の16文字に対して単純なアプローチを使用するのと同等の追加作業を行う必要があることに注意してください。 。 16バイトのグループが多数一致する場合、それらをすばやくスキャンできるとパフォーマンスが向上する可能性があります。ただし、最初の16バイトが一致しない場合は、文字単位の比較から始める方が効率的です。
ちなみに、「素朴な」アプローチのもう1つの潜在的な利点は、ヘッダーの一部としてインラインで定義できることです(またはコンパイラーは、それ自体について特別な「知識」を持っていると見なす場合があります)。考慮してください:
int strcmp(char *p1, char *p2)
{
int idx=0,t1,t2;
do
{
t1=*p1; t2=*p2;
if (t1 != t2)
{
if (t1 > t2) return 1;
return -1;
}
if (!t1)
return 0;
p1++; p2++;
} while(1);
}
...invoked as:
if (strcmp(p1,p2) > 0) action1();
if (strcmp(p3,p4) != 0) action2();
メソッドはインライン化するには少し大きくなりますが、インライン化を行うと、最初のケースではコンパイラーはコードを削除して戻り値がゼロより大きいかどうかを確認し、2番目のケースではコードを削除してt1はt2より大きい。メソッドが間接ジャンプを介してディスパッチされた場合、そのような最適化は不可能です。
それは実装に依存します。 MacOS Xでは、memcpy、memmove、memsetなどの関数には、使用しているハードウェアに応じて最適化された実装があります(同じ呼び出しは、プロセッサーに応じて異なるコードを実行し、ブート時に設定されます)。これらの実装はSIMDを使用しますおよび大量(メガバイト)の場合は、かなり凝ったトリックを使用してキャッシュの使用を最適化します。私が知る限り、strcpyとstrcmpには何もありません。
この種のコードを使用するようにC++標準ライブラリを説得することは困難です。
SIMDバージョンのライブラリー関数には、計算量が非常に少ないだけでは意味がないと思います。 strcmp
、memcpy
などの関数は、実際にはCPU速度ではなくメモリ帯域幅によって制限されていると思います。
strcmp
のSSE2バージョンを作成することは、私にとって興味深い課題でした。
コードの膨張のため、コンパイラ組み込み関数はあまり好きではないので、自動ベクトル化アプローチを選択することにしました。私のアプローチはテンプレートに基づいており、SIMDレジスタをさまざまなサイズの単語の配列として近似します。
自動ベクトル化の実装を記述して、GCCおよびMSVC++コンパイラでテストしてみました。
だから、私が学んだことは:
1。 GCCの自動ベクトライザーは素晴らしい(素晴らしい?)
2。 MSVCの自動ベクトライザーはGCCよりも悪い(私のパッキング関数をベクトル化しない)
3。すべてのコンパイラはPMOVMSKB命令の生成を拒否しました、それは本当に悲しいです
結果:
SSE2自動ベクトル化により、オンラインGCCでコンパイルされたバージョンは最大40%向上します。 BulldozerアーキテクチャのCPUを搭載したWindowsマシンでは、自動ベクトル化されたコードはオンラインコンパイラよりも高速で、結果はstrcmp
のネイティブ実装と一致します。ただし、このアイデアの最も良い点は、少なくともARM&X86では、同じコードを任意のSIMD命令セット用にコンパイルできることです。
注意:
コンパイラがPMOVMSKB命令を生成する方法を誰かが見つけた場合、全体的なパフォーマンスが大幅に向上するはずです。
GCCのコマンドラインオプション:-std = c ++ 11 -O2 -m64 -mfpmath = sse -march = native -ftree-vectorize -msse2 -march = native -Wall -Wextra
リンク:
Coliruオンラインコンパイラによってコンパイルされたソースコード
アセンブリ+ソースコード(コンパイラエクスプローラ)
@PeterCordes、助けてくれてありがとう。
AVX 2.0は実際には高速です
編集:レジスタとIPCに関連しています
1つの大きな命令に依存する代わりに、32バイトの16レジスタを備えた大量のSIMD命令を使用できます。UTF16では、265文字でプレイできます。
数年でavx512でそれを倍増します!
AVX命令にも高いスループットがあります。
このブログによると: https://blog.cloudflare.com/improving-picohttpparser-further-with-avx2/
今日、最新のHaswellプロセッサでは、強力なAVX2命令を使用できます。 AVX2命令は32バイトで動作し、ほとんどのブール/ロジック命令は命令あたり0.5サイクルのスループットで実行されます。これは、1つのPCMPESTRIを実行するのと同じ時間で約22のAVX2命令を実行できることを意味します。それを試してみませんか?
Edit 2.0SSE/AVXユニットはパワーゲーティングされており、SSEおよび/または通常のAVX命令との混合には、 strcmp命令では不要な、パフォーマンスの低下を伴うコンテキストスイッチ。