web-dev-qa-db-ja.com

memcpy用の強化されたREP MOVSB

拡張REP MOVSB(ERMSB)を使用して、カスタムmemcpyの高帯域幅を取得したいと思います。

ERMSBはIvy Bridgeマイクロアーキテクチャで導入されました。 ERMSBがわからない場合は、 Intel最適化マニュアル の「Enhanced REP MOVSBおよびSTOSB操作(ERMSB)」セクションを参照してください。

これを直接行う唯一の方法は、インラインアセンブリを使用することです。 https://groups.google.com/forum/#!topic/gnu.gcc.help/-Bmlm_EG_fE から次の関数を取得しました

static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

ただし、これを使用すると、帯域幅はmemcpyを使用した場合よりもはるかに少なくなります。 __movsbは15 GB /秒、memcpyはi7-6700HQ(Skylake)システム、Ubuntu 16.10、DDR4 @ 2400 MHzデュアルチャネル32 GB、GCC 6.2で26 GB /秒を取得します。

帯域幅がREP MOVSBで非常に低いのはなぜですか?それを改善するために何ができますか?

これをテストするために使用したコードを次に示します。

//gcc -O3 -march=native -fopenmp foo.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stddef.h>
#include <omp.h>
#include <x86intrin.h>

static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

int main(void) {
  int n = 1<<30;

  //char *a = malloc(n), *b = malloc(n);

  char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096);
  memset(a,2,n), memset(b,1,n);

  __movsb(b,a,n);
  printf("%d\n", memcmp(b,a,n));

  double dtime;

  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) __movsb(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);

  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) memcpy(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);  
}

rep movsbに興味がある理由は、これらのコメントに基づいています

IvybridgeとHaswellでは、MLCに合わせてバッファを大きくすると、rep movsbを使用してmovntdqaに勝つことができます。 movntdqaはLLCにRFOを引き起こしますが、rep movsbはそうではありません...

このmemcpyの実装に欠けている/最適でないものは何ですか?


