1. ホーム
  2. performance

[解決済み] memcpyのための拡張REP MOVSB

2023-05-31 12:30:45

質問

拡張 REP MOVSB (ERMSB) を使用して、高帯域幅のカスタムの memcpy .

ERMSB は Ivy Bridge マイクロアーキテクチャーで導入されました。の "Enhanced REP MOVSB and STOSB operation (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/s を取得し memcpy は 26 GB/s、i7-6700HQ (Skylake) システム、Ubuntu 16.10、DDR4@2400 MHz デュアル チャネル 32 GB、GCC 6.2 では 26 GB/s となります。

で帯域幅がこれほど低いのはなぜですか? 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 を打ち負かすことができることに注意してください。 Ivybridge および Haswell でメモリにストリーミングする場合、rep movsb は movntdqa よりも大幅に高速です (ただし、Ivybridge 以前では遅いので注意してください!)。

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


以下は、同じシステムでの私の結果です。 からの結果です。 .

 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 prefetched よりも速いです。 MOVSB copy .


私の最初のテストでは、ターボを無効にしていませんでした。ターボを無効にして再度テストしましたが、大きな違いはないように見えます。 しかし、パワー マネジメントを変更すると、大きな違いが生じます。

私が

sudo cpufreq-set -r -g performance

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

sudo cpufreq-set -r -g powersave

を見ると、17GB/sくらいが最高です。 しかし memcpy はパワーマネージメントに敏感ではないようです。


私は周波数をチェックしました ( turbostat ) SpeedStepを有効にした場合としない場合 である。 performancepowersave で、アイドル時、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 . これは performance SpeedStep を使用しない場合のみ、CPU は一定の周波数で動作します。

