memcpy実装の歴史とパフォーマンスについて

void * memcpy(void * destination、const void * source、size_t num);

複雑なように見えますか? そして、この関数の実装に関する全体のストーリーを書くことができます。



お気に入りの作業ツールであるVtune XEプロファイラーのウィンドウを見ると、メモリのコピーにかなりの時間がかかっていることに再び気づいたことがよくあります。 これは通常、次のように記述されています。libgcc/ [g] libc / kernel memcpy-XX%で費やされたクロック刻み。



これがおそらくmemcpyが頻繁に対応した理由です。たとえば、同様のスレッドlkmlによく登場します 。 (多くの場合、実装はソート専用です)。 メモリをコピーするための多くのオプションとアルゴリズムがあるソートとは対照的に、すべてがシンプルに思えます。 実際、パフォーマンスではなく正確さについて話しても、オプションは可能です。 (これを確認して-Linus TorvaldsとUlrich Drapperの参加による壮大なバグの議論 )。



8086の時代、つまり34年前に、memcpyの実装内には次のコードがありました。

mov [E] SI、src

mov [E] DI、ptr_dst

mov [E] CX、len

担当者movsb

(簡単にするため、以下ではすべてのチェックなどを省略しています)



それから何が変わったのですか? 単一の画像ではなく、katアセンブリコードの下。



Agner Fogh Optimizing Assemblyの古典的な作品は、memcpyのパフォーマンスのほとんどの側面についての良い(しかしあまり詳細ではない)説明を持っています。



90年代半ばに、プログラマーは、新しい命令が常にインターネットを高速化するわけではないが、拡張SIMDレジスタを使用して、REP MOVSよりも速くメモリをコピーできることを発見しました。



最初に、MMxレジスタが中間ストレージとして使用され、次にXMMが使用されました。 今後は、YMMがそれを取得できなかったと言います。

movups XMM [0-4]、[src](x4、フルキャッシュライン)

movups [dst]、XMM [0-4]



次に、メモリへの[un]整列読み取り、[alignment]、および[un]整列書き込みのさまざまな組み合わせが追加されました。最良の場合(SSE4.1)のようなものです

mov [nt] dqa XMM2、[src + i * 2]

mov [nt] dqa XMM1、[src + i * 2 + 1]

movdqa XMM1、XMM0

movdqa XMM0、XMM3

palignr XMM3、XMM2、シフト

palignr XMM2、XMM1、シフト

mov [nt] dqa [dst + i * 2]、XMM2

mov [nt] dqa [dst + i * 2 + 1]、XMM3

わずかな難点は、アライメント命令が直前のオペランド(シフト)のみで存在することです。これにより、コードが肥大化します(glibcを参照)。 ところで、Nehalemアーキテクチャから始めて、上記のコードが対抗する不均衡なメモリアクセスは、もは​​や無料ではありませんが、ブレーキの最も重要な原因ではなくなりました。



したがって、memcpyの実装がいくつか登場しました。それぞれの実装は、一部のプロセッサでは高速ですが、残りのプロセッサでは低速でした。 しばらくして、いくつかのオプション、および呼び出すオプションを選択するコードがglibcに組み込まれました。 おそらくCLRおよびJVM環境でも、System.arraycopyの効果的な実装をオンザフライで選択できます。 glibcとは異なり、このようなSSEコードは単にカーネルに偽装することはできません。 そこでさらに興味深いのは、SIMDレジスタを保存する必要があることですが、これはそれほど高速ではありません。 Linuxでは何、Windowsでは何。



そして最近、突然、一度、すべてが正方形に戻りました。 (たぶん、memcpyをAVXに強制的に書き換えないようにするためですか?)最新のプロセッサでは、古典的なmemcpyの実装が再び最速です。 だから誰かが34年間寝坊したなら、古いコードを引き出して、memcpyをMMX、SSE2、SSE3、SSE4.1に順番にコピーした同僚を意気揚々と見る時です。



ところで、コピーのパフォーマンスをテストすることをさらに興味深いものにするために(特に実際のソフトウェアのコンテキストで)、非一時的な読み取りと書き込み、メモリアクセス速度の制限、最後のレベルの一般的なキャッシュに関連する効果、およびDTLBミスの影響を受ける可能性があります。



結論

1.別の実装を書くことは今では役に立たない、std :: memcpyはrep movsを使用して効果を維持します。

2.古いプラットフォームでの歴史的背景とパフォーマンスの研究については、この記事とAgner Fogを参照してください。

3. Atom、他のX86プラットフォーム、および古い(Nezhemamより前の)プロセッサーでは、rep movは依然として低速です。



更新:

コメントで繰り返し要求されたように、memcpyを使用して、ループ内のすべてのアライメントの組み合わせを使用して、数キロバイトの単純なマイクロベンチマークを実行しました。

Digit-最も高度なSSE4.1コードがstdよりも高速である回数:: memcpyがrep movsを介して実装されている

ブルドーザー-1.22x(データのstepmexに感謝)

ペンリン-1.6x

ネハレム-1.5x

サンディブリッジ-1.008x

このベンチマークは特に正確ではありません。他の多くの要因が実際のソフトウェアで役割を果たします。



All Articles