tinymembnech からの同じシステムでの私の結果を以下に示します。

 C copy backwards                                     :   7910.6 MB/s (1.4%)
 C copy backwards (32 byte blocks)                    :   7696.6 MB/s (0.9%)
 C copy backwards (64 byte blocks)                    :   7679.5 MB/s (0.7%)
 C copy                                               :   8811.0 MB/s (1.2%)
 C copy prefetched (32 bytes step)                    :   9328.4 MB/s (0.5%)
 C copy prefetched (64 bytes step)                    :   9355.1 MB/s (0.6%)
 C 2-pass copy                                        :   6474.3 MB/s (1.3%)
 C 2-pass copy prefetched (32 bytes step)             :   7072.9 MB/s (1.2%)
 C 2-pass copy prefetched (64 bytes step)             :   7065.2 MB/s (0.8%)
 C fill                                               :  14426.0 MB/s (1.5%)
 C fill (shuffle within 16 byte blocks)               :  14198.0 MB/s (1.1%)
 C fill (shuffle within 32 byte blocks)               :  14422.0 MB/s (1.7%)
 C fill (shuffle within 64 byte blocks)               :  14178.3 MB/s (1.0%)
 ---
 standard memcpy                                      :  12784.4 MB/s (1.9%)
 standard memset                                      :  30630.3 MB/s (1.1%)
 ---
 MOVSB copy                                           :   8712.0 MB/s (2.0%)
 MOVSD copy                                           :   8712.7 MB/s (1.9%)
 SSE2 copy                                            :   8952.2 MB/s (0.7%)
 SSE2 nontemporal copy                                :  12538.2 MB/s (0.8%)
 SSE2 copy prefetched (32 bytes step)                 :   9553.6 MB/s (0.8%)
 SSE2 copy prefetched (64 bytes step)                 :   9458.5 MB/s (0.5%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  13103.2 MB/s (0.7%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  13179.1 MB/s (0.9%)
 SSE2 2-pass copy                                     :   7250.6 MB/s (0.7%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7437.8 MB/s (0.6%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7498.2 MB/s (0.9%)
 SSE2 2-pass nontemporal copy                         :   3776.6 MB/s (1.4%)
 SSE2 fill                                            :  14701.3 MB/s (1.6%)
 SSE2 nontemporal fill                                :  34188.3 MB/s (0.8%)

私のシステムでは、SSE2 copy prefetchedMOVSB copyよりも高速です。


最初のテストでは、ターボを無効にしませんでした。ターボを無効にして再度テストしたところ、大きな違いは見られません。ただし、電源管理を変更すると大きな違いが生じます。

私がする時

Sudo cpufreq-set -r -g performance

rep movsbで20 GB/sを超えることがあります。

Sudo cpufreq-set -r -g powersave

私が見る最高のものは約17 GB/sです。しかし、memcpyは電源管理に敏感ではないようです。


周波数を確認しました(turbostatを使用) SpeedStepを有効または無効にperformanceを使用し、powersaveをアイドル、1コアの負荷、 4コアの負荷。 IntelのMKL密行列乗算を実行して、負荷を作成し、OMP_SET_NUM_THREADSを使用してスレッド数を設定しました。結果の表は次のとおりです(GHz単位の数値)。

              SpeedStep     idle      1 core    4 core
powersave     OFF           0.8       2.6       2.6
performance   OFF           2.6       2.6       2.6
powersave     ON            0.8       3.5       3.1
performance   ON            3.5       3.5       3.1

これは、powersaveがSpeedStepを無効にした場合でも、CPUが0.8 GHzのアイドル周波数までクロックダウンすることを示しています。 CPUが一定の周波数で実行されるのは、SpeedStepなしのperformanceのみです。

Sudo cpufreq-set -r performancecpufreq-setが奇妙な結果を出したため)を使用して、電源設定を変更しました。これによりターボが再びオンになるため、その後ターボを無効にする必要がありました。

54
Z boson

これは私の心と最近の調査に非常に近いトピックなので、いくつかの角度からそれを見ていきます:歴史、いくつかのテクニカルノート(ほとんどはアカデミック)、私の箱でのテスト結果、そして最後にあなたの実際の質問に答える試みrep movsbがいつ、どこで理にかなっているのか。

一部、これは結果を共有するための呼び出しです- Tinymembench を実行し、CPUの詳細とともに結果を共有できる場合RAMの構成は素晴らしいでしょう。特に、4チャネルセットアップ、Ivy Bridgeボックス、サーバーボックスなどがある場合。

歴史と公式アドバイス

高速文字列コピー命令のパフォーマンス履歴は、少し階段状の問題です。つまり、パフォーマンスが停滞している期間が、大きなアップグレードと交互に並んでいる、または競合するアプローチよりも高速です。たとえば、Nehalem(主にスタートアップオーバーヘッドを対象とする)とIvy Bridge(大規模コピーの合計スループットを対象とする)でパフォーマンスが急上昇しました。 Intelエンジニアからのrep movs命令の実装の難しさに関する10年前の洞察を見つけることができます このスレッドで

たとえば、Ivy Bridgeの導入前のガイドでは、典型的な アドバイス は、それらを避けるか、非常に慎重に使用することです1

現在(2016年6月)のガイドには、次のようなさまざまな混乱した、やや一貫性のないアドバイスがあります。2

実装の特定のバリアントは、実行時にデータレイアウト、アライメント、およびカウンター(ECX)値に基づいて選択されます。たとえば、REPプレフィックス付きのMOVSB/STOSBは、最高のパフォーマンスを得るために3以下のカウンタ値で使用する必要があります。

では、3バイト以下のコピーの場合はどうでしょうか?そもそもrepプレフィックスは必要ありません。スタートアップレイテンシが〜9サイクルと主張されているので、未使用をマスクするためのビット調整を少し加えた単純なDWORDまたはQWORD movの方がほぼ確実に良いからですバイト(またはサイズが正確に3であることがわかっている場合は、おそらく2つの明示的なバイト、Word movsを使用)。

彼らは言い続けます:

String MOVE/STORE命令には、複数のデータ粒度があります。データを効率的に移動するには、より大きなデータ粒度が望ましいです。これは、任意のカウンター値を複数のダブルワードに分解し、カウント値が3以下のシングルバイト移動により、効率が向上することを意味します。

これは、rep movsbが少なくとも大規模なコピーのmovdまたはmovqバリアントと同じかそれより速いERMSBを備えた現在のハードウェアでは間違いのように思われます。

一般に、現在のガイドのそのセクション(3.7.5)には、合理的でひどく陳腐なアドバイスが混在しています。これは、Intelマニュアルの一般的なスループットです。これは、各アーキテクチャごとにインクリメンタルに更新され(現在のマニュアルでも20年近くのアーキテクチャをカバーすることを目的としている)、古いセクションが更新されたり、条件付きアドバイスを行ったりしないためですこれは現在のアーキテクチャには適用されません。

その後、セクション3.7.6でERMSBを明示的に扱います。

残りのアドバイスを徹底的に検討することはしませんが、以下の「なぜ使用するか」で良い部分を要約します。

このガイドからのその他の重要な主張は、Haswellでrep movsbが内部的に256ビット操作を使用するように拡張されていることです。

技術的な考慮事項

これは、rep命令がimplementationの見地から持っている根本的な長所と短所の簡単な要約です。

rep movsの利点

  1. rep movs命令が発行されると、CPUknowsで、既知のサイズのブロック全体が転送されます。これにより、個別の命令では不可能な方法で操作を最適化できます。次に例を示します。

    • キャッシュライン全体が上書きされることがわかっている場合に、RFO要求を回避します。
    • プリフェッチ要求をすぐに正確に発行します。ハードウェアプリフェッチはmemcpyのようなパターンを検出するのに適していますが、それでも起動に数回の読み取りが必要であり、コピーされた領域の終わりを超えて多くのキャッシュラインを「オーバープリフェッチ」します。 rep movsbは領域サイズを正確に認識しており、正確にプリフェッチできます。
  2. どうやら、内の店舗間の注文の保証はありません3 一貫性のあるトラフィックとブロック移動の他の側面を単純化するのに役立つ単一のrep movsと、かなり厳密なメモリ順序に従う必要がある単純なmov命令4

  3. 原則として、rep movs命令は、ISAで公開されていないさまざまなアーキテクチャ上のトリックを利用できます。たとえば、アーキテクチャには、ISAが公開するより広い内部データパスがある場合があります5 そして、rep movsはそれを内部的に使用できます。

欠点

  1. rep movsbは、基礎となるソフトウェア要件よりも強力な特定のセマンティクスを実装する必要があります。特に、memcpyは重複する領域を禁止するため、その可能性を無視する場合がありますが、rep movsbはそれらを許可し、期待される結果を生成する必要があります。現在の実装では、主に起動時のオーバーヘッドに影響しますが、大規模なスループットにはおそらく影響しません。同様に、rep movsbは、2の大きな累乗の倍数である大きなブロックをコピーするために実際に使用している場合でも、バイト単位のコピーをサポートする必要があります。

  2. ソフトウェアには、rep movsbを使用している場合、ハードウェアと通信できないアライメント、コピーサイズ、およびエイリアスの可能性に関する情報が含まれている場合があります。多くの場合、コンパイラはメモリブロックのアライメントを決定できます。6 rep movseveryの呼び出しで行う必要がある起動作業の多くを回避できます。

試験結果

以下は、2.6 GHzのi7-6700HQでの tinymembench のさまざまなコピー方法のテスト結果です(同じCPUを使用しているため、新しいデータポイントを取得できません...):

 C copy backwards                                     :   8284.8 MB/s (0.3%)
 C copy backwards (32 byte blocks)                    :   8273.9 MB/s (0.4%)
 C copy backwards (64 byte blocks)                    :   8321.9 MB/s (0.8%)
 C copy                                               :   8863.1 MB/s (0.3%)
 C copy prefetched (32 bytes step)                    :   8900.8 MB/s (0.3%)
 C copy prefetched (64 bytes step)                    :   8817.5 MB/s (0.5%)
 C 2-pass copy                                        :   6492.3 MB/s (0.3%)
 C 2-pass copy prefetched (32 bytes step)             :   6516.0 MB/s (2.4%)
 C 2-pass copy prefetched (64 bytes step)             :   6520.5 MB/s (1.2%)
 ---
 standard memcpy                                      :  12169.8 MB/s (3.4%)
 standard memset                                      :  23479.9 MB/s (4.2%)
 ---
 MOVSB copy                                           :  10197.7 MB/s (1.6%)
 MOVSD copy                                           :  10177.6 MB/s (1.6%)
 SSE2 copy                                            :   8973.3 MB/s (2.5%)
 SSE2 nontemporal copy                                :  12924.0 MB/s (1.7%)
 SSE2 copy prefetched (32 bytes step)                 :   9014.2 MB/s (2.7%)
 SSE2 copy prefetched (64 bytes step)                 :   8964.5 MB/s (2.3%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11777.2 MB/s (5.6%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11826.8 MB/s (3.2%)
 SSE2 2-pass copy                                     :   7529.5 MB/s (1.8%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7122.5 MB/s (1.0%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7214.9 MB/s (1.4%)
 SSE2 2-pass nontemporal copy                         :   4987.0 MB/s

いくつかの重要なポイント:

  • rep movsメソッドは、「非一時的」ではない他のすべてのメソッドよりも高速です。7、一度に8バイトをコピーする「C」アプローチよりもかなり高速です。
  • 「非テンポラル」メソッドは、rep movsメソッドよりも最大約26%高速です-ただし、これは報告したデルタよりもはるかに小さいデルタです(26 GB/s対15 GB/s =〜73% )。
  • 非一時ストアを使用していない場合、Cからの8バイトのコピーを使用することは、128ビット幅のSSEロード/ストアとほとんど同じです。これは、適切なコピーループが帯域幅を飽和させるのに十分なメモリプレッシャーを生成する可能性があるためです(たとえば、2.6 GHz * 1ストア/サイクル* 8バイト= 26 GB /秒のストア)。
  • Tinymembenchには明示的な256ビットアルゴリズムはありません(おそらく「標準」memcpyを除く)が、おそらく上記の注意のために重要ではありません。
  • 非テンポラルストアアプローチの一時スループットよりも向上したスループットは約1.45倍であり、これはNTが3転送のうち1を排除する場合に予想される1.5倍に非常に近い(つまり、NTに対して1読み取り、1書き込み)読み取り、1書き込み)。 rep movsアプローチは中間にあります。
  • かなり低いメモリレイテンシと適度な2チャネル帯域幅の組み合わせにより、この特定のチップはたまたまシングルスレッドからメモリ帯​​域幅を飽和させることができ、動作が劇的に変化します。
  • rep movsdは、このチップのrep movsbと同じ魔法を使用しているようです。 ERMSBは明示的にmovsbのみをターゲットとし、ERMSBを使用した以前のアーキテクチャでの以前のテストではmovsbmovsdよりもはるかに高速に実行されることを示しているため、興味深いことです。とにかくmovsbmovsdよりも一般的であるため、これはほとんどアカデミックです。

ハスウェル

コメントでiwillnotexistから親切に提供された Haswellの結果 を見ると、同じ一般的な傾向が見られます(最も関連性の高い結果が抽出されています)。

 C copy                                               :   6777.8 MB/s (0.4%)
 standard memcpy                                      :  10487.3 MB/s (0.5%)
 MOVSB copy                                           :   9393.9 MB/s (0.2%)
 MOVSD copy                                           :   9155.0 MB/s (1.6%)
 SSE2 copy                                            :   6780.5 MB/s (0.4%)
 SSE2 nontemporal copy                                :  10688.2 MB/s (0.3%)

rep movsbアプローチは、一時的でないmemcpyよりもまだ遅いですが、ここでは約14%だけです(Skylakeテストでは〜26%と比較)。一時的な従兄弟を超えるNTテクニックの利点は、現在約57%であり、帯域幅削減の理論的な利点よりも少し大きいです。

いつrep movsを使用すべきですか?

最後に、実際の質問を突き刺します。いつ、またはなぜそれを使用する必要がありますか?上記に基づいて、いくつかの新しいアイデアを紹介します。残念ながら、単純な答えはありません。さまざまな要因をトレードオフする必要があります。これには、将来の開発など、おそらく正確に知ることさえできないものも含まれます。

rep movsbに代わるものは、最適化されたlibc memcpy(コンパイラーによってインライン化されたコピーを含む)であるか、または手巻きのmemcpyバージョンである可能性があることに注意してください。以下の利点のいくつかは、これらの選択肢のいずれかと比較してのみ適用されます(たとえば、「シンプルさ」は手巻きバージョンに対して役立ちますが、組み込みのmemcpyに対しては役立ちません)、いくつかは両方に適用されます。

利用可能な指示の制限

一部の環境では、特定の命令または特定のレジスタの使用に制限があります。たとえば、Linuxカーネルでは、SSE/AVXまたはFPレジスタの使用は通常許可されていません。したがって、最適化されたmemcpyバリアントのほとんどは、SSEまたはAVXレジスタに依存しているため使用できません。x86では、プレーンな64ビットのmovベースのコピーが使用されます。これらのプラットフォームでは、rep movsbを使用すると、SIMDコードの制限を破ることなく、最適化されたmemcpyのパフォーマンスのほとんどを実現できます。

より一般的な例は、多くの世代のハードウェアを対象としなければならないコードで、ハードウェア固有のディスパッチを使用しません(たとえば、cpuidを使用)。ここでは、AVXなどを除外する古い命令セットのみを使用するように強制される場合があります。rep movsbは、新しい命令を使用せずに幅広いロードおよびストアへの「隠された」アクセスを許可するため、ここでは良いアプローチかもしれません。ただし、ERMSB以前のハードウェアを対象とする場合は、rep movsbパフォーマンスが許容範囲内であるかどうかを確認する必要があります...

将来の校正

rep movsbの優れた点は、in theory将来のアーキテクチャで、ソースを変更せずに明示的な移動ができないアーキテクチャの改善を活用できることです。たとえば、256ビットのデータパスが導入された場合、rep movsbはソフトウェアに変更を加えることなく(Intelが主張するように)それらを利用することができました。 128ビットの移動を使用するソフトウェア(Haswell以前は最適でした)は、修正して再コンパイルする必要があります。

したがって、ソフトウェアメンテナンスの利点(ソースを変更する必要がない)と既存のバイナリの利点(改善を利用するために新しいバイナリを展開する必要がない)の両方です。

これがどれほど重要であるかは、メンテナンスモデル(たとえば、実際に新しいバイナリが展開される頻度)と、これらの命令が今後どれくらい速くなるかを判断するのが非常に困難です。少なくともIntelは、少なくともreasonable将来のパフォーマンス(15.3.3.6 ):

REP MOVSBおよびREP STOSBは、将来のプロセッサでも引き続き十分に機能します。

後続の作業との重複

もちろん、このメリットはプレーンなmemcpyベンチマークには現れません。定義上、重複する後続の作業がないため、実際のシナリオではメリットの大きさを慎重に測定する必要があります。最大限に活用するには、memcpyを囲むコードの再編成が必要になる場合があります。

この利点は、Intelの最適化マニュアル(セクション11.16.3.4)およびその言葉で指摘されています。

カウントが少なくとも1,000バイト以上であることがわかっている場合、拡張REP MOVSB/STOSBを使用すると、非消費コードのコストを償却する別の利点が得られます。ヒューリスティックは、例としてCnt = 4096およびmemset()の値を使用して理解できます。

•memset()の256ビットSIMD実装では、VMOVDQAを使用して32バイトストア操作の128インスタンスを発行/実行してから、非消費命令シーケンスを廃止する必要があります。

•ECX = 4096の拡張REP STOSBのインスタンスは、ハードウェアによって提供される長いマイクロオペレーションフローとしてデコードされますが、1つの命令として廃止されます。 memset()の結果が消費される前に完了する必要があるstore_data操作が多数あります。データ保存操作の完了はプログラム順序の廃止とは切り離されているため、非消費コードストリームの大部分は発行/実行および廃止を通じて処理でき、非消費シーケンスが競合しない場合は基本的にコストがかかりませんストアバッファリソース用。

Intelはrep movsbが発行された後のいくつかのuopsの後にコードが発行されているが、多くの店舗がまだ飛行中であり、rep movsb全体がまだ引退していないと言っています。そのコードがコピーループの後に来た場合よりも、アウトオブオーダーマシンを介してより多くの進歩を遂げます。

明示的なロードおよびストアループからのuopはすべて、プログラムの順序で実際に個別に廃止する必要があります。それは、次のuopのためにROBにスペースを作るために起こる必要があります。

rep movsbのような非常に長いマイクロコード化された命令が正確にどの程度機能するかについての詳細な情報はないようです。マイクロコードブランチがマイクロコードシーケンサーから別のuopsストリームをどのように要求するか、またはuopsがどのように廃止されるかは正確にはわかりません。個々のuopが個別にリタイアする必要がない場合、おそらく命令全体がROBの1つのスロットのみを占有しますか?

OoOマシンにフィードするフロントエンドは、uopキャッシュにrep movsb命令を検出すると、マイクロコードシーケンサーROM(MS-ROM)をアクティブにして、マイクロコードuopをフィードするキューに送信します。発行/名前変更ステージ。他のuopがそれと混ざって発行/実行することはおそらく不可能です8 rep movsbは引き続き発行されますが、コピーの一部がまだ実行されていない間に、後続の命令をフェッチ/デコードし、最後のrep movsb uopの直後に発行できます。これは、後続のコードの少なくともいくつかがmemcpyの結果に依存していない場合にのみ役立ちます(これは珍しいことではありません)。

現在、この利点のサイズは制限されています。遅くてもrep movsb命令を超えてN命令(実際にはuops)を実行できます。この時点でストールします。Nは ROBサイズ 。現在のROBサイズは約200(Haswellで192、Skylakeで224)です。これは、IPCが1の後続コードの最大200サイクルの無料作業のメリットです。200サイクルでどこかにコピーできます10 GB/sで約800バイトなので、そのサイズのコピーの場合、コピーのコストに近い(コピーを無料にする方法で)無料の作業を得ることができます。

ただし、コピーサイズが大きくなると、この相対的な重要性は急速に低下します(たとえば、代わりに80 KBをコピーする場合、無料の作業はコピーコストの1%に過ぎません)。それでも、控えめなサイズのコピーには非常に興味深いものです。

コピーループは、後続の命令の実行も完全にはブロックしません。インテルは、メリットの大きさ、またはどのようなコピーや周辺コードが最もメリットがあるかについて詳しく説明していません。 (ホットまたはコールドの宛先またはソース、高ILPまたは低ILP高レイテンシコードの後)。

コードサイズ

実行されるコードサイズ(数バイト)は、典型的な最適化されたmemcpyルーチンと比較して微視的です。パフォーマンスがiキャッシュ(uopキャッシュを含む)ミスによって制限される場合は、コードサイズを小さくするとメリットがあります。

繰り返しますが、コピーのサイズに基づいてこの利点の大きさを制限できます。数値的には実際には解決しませんが、直感的には、動的なコードサイズをBバイト減らすことで、定数CでC * Bキャッシュミスをせいぜい節約できます。すべてのcallmemcpyにすると、キャッシュミスコスト(またはメリット)が1回発生しますが、コピーされるバイト数に応じてスループットが向上するという利点があります。そのため、大規模な転送では、スループットが高いほどキャッシュ効果が支配的になります。

繰り返しますが、これはループ全体が間違いなくuopキャッシュに収まるような単純なベンチマークに現れるものではありません。この効果を評価するには、実世界のインプレーステストが必要です。

アーキテクチャ固有の最適化

ハードウェア上で、rep movsbはプラットフォームmemcpyよりもかなり遅いと報告しました。ただし、ここでも以前のハードウェア(Ivy Bridgeなど)で反対の結果が報告されています。

文字列移動操作は定期的に愛を得るように見えるので、それは完全に妥当です-しかし、すべての世代ではないので、それは以前のアーキテクチャでより速くまたは少なくとも他の利点に基づいて勝つ可能性があります最新のものになりましたが、後続のハードウェアでは遅れをとっています。

引用 Andy Glew、P6でこれらを実装した後、このことについて1つまたは2つ知っている必要があります。

マイクロコードで高速文字列を実行することの大きな弱点は[...]すべての世代でマイクロコードが調子を失い、誰かがそれを修正するまで徐々に遅くなりました。図書館の男性のコピーが調子外れになるように。逃した機会の1つは、利用可能になったときに128ビットのロードとストアを使用することなどであった可能性があると思います。

その場合、標準ライブラリとJITコンパイラで見られる典型的な「すべてのトリック」のmemcpyルーチンに適用する、別の「プラットフォーム固有の」最適化と見なすことができます。ただし、それが存在するアーキテクチャでのみ使用しますより良い。 JITまたはAOTでコンパイルされたものの場合、これは簡単ですが、静的にコンパイルされたバイナリの場合、これはプラットフォーム固有のディスパッチを必要としますが、多くの場合既に存在します(リンク時に実装される場合があります)、またはmtune引数を使用して静的な決定を行うことができます。

シンプルさ

絶対最速の非時間的手法に遅れをとっているように見えるSkylakeでも、ほとんどのアプローチよりも高速で、very simpleです。これは、検証の時間が短くなり、謎のバグが少なくなり、モンスターのmemcpy実装の調整と更新の時間が短くなることを意味します(逆に、標準ライブラリの実装者の気まぐれへの依存度が低くなります)。

遅延バウンドプラットフォーム

メモリスループット制限アルゴリズム9 実際には、DRAM帯域幅の制限または同時実行性/遅延の制限という2つの主な全体体制で動作している可能性があります。

最初のモードは、おそらく使い慣れたモードです。DRAMサブシステムには、チャネル数、データレート/幅、および周波数に基づいて非常に簡単に計算できる特定の理論帯域幅があります。たとえば、2チャネルのDDR4-2133システムの最大帯域幅は2.133 * 8 * 2 = 34.1 GB/sで、 ARKで報告 と同じです。

ソケットのすべてのコアに追加されたDRAMからのレート(および通常はさまざまな非効率性のために多少低下します)を超えることはありません(つまり、シングルソケットシステムのグローバルな制限です)。

もう1つの制限は、コアがメモリサブシステムに実際に発行できる同時要求の数によって課されます。 64バイトのキャッシュラインに対して、コアで一度に1つのリクエストしか処理できない場合を想像してください。リクエストが完了したら、別のリクエストを発行できます。非常に高速な50nsのメモリレイテンシも想定します。そして、34.1 GB/sの大きなDRAM帯域幅にもかかわらず、実際には64バイト/ 50 ns = 1.28 GB/s、または最大帯域幅の4%未満しか得られません。

実際には、コアは一度に複数のリクエストを発行できますが、無制限の数は発行できません。通常、L1とメモリ階層の残りの間にコアあたり10line fill buffersがあり、L2とDRAMの間におそらく16程度のフィルバッファがあると理解されています。 。プリフェッチは同じリソースを求めて競合しますが、少なくとも有効なレイテンシを減らすのに役立ちます。詳細については、素晴らしい投稿のいずれかをご覧ください Dr。Bandwidthがこのトピックについて書いています 、主にIntelフォーラムで。

それでも、most最近のCPUは、RAM帯域幅ではなく、this係数によって制限されます。通常、コアあたり12〜20 GB/sを達成しますが、RAM帯域幅は50+ GB/s(4チャネルシステム上)です。より良いアンコアを持っているように見える最近の第2世代の2チャネル「クライアント」コアのみ、おそらくより多くのラインバッファーがシングルコアのDRAM制限に達する可能性があり、Skylakeチップはそれらの1つであるようです。

もちろん、Intelが50 GB/sのDRAM帯域幅でシステムを設計するのには理由がありますが、同時実行制限のためにコアあたり20 GB/s未満しか維持できません:前者の制限はソケット全体で、後者はコアごとです。そのため、8コアシステムの各コアは20 GB /秒に相当するリクエストをプッシュでき、その時点で再びDRAM制限になります。

なぜ私はこれについて続けていますか?最適なmemcpyの実装は、多くの場合、操作しているレジームに依存します。DRAMBWが制限されると(チップは明らかにそうですが、ほとんどがシングルコアではないため)、一時的でない書き込みを使用することは非常に重要です通常、帯域幅の1/3を無駄にする所有権の読み取り。上記のテスト結果で正確にわかります。don'tNTストアを使用するmemcpy実装は、帯域幅の1/3を失います。

ただし、同時実行が制限されている場合、状況は平等になり、場合によっては逆になります。予備のDRAM帯域幅があるので、NTストアは役に立たず、ラインバッファーのハンドオフ時間がプリフェッチでRFOラインをLLCにもたらすシナリオよりも長くなる可能性があるため、レイテンシーが増加する可能性があるため、さらに損傷する可能性があります(またはL2)その後、LLCでストアが完了し、効果的な低遅延が実現します。最後に、serveruncoresは、クライアントストア(および高帯域幅)よりもNTストアが非常に遅い傾向があり、この効果が強調されます。

そのため、他のプラットフォームでは、NTストアは(少なくともシングルスレッドのパフォーマンスに関心がある場合は)あまり役に立たず、おそらく(両方の世界で最高の結果が得られれば)rep movsbが勝つことがあります。

本当に、この最後の項目はほとんどのテストの呼び出しです。 NTストアは、ほとんどのアーチ(現在のサーバーアーチを含む)でのシングルスレッドテストの明らかな利点を失うことを知っていますが、rep movsbがどのように比較的機能するかわかりません...

参照資料

上記に統合されていないその他の情報源。

comp.Arch調べ of rep movsb vs代替案。分岐予測についての良いメモがたくさんあり、小さなブロックに対して私がよく提案したアプローチの実装:必要なバイト数だけを正確に書き込もうとするのではなく、重複する最初および/または最後の読み取り/書き込みを使用する(たとえば、実装9バイトから16バイトまでのすべてのコピーは、最大7バイトでオーバーラップする可能性のある2つの8バイトコピーとして)。


1 おそらく、意図は、たとえばコードサイズが非常に重要な場合に限定することです。

2 セクション3.7.5:REPプレフィックスとデータ移動を参照してください。

3 これは、単一の命令自体内のさまざまなストアにのみ適用されることに注意することが重要です。一度完了すると、ストアのブロックは、前後のストアに対して順序付けられたままになります。したがって、コードはrep movsからストアを順番に見ることができます相互にですが、前または後続のストアに関してはそうではありません(そして後者はあなたを保証します通常必要)。別のストアではなく、コピー先の終わりを同期フラグとして使用する場合にのみ問題になります。

4 非一時的な離散ストアもほとんどの順序付け要件を回避しますが、WC/NTストアにはいくつかの順序付け制約があるため、実際にはrep movsの自由度がさらに高いことに注意してください。

5 これは、多くのチップが64ビットのデータパスを持っていた32ビット時代の後半で一般的でした(たとえば、64ビットdouble型をサポートするFPUをサポートするため)。現在、PentiumやCeleronブランドなどの「去勢」チップではAVXが無効になっていますが、rep movsマイクロコードは256bのロード/ストアを使用できます。

6 たとえば、言語アライメント規則、アライメント属性または演算子、エイリアス規則、またはコンパイル時に決定されるその他の情報が原因です。アライメントの場合、正確なアライメントを決定できない場合でも、少なくともループからアライメントチェックを巻き上げたり、冗長なチェックを削除したりできます。

7 私は、「標準」memcpyが非一時的なアプローチを選択していると仮定しています。

8 rep movsbによって生成されたuopストリームが単にディスパッチを独占し、明示的なmovの場合と非常によく似ている場合があるため、それは必ずしも明らかではありません。しかし、そのようには動作しないようです-後続の命令からのuopは、マイクロコード化されたrep movsbからのuopと混ざることがあります。

9 つまり、多数の独立したメモリリクエストを発行できるため、利用可能なDRAMからコアへの帯域幅を飽和させる可能性があり、その中のmemcpyはポスチャチャイルドになります(また、ポインタ追跡などの純粋にレイテンシに制限された負荷になります)。

68
BeeOnRope

拡張REP MOVSB(Ivy Bridge以降)

Ivy Bridgeマイクロアーキテクチャ(2012年と2013年にリリースされたプロセッサ)が導入されました強化されたREP MOVSB(対応するビットをチェックする必要があります)、メモリを高速にコピーできるようになりました。

2017年にリリースされた後期プロセッサの最も安価なバージョンであるKaby Lake CeleronおよびPentiumには、高速メモリコピーに使用できるAVXはありませんが、Enhanced REP MOVSBはあります。

ブロックサイズが256バイト以上の場合、REP MOVSB(ERMSB)はAVXコピーまたは汎用レジスタコピーよりも高速です。 64バイト未満のブロックでは、ERMSBの内部起動が約35サイクルあるため、非常に遅くなります。

最適化に関するインテルのマニュアル、セクション3.7.6 Enhanced REP MOVSBおよびSTOSB操作(ERMSB)を参照してください http://www.intel.com/content/dam/www/public/us/en/documents/manuals/ 64-ia-32-architectures-optimization-manual.pdf

  • 起動コストは35サイクルです。
  • 送信元アドレスと宛先アドレスの両方を16バイト境界に揃える必要があります。
  • ソース領域は宛先領域とオーバーラップしてはなりません。
  • パフォーマンスを高めるには、長さを64の倍数にする必要があります。
  • 方向は前方(CLD)でなければなりません。

前述したように、REP MOVSBは長さが少なくとも256バイトのときに他の方法よりも性能が向上し始めますが、AVXコピーよりも明確な利点を得るには、長さが2048バイトを超える必要があります。

REP MOVSB対AVXコピーの場合の位置合わせの影響については、Intelマニュアルに次の情報が記載されています。

  • ソースバッファがアライメントされていない場合、128ビットAVXに対するERMSB実装への影響は同様です。
  • 宛先バッファーがアライメントされていない場合、ERMSB実装への影響は25%低下しますが、memcpyの128ビットAVXインプリメンテーションは、16バイトにアライメントされたシナリオに比べて5%しか低下しません。

64ビット未満のIntel Core i5-6600でテストを行い、REP MOVSB memcpy()と単純なMOV RAX [SRC]を比較しました。 MOV [DST]、RAX実装 データがL1キャッシュに適合するとき

REP MOVSB memcpy():

 - 1622400000 data blocks of  32 bytes took 17.9337 seconds to copy;  2760.8205 MB/s
 - 1622400000 data blocks of  64 bytes took 17.8364 seconds to copy;  5551.7463 MB/s
 - 811200000 data blocks of  128 bytes took 10.8098 seconds to copy;  9160.5659 MB/s
 - 405600000 data blocks of  256 bytes took  5.8616 seconds to copy; 16893.5527 MB/s
 - 202800000 data blocks of  512 bytes took  3.9315 seconds to copy; 25187.2976 MB/s
 - 101400000 data blocks of 1024 bytes took  2.1648 seconds to copy; 45743.4214 MB/s
 - 50700000 data blocks of  2048 bytes took  1.5301 seconds to copy; 64717.0642 MB/s
 - 25350000 data blocks of  4096 bytes took  1.3346 seconds to copy; 74198.4030 MB/s
 - 12675000 data blocks of  8192 bytes took  1.1069 seconds to copy; 89456.2119 MB/s
 - 6337500 data blocks of  16384 bytes took  1.1120 seconds to copy; 89053.2094 MB/s

MOV RAX ... memcpy():

 - 1622400000 data blocks of  32 bytes took  7.3536 seconds to copy;  6733.0256 MB/s
 - 1622400000 data blocks of  64 bytes took 10.7727 seconds to copy;  9192.1090 MB/s
 - 811200000 data blocks of  128 bytes took  8.9408 seconds to copy; 11075.4480 MB/s
 - 405600000 data blocks of  256 bytes took  8.4956 seconds to copy; 11655.8805 MB/s
 - 202800000 data blocks of  512 bytes took  9.1032 seconds to copy; 10877.8248 MB/s
 - 101400000 data blocks of 1024 bytes took  8.2539 seconds to copy; 11997.1185 MB/s
 - 50700000 data blocks of  2048 bytes took  7.7909 seconds to copy; 12710.1252 MB/s
 - 25350000 data blocks of  4096 bytes took  7.5992 seconds to copy; 13030.7062 MB/s
 - 12675000 data blocks of  8192 bytes took  7.4679 seconds to copy; 13259.9384 MB/s

そのため、128ビットブロックであっても、REP MOVSBはループ内の単純なMOV RAXコピー(展開されていない)よりも低速です。 ERMSBの実装は、256バイトブロックからのみMOV RAXループよりも優れたパフォーマンスを発揮し始めます。

Nehalem以降での通常の(拡張されていない)REP MOVS

驚くべきことに、まだREP MOVBが拡張されていない以前のアーキテクチャ(Nehalem以降)には、大きなブロックに対してREP MOVSD/MOVSQ(REP MOVSB/MOVSWではない)の実装が非常に高速でしたが、L1キャッシュを十分に大きくすることができませんでした。

Intel Optimization Manual(2.5.6 REP String Enhancement)は、Nehalemマイクロアーキテクチャに関連する次の情報を提供します-2009年および2010年にリリースされたIntel Core i5、i7およびXeonプロセッサ。

REP MOVSB

MOVSBのレイテンシは、ECX <4の場合、9サイクルです。そうでない場合、ECX> 9のREP MOVSBの起動コストは50サイクルです。

  • 小さな文字列(ECX <4):REP MOVSBのレイテンシは9サイクルです。
  • 小さな文字列(ECXは4〜9):Intelマニュアルには公式情報はありません。おそらく9サイクル以上50サイクル未満です。
  • 長い文字列(ECX> 9):50サイクルの起動コスト。

私の結論:REP MOVSBはNehalemではほとんど役に立ちません。

MOVSW/MOVSD/MOVSQ

Intel Optimization Manual(2.5.6 REPストリング拡張)からの引用:

  • 短い文字列(ECX <= 12):REP MOVSW/MOVSD/MOVSQのレイテンシは約20サイクルです。
  • 高速文字列(ECX> = 76:REP MOVSBを除く):プロセッサの実装では、16バイトのできるだけ多くのデータを移動することにより、ハードウェアを最適化します。 16バイトのデータ転送のいずれかがキャッシュライン境界をまたがる場合、REP文字列のレイテンシのレイテンシは異なります。 =キャッシュの分割:遅延は約35サイクルの起動コストで構成され、64バイトのデータごとに6サイクルが追加されます。
  • 中間文字列の長さ:REP MOVSW/MOVSD/MOVSQの待機時間には、約15サイクルと、Word/dword/qwordでのデータ移動の反復ごとに1サイクルの起動コストがあります。

Intelはここでは正しくないようです。上記の引用から、非常に大きなメモリブロックの場合、REP MOVSWはREP MOVSD/MOVSQと同じくらい速いことがわかりますが、テストではREP MOVSD/MOVSQだけが高速で、REP MOVSWはネハレムとウェストミアのREP MOVSBよりもさらに遅いことが示されています。

マニュアルでIntelが提供した情報によると、以前のIntelマイクロアーキテクチャ(2008年以前)では、起動コストはさらに高くなります。

結論:L1キャッシュに適合するデータをコピーする必要がある場合、64バイトのデータをコピーするのに4サイクルで十分であり、XMMレジスタを使用する必要はありません!

REP MOVSD/MOVSQは、データがL1キャッシュに適合する場合、すべてのIntelプロセッサーで優れた動作をするユニバーサルソリューションです(ERMSBは不要)。

ソースと宛先がL1キャッシュにあり、起動コストの影響を大きく受けないがL1キャッシュサイズを超えるほど大きくないブロックのREP MOVS *のテストを次に示します。ソース: http://users.atw.hu/instlatx64/

ヨナ(2006-2008)

    REP MOVSB 10.91 B/c
    REP MOVSW 10.85 B/c
    REP MOVSD 11.05 B/c

ネハレム(2009-2010)

    REP MOVSB 25.32 B/c
    REP MOVSW 19.72 B/c
    REP MOVSD 27.56 B/c
    REP MOVSQ 27.54 B/c

ウェストミア(2010-2011)

    REP MOVSB 21.14 B/c
    REP MOVSW 19.11 B/c
    REP MOVSD 24.27 B/c

Ivy Bridge(2012-2013)-拡張REP MOVSBを使用

    REP MOVSB 28.72 B/c
    REP MOVSW 19.40 B/c
    REP MOVSD 27.96 B/c
    REP MOVSQ 27.89 B/c

SkyLake(2015-2016)-拡張REP MOVSBを使用

    REP MOVSB 57.59 B/c
    REP MOVSW 58.20 B/c
    REP MOVSD 58.10 B/c
    REP MOVSQ 57.59 B/c

Kaby Lake(2016-2017)-拡張REP MOVSBを使用

    REP MOVSB 58.00 B/c
    REP MOVSW 57.69 B/c
    REP MOVSD 58.00 B/c
    REP MOVSQ 57.89 B/c

ご覧のとおり、REP MOVSの実装は、マイクロアーキテクチャごとに大きく異なります。 Ivy Bridgeなどの一部のプロセッサでは、REP MOVSBはREP MOVSD/MOVSQよりもわずかに高速ですが、最速ですが、Nehalem以降のすべてのプロセッサでREP MOVSD/MOVSQが非常にうまく機能することは間違いありません。「Enhanced REP Enhacnced REP MOVSBを使用したIvy Bridge(2013)では、REP MOVSDはNehalem(2010)を使用せずにクロックデータごとに同じバイトを表示するため、 強化されたREP MOVSB。実際、REP MOVSBは、SkyLake(2015)以降、Ivy Bridgeの2倍の速さで非常に高速になりました。そのため、CPUIDのこのEnhacnced REP MOVSBビットは紛らわしいかもしれません-REP MOVSBそれ自体は問題ありませんが、_REP MOVS*は高速です。

最も複雑なERMBSB実装は、Ivy Bridgeマイクロアーキテクチャ上にあります。はい、非常に古いプロセッサでは、ERMSBの前に、大きなブロックのREP MOVS *は通常のコードでは使用できないキャッシュプロトコル機能を使用していました(RFOなし)。ただし、このプロトコルは、ERMSBを備えたIvy Bridgeでは使用されなくなりました。 Peter Cordesの回答の「なぜ複雑なmemcpy/memsetが優れているのか?」に対する回答に関するAndy Glewのコメント によれば、通常のコードでは利用できないキャッシュプロトコル機能が古いプロセッサーで使用されていたため、しかし、もはやIvy Bridgeにはありません。また、REP MOVS *の起動コストが非常に高い理由について説明があります。「正しい方法を選択して設定するための大きなオーバーヘッドは、主にマイクロコードの分岐予測がないためです」。 1996年にPentium Pro(P6)が64ビットのマイクロコードのロードとストア、および非RFOキャッシュプロトコルでREP MOVS *を実装したという興味深いメモもありました-Ivy BridgeのERMSBとは異なり、メモリの順序に違反しませんでした。

免責事項

  1. この回答は、ソースデータと宛先データがL1キャッシュに適合する場合にのみ関連します。状況に応じて、メモリアクセスの特性(キャッシュなど)を考慮する必要があります。特にEnhanced REP MOVSBをまだ搭載していないプロセッサでは、プリフェッチとNTIが特定のケースでより良い結果をもたらす可能性があります。これらの古いプロセッサーでも、REP MOVSDは通常のコードでは使用できないキャッシュプロトコル機能を使用している可能性があります。
  2. この回答の情報はIntelプロセッサにのみ関連し、REP MOVS *命令の実装がより良いまたはより悪い可能性のあるAMDなどの他のメーカーのプロセッサには関連していません。
  3. 確認のために、SkyLakeとKaby Lakeの両方のテスト結果を示しました。これらのアーキテクチャは、命令ごとのサイクルデータが同じです。
  4. すべての製品名、商標および登録商標は、それぞれの所有者の財産です。
10
Maxim Masiutin

あなたが欲しいと言う:

eRMSBがいつ役立つかを示す回答

しかし、それがあなたがそれが意味すると思うものを意味するかどうかはわかりません。リンク先の3.7.6.1ドキュメントを見ると、次のように明示されています。

eRMSBを使用してmemcpyを実装すると、長さとアライメント係数に応じて、256ビットまたは128ビットAVXの代替を使用した場合と同じレベルのスループットに到達しない場合があります。

したがって、CPUIDがERMSBのサポートを示しているからといって、REP MOVSBがメモリをコピーする最速の方法になるという保証はありません。これは、以前のいくつかのCPUほどひどくないことを意味します。

ただし、特定の条件下でより高速に実行できる代替手段がある可能性があるからといって、REP MOVSBが役に立たないわけではありません。この命令で発生したパフォーマンスのペナルティはなくなったため、再び有用な命令になる可能性があります。

覚えておいてください、私が見たもっと複雑なmemcpyルーチンのいくつかと比較して、それはほんの少しのコード(2バイト!)です。コードの大きなチャンクをロードして実行することにもペナルティがあるため(CPUのキャッシュから他のコードの一部がスローされる)、AVXなどの「利点」が残りのコードへの影響によって相殺されることがありますコード。何をしているのかに依存します。

あなたも尋ねる:

REP MOVSBで帯域幅がこれほど低いのはなぜですか?改善するにはどうすればよいですか?

REP MOVSBをより高速に実行するために「何かをする」ことはできません。それがすることをします。

Memcpyから見た高速化が必要な場合は、そのソースを掘り下げることができます。どこかにあります。または、デバッガからトレースして、実際のコードパスを確認できます。私の期待は、これらのAVX命令のいくつかを使用して、一度に128または256ビットで動作することです。

または、あなたはただ...ええ、あなたは私たちにそれを言わないように頼みました。

7
David Wohlferd

これは述べられた質問に対する答えではなく、見つけようとするときの私の結果(および個人的な結論)だけです。

要約すると、GCCはすでにmemset()/memmove()/memcpy()を最適化しています(たとえば gcc/config/i386/i386.c:expand_set_or_movmem_via_rep()を参照 GCCソースで、同じファイルで_stringop_algs_も検索して、アーキテクチャ依存のバリアントを確認します)。したがって、GCCで独自のバリアントを使用することで大幅なゲインを期待する理由はありません(アライメントされたデータのアライメント属性などの重要な要素を忘れた場合、または_-O2 -march= -mtune=_のような十分に具体的な最適化を有効にしない限り)。同意する場合、記載されている質問に対する回答は実際には多かれ少なかれ無関係です。

memrepeat()と比較してmemcpy()の反対であるmemmove()があればよかったのに、バッファの最初の部分を繰り返してバッファ全体を埋めていきます。 )


現在、Ivy Bridgeマシンを使用しています(Core i5-6200Uラップトップ、Linux 4.4.0 x86-64カーネル、erms in _/proc/cpuinfo_ flags)。 _rep movsb_に基づくカスタムmemcpy()バリアントが単純なmemcpy()を上回る場合を見つけることができるかどうかを知りたかったので、非常に複雑なベンチマークを作成しました。

核となる考え方は、メインプログラムが3つの大きなメモリ領域を割り当てることです:originalcurrent、およびcorrect、それぞれまったく同じサイズで、少なくともページ揃え。コピー操作はセットにグループ化され、各セットはすべてのソースとターゲットが(ある程度のバイト数で)整列されているか、すべての長さが同じ範囲内にあるなど、個別のプロパティを持ちます。各セットは、srcdstnトリプレットの配列を使用して記述されます。ここで、すべてのsrcから_src+n-1_およびdstから_dst+n-1_は完全にcurrentエリア内にあります。

Xorshift * PRNGはoriginalをランダムデータに初期化するために使用されます。上記で警告したように、これは非常に複雑ですが、コンパイラの簡単なショートカットは残していません。)correct領域は、originalcurrentデータから開始し、現在のセットのすべてのトリプレットを適用して取得します。 Cライブラリが提供するmemcpy()を使用し、current領域をcorrectにコピーすることにより、ベンチマークされた各機能が正しく動作することを確認できます。

コピー操作の各セットは、同じ関数を使用して多数回計時され、これらの中央値が比較に使用されます。 (私の意見では、中央値はベンチマークで最も意味があり、賢明なセマンティクスを提供します-関数は少なくとも半分の時間で少なくともその速度です)

コンパイラーの最適化を回避するために、実行時にプログラムに関数とベンチマークを動的にロードさせます。関数はすべて同じ形式void function(void *, const void *, size_t)を持っています-memcpy()memmove()とは異なり、何も返さないことに注意してください。ベンチマーク(コピー操作の名前付きセット)は、関数呼び出し(currentエリアへのポインターとそのサイズをパラメーターとして取るなど)によって動的に生成されます。

残念ながら、私はまだセットを見つけていません

_static void rep_movsb(void *dst, const void *src, size_t n)
{
    __asm__ __volatile__ ( "rep movsb\n\t"
                         : "+D" (dst), "+S" (src), "+c" (n)
                         :
                         : "memory" );
}
_

打つだろう

_static void normal_memcpy(void *dst, const void *src, size_t n)
{
    memcpy(dst, src, n);
}
_

linux-4.4.0 64ビットカーネルを実行する前述のCore i5-6200UラップトップでGCC 5.4.0を使用して_gcc -Wall -O2 -march=ivybridge -mtune=ivybridge_を使用します。ただし、4096バイトの整列およびサイズ変更されたチャンクのコピーは近くなります。

これは、少なくともこれまでのところ、_rep movsb_ memcpyバリアントを使用するのが理にかなっているケースを見つけていないことを意味します。それはそのような場合がないという意味ではありません。まだ見つけていません。

(この時点で、コードは私が誇りに思っているよりも恥ずかしいスパゲッティの混乱なので、誰かが尋ねない限り、ソースの公開を省略します。しかし、上記の説明はより良いものを書くのに十分なはずです。)


しかし、これは私をそれほど驚かせません。 Cコンパイラは、オペランドポインターのアライメントに関する多くの情報、およびコピーするバイト数がコンパイル時の定数(2の適切な累乗の倍数)であるかどうかを推測できます。この情報は、Cライブラリのmemcpy()/memmove()関数を独自の関数に置き換えるために、コンパイラによって使用される可能性があります。

GCCはこれを正確に行います(たとえば、GCCソースで gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() を参照してください。また、同じファイルで_stringop_algs_を探して、アーキテクチャ依存のバリアントを確認してください)。実際、memcpy()/memset()/memmove()は、かなりの数のx86プロセッサバリアント用にすでに個別に最適化されています。 GCC開発者がermsのサポートをまだ含めていなかったら、私は非常に驚きました。

GCCは、開発者が適切な生成コードを保証するために使用できるいくつかの 関数属性 を提供します。たとえば、alloc_align (n)は、関数が少なくともnバイトにアライメントされたメモリを返すことをGCCに伝えます。アプリケーションまたはライブラリは、「リゾルバー関数」(関数ポインターを返す)を作成し、ifunc (resolver)属性を使用して関数を定義することにより、実行時に使用する関数の実装を選択できます。

このためにコードで使用する最も一般的なパターンの1つは

_some_type *pointer = __builtin_assume_aligned(ptr, alignment);
_

ここで、ptrは何らかのポインターであり、alignmentは整列されるバイト数です。 GCCは、pointeralignmentバイトに揃えられていることを認識/想定します。

正しく使用するのがはるかに難しいにもかかわらず、別の便利なビルトインは __builtin_prefetch() です。全体的な帯域幅/効率を最大化するために、各サブオペレーションのレイテンシーを最小化すると、最良の結果が得られることがわかりました。 (散在した要素を連続した一時ストレージにコピーする場合、プリフェッチには通常キャッシュライン全体が含まれるため、これは困難です。プリフェッチされる要素が多すぎると、未使用のアイテムを保存することでキャッシュのほとんどが無駄になります。)

6
Nominal Animal

データを移動するより効率的な方法があります。最近では、memcpyの実装により、データのメモリアライメントやその他の要因に基づいて最適化されたアーキテクチャ固有のコードがコンパイラから生成されます。これにより、x86の世界で一時的でないキャッシュ命令とXMMおよびその他のレジスタをより適切に使用できます。

rep movsbは、組み込み関数のこの使用を防ぎます。

したがって、memcpyのようなものについては、非常に特定のハードウェアに結び付けられるものを作成する場合、および高度に最適化されたmemcpy関数を作成するために時間をかける場合を除きますアセンブリー(またはCレベルの組み込み関数を使用)では、farコンパイラーがそれを理解できるようにする方が良いでしょう。

3
David Hoelzer

一般的なmemcpy()ガイドとして:

a)コピーされるデータが小さく(20バイト未満)、サイズが固定されている場合は、コンパイラーに実行させます。理由:コンパイラは通常のmov命令を使用して、起動時のオーバーヘッドを回避できます。

b)コピーされるデータが小さく(約4 KiB未満)、アライメントが保証されている場合は、rep movsb(ERMSBがサポートされている場合)またはrep movsd(ERMSBがサポートされていない場合)を使用します。理由:SSEまたはAVXの代替を使用すると、何かをコピーする前に大量の「起動オーバーヘッド」が発生します。

c)コピーされるデータが小さく(約4 KiB未満)、アライメントが保証されない場合は、rep movsbを使用します。理由:SSEまたはAVX、またはその大部分にrep movsdに加えて開始または終了時にrep movsbを使用すると、オーバーヘッドが大きすぎます。

d)他のすべての場合には、次のようなものを使用します。

    mov edx,0
.again:
    pushad
.nextByte:
    pushad
    popad
    mov al,[esi]
    pushad
    popad
    mov [edi],al
    pushad
    popad
    inc esi
    pushad
    popad
    inc edi
    pushad
    popad
    loop .nextByte
    popad
    inc edx
    cmp edx,1000
    jb .again

理由:これは非常に遅いため、プログラマーは巨大なデータのコピーを必要としない代替手段を見つける必要があります。また、大量のデータのコピーが回避されるため、結果のソフトウェアは大幅に高速になります。

1
Brendan