私は、例えば sudo cpufreq-set -r performance (なぜなら cpufreq-set で電源設定を変更しました。これはターボをオンに戻すので、その後ターボを無効化する必要がありました。

どのように解決しましたか?

これは私の心に非常に近いトピックであり、最近の調査でもあります。そこで、歴史、いくつかの技術的なメモ(主に学術的な)、私のボックスでのテスト結果、そして最後に、いつ、どこで、というあなたの実際の質問に答える試みについて、いくつかの角度から見てみたいと思います。 rep movsb が意味をなすかもしれないというあなたの実際の質問に答える試みです。

部分的に、これは 結果を共有するための呼びかけ - を実行できれば テニミュベンチ を実行し、その結果を CPU と RAM の構成の詳細とともに共有していただけると幸いです。特に、4 チャンネル セットアップ、Ivy Bridge ボックス、サーバー ボックスなどをお持ちの場合です。

歴史と公式アドバイス

つまり、パフォーマンスが停滞している期間と、競合するアプローチと同等かそれ以上に速くなるような大きなアップグレードが交互に繰り返されてきました。例えば、Nehalemでは(主に起動時のオーバーヘッドをターゲットに)性能が向上し、Ivy Bridgeでは(主に大規模コピーの総スループットをターゲットに)再び性能が向上しました。を実装することの難しさについて、10年前の洞察を見出すことができます。 rep movs インテルエンジニアからの指示 このスレッドで .

たとえば、Ivy Bridge の導入に先立つガイドでは、典型的な アドバイス を避けるか、または非常に慎重に使用するようにとあります。 1 .

現在 (つまり 2016 年 6 月) のガイドには、次のようなさまざまなわかりにくい、やや一貫性に欠けるアドバイスがあります。 2 :

<ブロッククオート

実装の特定のバリアントは実行時に選択されます。 は、データレイアウト、アライメント、カウンタ(ECX)の値に基づいて、実行時に選択されます。例えば 例えば、REP接頭辞を持つMOVSB/STOSBは、最高のパフォーマンスを得るために、カウンターの値が3以下の場合に使用されるべきです。 の値が 3 以下であれば、最高のパフォーマンスを発揮します。

では、3バイト以下のコピーの場合?は必要ありません。 rep プレフィックスはそもそも必要ありません。 mov を使用し、未使用のバイトをマスクするために少しビット操作する(または、2 つの明示的なバイト、ワード mov を使用することもできます。)

彼らは続けて言う。

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

これは確かに、ERMSB を備えた現在のハードウェアでは間違っているようです。 rep movsb と同等かそれ以上の速さで movd または movq の変数を使用します。

一般に、現在のガイドのそのセクション (3.7.5) には、妥当なアドバイスとひどく時代遅れのアドバイスが混在しています。Intel のマニュアルは、各アーキテクチャごとに段階的に更新され (現在のマニュアルでも約 20 年分のアーキテクチャをカバーしている)、古いセクションは、現在のアーキテクチャに適用されないアドバイスを置き換えたり条件付けたりするために更新されないことがよくあるため、これは一般的なことです。

その後、彼らは 3.7.6 節で ERMSB を明示的にカバーするようになりました。

残りのアドバイスを網羅することはしませんが、良い部分を以下の「なぜそれを使うのか」にまとめます。

このガイドの他の重要な主張として、Haswell 上では rep movsb は内部で 256 ビットのオペレーションを使用するように強化されたことです。

技術的考察

が持つ根本的なメリットとデメリットを簡単にまとめてみました。 rep 命令と 実装の観点から .

の利点 rep movs

  1. が表示された場合 rep movs 命令が発行されると、CPU は が知っている は、既知のサイズのブロック全体が転送されることを認識します。これは、たとえば、離散命令ではできない方法で操作を最適化するのに役立ちます。

    • キャッシュ ライン全体が上書きされることがわかっている場合、RFO 要求を回避する。
    • プリフェッチ要求を即座にかつ正確に発行すること。ハードウェアプリフェッチは、以下の検出において良い仕事をします。 memcpy -のようなパターンを検出するのは良い仕事ですが、それでも起動に数回読み込みが必要で、コピーされた領域の終わりを越えて多くのキャッシュラインを "オーバープリフェッチ" することになります。 rep movsb は正確に領域サイズを知っており、正確にプリフェッチすることができます。
  2. 内の店舗間の順序を保証するものではないらしい。 3 単一の rep movs これは、coherency トラフィックやブロック移動の他の側面を簡素化するのに役立ちますが、単純な mov 命令と比較して、より厳密なメモリ順序に従わなければならない 4 .

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

デメリット

  1. rep movsb は、基本的なソフトウェア要件よりも強いかもしれない特定のセマンティックを実装する必要があります。特に memcpy はリージョンが重なることを禁止しているので、その可能性を無視するかもしれませんが rep movsb はそれらを許容し、期待された結果を出さなければなりません。現在の実装では、ほとんどが起動時のオーバーヘッドに影響しますが、おそらくラージブロックのスループットには影響しないでしょう。同様に rep movsb はバイト単位のコピーをサポートしなければならず、たとえ実際にそれを使って大きな2のべき乗の倍数である大きなブロックをコピーしていたとしてもです。

  2. を使用する場合、ソフトウェアはアライメント、コピーサイズ、およびハードウェアに伝えられないエイリアシングの可能性についての情報を持っているかもしれません。 rep movsb . コンパイラーは多くの場合、メモリ ブロックのアライメントを決定することができます。 6 のようなスタートアップ作業の多くを回避することができます。 rep movs で行わなければならない ごとに を呼び出す必要があります。

テスト結果

以下は、様々なコピー方法でのテスト結果です。 tinymembench を 2.6 GHz の i7-6700HQ で実行しました (同じ 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 メソッドは "non-temporal" でない他のすべてのメソッドより高速です。 7 また、一度に 8 バイトをコピーする "C" アプローチよりもかなり高速です。
  • non-temporal"メソッドよりも最大で約 26% 高速です。 rep movs しかし、これは報告されたものよりもはるかに小さな差です (26 GB/s vs 15 GB/s = ~73%)。
  • 非一時的なストアを使用しない場合、C からの 8 バイトのコピーを使用すると、128 ビット幅の SSE ロード/ストアとほぼ同じになります。それは、優れたコピー ループが帯域幅を飽和させるのに十分なメモリ圧力を生成できるからです (たとえば、2.6 GHz * 1 store/cycle * 8 バイト = store では 26 GB/秒)。
  • tinymembench には明示的な 256 ビット アルゴリズムはありません (おそらく "standard" を除きます)。 memcpy を除く) が、上記の注意事項により、おそらく重要ではないでしょう。
  • 非一時的ストア アプローチの一時的ストア アプローチに対するスループットの増加は約 1.45 倍であり、これは NT が 3 つの転送のうち 1 つを削除する場合に期待される 1.5 倍に非常に近い値です (すなわち、NT の 1 読み、1 書きに対して 2 読み、1 書きの場合)。その rep movs アプローチは中間に位置しています。
  • かなり低いメモリ レイテンシと適度な 2 チャネル帯域幅の組み合わせは、この特定のチップがたまたまシングル スレッドからそのメモリ帯域幅を飽和させることができたことを意味し、これにより動作が劇的に変化します。
  • rep movsd と同じマジックを使っているようです。 rep movsb と同じ魔法を使っているようです。ERMSB が明示的にターゲットにしているのは movsb をターゲットにしており、ERMSB を使った以前のアーキでのテストでは movsb よりもはるかに高速に動作しています。 movsd . これはほとんど学術的なものです。 movsb よりも一般的だからです。 movsd よりも一般的です。

ハスウェル

を見てみると 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 のバージョンで使用できます。以下の利点のいくつかは、これらの選択肢のどちらか一方と比較してのみ適用されます (たとえば、"simplicity" は手巻きバージョンに対しては役立ちますが、組み込みの memcpy に対してではなく)、いくつかは両方に適用されます。

使用できる命令の制限

環境によっては、特定の命令や特定のレジスタの使用に制約がある場合があります。例えば、Linux カーネルでは、SSE/AVX や FP レジスタの使用は一般に許可されません。そのため、ほとんどの最適化された memcpy の変種は SSE や AVX レジスタに依存しているため使用できず、 64 ビットのプレーンな mov -ベースのコピーは x86 で使用されます。これらのプラットフォームでは rep movsb を使うことで、最適化された memcpy を使用することで、SIMD コードに対する制限を破ることなく、最適化された

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

フューチャープルーフィング

の素晴らしい側面は rep movsb はそれができることです。 理論的には は、明示的な移動では不可能な、将来のアーキテクチャにおけるアーキテクチャの改良を、ソースの変更なしに利用することができるということです。たとえば、256 ビットのデータ パスが導入されたとき。 rep movsb は、ソフトウェアに変更を加えることなく、(Intel が主張するように)その利点を活用することができました。128 ビットの移動を使用するソフトウェア (Haswell より前は最適だった) は、変更および再コンパイルが必要になります。

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

これがどれほど重要かは、メンテナンス モデル (たとえば、新しいバイナリが実際に展開される頻度) と、これらの命令が将来どのくらい速くなるかという非常に難しい判断に依存します。少なくとも、Intel はこの方向で利用を誘導しており、少なくとも 合理的な のパフォーマンスを約束することで、この方向に誘導しています ( 15.3.3.6 ):

REP MOVSB と REP STOSB は、将来のプロセッサでもそれなりに良いパフォーマンスを発揮し続けるでしょう。 は、将来のプロセッサーでもそれなりの性能を発揮し続けるでしょう。

後続の作業と重なる

この特典は、プレーンな memcpy ベンチマークではもちろん表示されません。ベンチマークは定義上、後続の作業が重ならないので、利益の大きさは実際のシナリオで慎重に測定されなければなりません。最大限の利点を得るには、 の周囲のコードを再整理する必要があるかもしれません。 memcpy .

この利点は Intel が最適化マニュアル (11.16.3.4 節) の中で指摘しており、彼らの言葉にもある通りです。

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

- memset() の 256 ビット SIMD 実装は、32 バイトのストア操作の 128 インスタンスを発行/実行/終了する必要があります。 VMOVDQA を使用した 32 バイトのストア操作の 128 インスタンスを発行/実行する必要があります。 非消費型命令シーケンスが破棄される前に、VMOVDQA で 32 バイトのストア操作の 128 インスタンスを発行/実行する必要があります。 引退させる必要があります。

- ECX= 4096の拡張REP STOSBインスタンスは、ハードウェアによって提供される長いマイクロオペレーションフローとしてデコードされます。 ハードウェアによって提供される長いマイクロオペレーションフローとしてデコードされますが、1 つの命令としてリタイアします。 命令としてリタイアします。memset() の結果が完了する前に、完了しなければならない多くの store_data 操作があります。 memset()の結果が消費される前に完了しなければならない多くのstore_data操作があります。なぜなら ストアデータ操作の完了は、プログラム順序のリタイアメントから切り離されているため 消費されないコードストリームのかなりの部分が、発行/実行/リタイアを経て処理できる。 消費しないコードストリームの相当部分は、発行/実行とリタイアメントを通じて処理することができます。 非消費型シーケンスがストアバッファリソースを競合しない場合は、実質的にコストなしで、発行/実行とリタイアを処理できます。

つまり、Intelは、いくつかのuopsの後、コード rep movsb が発行されましたが、多くのストアがまだ飛行中で、その間に rep movsb が発行されますが、多くのストアがまだ飛行しており、全体としてまだ引退していないため、後続の命令からの uops は、そのコードがコピー ループの後に来た場合よりも、アウトオブオーダーの機械を通してより進歩することができます。

明示的なロードとストアのループからの uops はすべて、プログラム順序で別々に実際に引退する必要があります。 これは、ROB で後続の uops のためのスペースを確保するために起こる必要があります。

のような非常に長いマイクロコード化された命令がどのように動作するかについての詳細な情報はあまりないようです。 rep movsb のような非常に長いマイクロコード命令がどのように動作するかについて、正確にはあまり詳細な情報がないようです。 マイクロコード分岐がどのようにマイクロコードシーケンサーから uops の別のストリームを要求するか、または uops がどのようにリタイアするかについては、正確には分かっていません。 個々の uops が別々に引退する必要がない場合、おそらく命令全体が ROB の 1 つのスロットを占有するだけでしょうか。

OoO マシンに供給するフロントエンドが rep movsb 命令を見ると、マイクロコードシーケンサー ROM (MS-ROM) が起動し、マイクロコード uops を発行/リネームステージに供給するキューに送信します。 おそらく、他のuopがそこに混じって発行/実行することはできないでしょう。 8 一方 rep movsb がまだ発行されている間、後続の命令はフェッチ/デコードされ、最後の rep movsb が実行された直後に、後続の命令をフェッチ/デコードして発行することができます。 この方法は、後続のコードの少なくとも一部が の結果に依存しない場合にのみ有効です。 memcpy の結果に依存しない場合のみ有効です (これは珍しいことではありません)。

さて、この恩恵の大きさは限られています。最大でN個の命令(実際にはuops)を実行することができ、遅い rep movsb 命令を超えて実行することができ、その時点で失速します。 ROB サイズ . 200 サイクルでは、10 GB/s で約 800 バイトをコピーできるため、そのサイズのコピーでは、コピーのコストに近い無料作業が得られるかもしれません (ある意味、コピーが無料になります)。

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

コピー ループは、後続の命令の実行を完全にブロックするわけでもありません。 Intel は、利益の大きさ、またはどのような種類のコピーまたは周囲のコードに最も利益があるかについて、詳細には説明しません。 (ホットまたはコールドのデスティネーションまたはソース、高 ILP または低 ILP の高レイテンシー コードの後)。

コード サイズ

実行されるコードサイズ(数バイト)は、典型的な最適化された memcpy ルーチンに比べて微々たるものです。パフォーマンスが i-cache (uop キャッシュを含む) のミスによって制限されている場合、縮小されたコード サイズは有益である可能性があります。

繰り返しになりますが、コピーのサイズに基づいて、この利点の大きさを制限することができます。実際に数値で計算することはしませんが、直感的には、動的なコード サイズを B バイト削減することで、最大で C * B のキャッシュミスを減らすことができるということです。 呼び出し への memcpy を呼び出すと、キャッシュミスのコスト (または利益) が一度だけ発生しますが、高いスループットの利点はコピーされるバイト数でスケールします。そのため、大きな転送では、より高いスループットがキャッシュの効果よりも優位になります。

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

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

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

文字列の移動操作は定期的に愛されているようなので、それはまったくもっともなことです。しかし、すべての世代ではないので、最新にされたアーキテクチャでは高速か、少なくとも同点(その時点で他の優位性に基づいて勝つかもしれません)かもしれませんが、その後のハードウェアでは遅れをとることになります。

引用 Andy Glew氏は、P6にこれらを実装した後、このことについて何か知っているはずです。

マイクロコードで高速な文字列を行うことの大きな弱点は、[...] です。 マイクロコードは世代が進むごとに調子を崩し、どんどん遅くなっていきました。 遅くなり、誰かがそれを修正するまでに時間がかかることです。ちょうど、ライブラリのメン が調子を崩してしまうのと同じです。その一つが、128ビットロードの採用だったということなのでしょう。 128ビットのロードとストアが使えるようになったときに、その機会を逃したということもあるでしょう。

その場合、それは典型的なあらゆる仕掛けで適用する、別の "プラットフォーム固有の"最適化と見なすことができます。 memcpy ルーチンを適用するための、プラットフォーム固有の最適化の 1 つと考えることができます。JIT や AOT コンパイルされたものではこれは簡単ですが、静的にコンパイルされたバイナリでは、プラットフォーム固有のディスパッチが必要になりますが、これはすでに存在していることが多く (リンク時に実装されていることもあります)、あるいは mtune 引数を使用して静的な決定を行うことができます。

簡略化

Skylake では、絶対的に高速な非一時的手法に遅れをとったように見えますが、それでもほとんどのアプローチよりも高速であり、また
非常にシンプルで

. これは、検証にかかる時間が短く、謎のバグが少なく、チューニングやモンスターの更新にかかる時間が短いことを意味します。 memcpy の実装のチューニングやアップデートにかかる時間が少なくなります (逆に、標準ライブラリの実装に依存している場合は、その気まぐれさに依存することも少なくなります)。

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

メモリスループット制限アルゴリズム 9 は、実際には 2 つの主要な全体レジームで動作することができます。DRAM バンド幅境界または同時実行/待ち時間境界です。

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% 未満にしかなりません。

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

まだ 最も 最近の CPU は この 要素によって制限され、RAM 帯域幅ではありません。通常、コアあたり 12 ~ 20 GB/s を達成しますが、RAM 帯域幅は 50 GB/s 以上 (4 チャンネル システムの場合) になることがあります。最近の一部の 2 チャネル クライアント コアだけが、より優れたアンコア、おそらくより多くのライン バッファを持ち、単一コアでの DRAM 制限を満たすことができます。

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

なぜ、こんな話を延々と続けているのか。なぜなら、最高の memcpy の実装は、どの領域で動作しているかに依存することが多いからです。DRAM の帯域幅が制限されると (私たちのチップは明らかにそうですが、ほとんどのチップはシングル コアではそうではありません)、非一時的な書き込みを使用することが非常に重要になり、通常帯域幅の 1/3 を浪費する所有者用の読み取りを省くことができます。上記のテスト結果で、まさにこのことがわかります。 でない を使用する memcpy 実装は、帯域幅の 1/3 を失います。

しかし、同時実行が制限されている場合、状況は均等化し、時には逆転します。なぜなら、ライン バッファのハンドオフ時間が、プリフェッチによって RFO ラインを LLC (または L2) に導き、LLC でストアを完了させるシナリオよりも長くなり、レイテンシーが効果的に低くなる可能性があるためです。最後に サーバー アンコールでは、クライアント ストアよりも遅い NT ストア (および高帯域幅) を使用する傾向があるため、この効果が顕著になります。

したがって、他のプラットフォームでは、NT ストアは (少なくともシングルスレッド パフォーマンスを気にする場合は) あまり有用ではなく、おそらく rep movsb が勝ります (両方の長所を得られる場合)。

本当に、この最後の項目は、ほとんどのテストのための呼びかけです。私は、NT ストアがほとんどのアーキテクチャ (現在のサーバー アーキテクチャを含む) でシングルスレッド テストに対する見かけ上の優位性を失っていることを知っていますが、どのようにして rep movsb が相対的にどのように動作するのかわかりません...。

参考文献

上記に含まれていない、その他の良い情報源です。

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


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

2 参照 3.7.5 節を参照してください。 REP Prefixとデータ移動。

3 これは、1つの命令内のさまざまなストアにのみ適用されることに注意することが重要です。いったん完了すると、ストアのブロックは依然として前後のストアに対して順序付けされて表示されます。そのため、コードでは rep movs からのストアが順序を無視して のストアが互いに対して とは異なりますが、前後のストアとは異なります(そして、通常必要とされるのは後者の保証です)。 これは、個別のストアではなく、コピー先の終了を同期フラグとして使用する場合にのみ問題になります。

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

5 これは 32 ビット時代の後半によく見られたことで、多くのチップが 64 ビットのデータパスを備えていました (たとえば、64 ビットをサポートしていた FPU をサポートするため)。 double 型に対応していた FPU をサポートするため)。今日、Pentium や Celeron ブランドのような「去勢された」チップでは AVX が無効になっていますが、おそらくは rep movs マイクロコードはまだ 256b のロード/ストアを使用することができます。

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

7 私は、"standard"という前提で話を進めています。 memcpy が非一時的なアプローチを選択しているという前提で、このバッファーのサイズではその可能性が高いです。

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

9 つまり、多数の独立したメモリ要求を発行できるため、利用可能な DRAM コア間帯域幅を飽和させるもので、そのうち memcpy ポインターの追跡のような純粋に待ち時間の長い負荷とは異なります)。