web-dev-qa-db-ja.com

現在のx86アーキテクチャは、(「通常の」メモリからの)非一時的なロードをサポートしていますか?

このトピックに関する複数の質問を認識していますが、明確な回答もベンチマーク測定値も見ていません。したがって、2つの整数配列を処理する単純なプログラムを作成しました。最初の配列aは非常に大きく(64 MB)、2番目の配列bはL1キャッシュに収まるように小さくなっています。プログラムはaを反復処理し、その要素をモジュールの意味でbの対応する要素に追加します(bの終わりに達すると、プログラムは最初から再開します) 。さまざまなサイズのbで測定されたL1キャッシュミスの数は次のとおりです。

enter image description here

測定は、32 kiBL1データキャッシュを備えたXeonE5 2680v3HaswellタイプのCPUで行われました。したがって、すべての場合において、bはL1キャッシュに適合します。ただし、ミスの数は、約16kiBのbメモリフットプリントによって大幅に増加しました。 abの両方のロードにより、この時点でbの先頭からキャッシュラインが無効になるため、これは予想されることです。

aの要素をキャッシュに保持する理由はまったくありません。これらは、一度だけ使用されます。したがって、aデータの非一時的なロードを使用してプログラムバリアントを実行しましたが、ミスの数は変わりませんでした。また、aデータの非一時的なプリフェッチを使用してバリアントを実行しましたが、それでもまったく同じ結果が得られました。

私のベンチマークコードは次のとおりです(非一時的なプリフェッチなしのバリアントが表示されています)。

int main(int argc, char* argv[])
{
   uint64_t* a;
   const uint64_t a_bytes = 64 * 1024 * 1024;
   const uint64_t a_count = a_bytes / sizeof(uint64_t);
   posix_memalign((void**)(&a), 64, a_bytes);

   uint64_t* b;
   const uint64_t b_bytes = atol(argv[1]) * 1024;
   const uint64_t b_count = b_bytes / sizeof(uint64_t);
   posix_memalign((void**)(&b), 64, b_bytes);

   __m256i ones = _mm256_set1_epi64x(1UL);
   for (long i = 0; i < a_count; i += 4)
       _mm256_stream_si256((__m256i*)(a + i), ones);

   // load b into L1 cache
   for (long i = 0; i < b_count; i++)
       b[i] = 0;

   int papi_events[1] = { PAPI_L1_DCM };
   long long papi_values[1];
   PAPI_start_counters(papi_events, 1);

   uint64_t* a_ptr = a;
   const uint64_t* a_ptr_end = a + a_count;
   uint64_t* b_ptr = b;
   const uint64_t* b_ptr_end = b + b_count;

   while (a_ptr < a_ptr_end) {
#ifndef NTLOAD
      __m256i aa = _mm256_load_si256((__m256i*)a_ptr);
#else
      __m256i aa = _mm256_stream_load_si256((__m256i*)a_ptr);
#endif
      __m256i bb = _mm256_load_si256((__m256i*)b_ptr);
      bb = _mm256_add_epi64(aa, bb);
      _mm256_store_si256((__m256i*)b_ptr, bb);

      a_ptr += 4;
      b_ptr += 4;
      if (b_ptr >= b_ptr_end)
         b_ptr = b;
   }

   PAPI_stop_counters(papi_values, 1);
   std::cout << "L1 cache misses: " << papi_values[0] << std::endl;

   free(a);
   free(b);
}

私が疑問に思うのは、CPUベンダーが非一時的なロード/プリフェッチ、または一部のデータをキャッシュに保持されていないものとしてラベル付けする方法(たとえば、LRUとしてタグ付けする方法)をサポートするか、サポートする予定かどうかです。 HPCなど、実際には同様のシナリオが一般的である状況があります。たとえば、スパース反復線形ソルバー/固有ソルバーでは、行列データは通常非常に大きい(キャッシュ容量よりも大きい)が、ベクトルはL3またはL2キャッシュに収まるほど小さい場合があります。それなら、どうしてもそこに置いておきたいと思います。残念ながら、各ソルバーの反復で行列要素が1回だけ使用され、処理後にそれらをキャッシュに保持する理由がない場合でも、行列データのロードにより、特にxベクトルキャッシュラインが無効になる可能性があります。

[〜#〜]更新[〜#〜]

L1ミスの代わりにランタイムを測定しながら、Intel Xeon Phi KNCで同様の実験を行いました(確実に測定する方法が見つかりません。PAPIとVTuneは奇妙なメトリックを提供しました)。結果​​は次のとおりです。

enter image description here

オレンジ色の曲線は通常の荷重を表しており、予想される形状をしています。青い曲線は、命令プレフィックスにいわゆるエビクションヒント(EH)が設定された負荷を表し、灰色の曲線は、aの各キャッシュラインが手動でエビクトされた場合を表します。 KNCによって有効にされたこれらのトリックは両方とも、16kiBを超えるbに対して期待どおりに機能したことは明らかです。測定されたループのコードは次のとおりです。

while (a_ptr < a_ptr_end) {
#ifdef NTLOAD
   __m512i aa = _mm512_extload_epi64((__m512i*)a_ptr,
      _MM_UPCONV_EPI64_NONE, _MM_BROADCAST64_NONE, _MM_HINT_NT);
#else
   __m512i aa = _mm512_load_epi64((__m512i*)a_ptr);
#endif
   __m512i bb = _mm512_load_epi64((__m512i*)b_ptr);
   bb = _mm512_or_epi64(aa, bb);
   _mm512_store_epi64((__m512i*)b_ptr, bb);

#ifdef EVICT
   _mm_clevict(a_ptr, _MM_HINT_T0);
#endif

   a_ptr += 8;
   b_ptr += 8;
   if (b_ptr >= b_ptr_end)
       b_ptr = b;
}

UPDATE 2

Xeon Phiでは、a_ptrの通常負荷バリアント(オレンジ色の曲線)のプリフェッチ用にicpcが生成されます。

400e93:       62 d1 78 08 18 4c 24    vprefetch0 [r12+0x80]

手動で(実行可能ファイルを16進編集することにより)これを次のように変更した場合:

400e93:       62 d1 78 08 18 44 24    vprefetchnta [r12+0x80]

青/灰色の曲線よりもさらに良い、希望の結果が得られました。ただし、ループの前に#pragma prefetch a_ptr:_MM_HINT_NTAを使用しても、コンパイラーに非一時的なプリフェッチを強制的に生成させることはできませんでした:(

47
Daniel Langr

具体的に見出しの質問に答えるには:

はい、最近1 主流のIntelCPUは、normalで非一時的な負荷をサポートします 2メモリ-ただし、movntdqaのような非一時的なロード命令を直接使用するのではなく、非一時的なプリフェッチ命令を介して「間接的に」のみ。これは、対応する非一時的なストアの指示を使用できる非一時的なストアとは対照的です。3 直接。

基本的な考え方は、通常のロードの前にキャッシュラインにprefetchntaを発行してから、通常どおりにロードを発行することです。その行がまだキャッシュにない場合は、非一時的な方法でロードされます。非テンポラルファッションの正確な意味はアーキテクチャによって異なりますが、一般的なパターンは、ラインが少なくともL1およびおそらくいくつかのより高いキャッシュレベルにロードされることです。実際、プリフェッチを使用するには、後でロードするために、ラインを少なくともsomeキャッシュレベルにロードする必要があります。この行は、キャッシュ内で特別に処理される場合もあります。たとえば、エビクションの優先度が高いことを示すフラグを立てたり、配置方法を制限したりします。

このすべての結果は、非時間的負荷はある意味でsupportですが、実際には痕跡を残さないストアとは異なり、実際には部分的に非時間的であるということです。いずれかのキャッシュレベルの行。非一時的なロードはsomeキャッシュの汚染を引き起こしますが、通常は通常のロードよりも少なくなります。正確な詳細はアーキテクチャ固有であり、最新のIntelについて以下にいくつかの詳細を含めました(少し長い記事を見つけることができます この回答で )。

Skylakeクライアント

テストに基づくと この回答ではprefetchnta Skylakeの動作は、通常はL1キャッシュにフェッチし、L2を完全にスキップし、限られた方法でL1キャッシュにフェッチするようです。 L3キャッシュ(おそらく1つまたは2つの方法のみであるため、ntaプリフェッチで使用できるL3の合計量は制限されます)。

これは Skylakeクライアント でテストされましたが、この基本的な動作はおそらくSandy Bridge以前(Intel最適化ガイドの文言に基づく)にまで遡り、KabyLake以降のアーキテクチャに基づいて転送されると思います。 Skylakeクライアントで。したがって、Skylake-SPまたはSkylake-Xパーツ、あるいは非常に古いCPUを使用していない限り、これはおそらくprefetchntaから期待できる動作です。

Skylakeサーバー

動作が異なることがわかっている最近のIntelチップは Skylakeサーバー (Skylake-X、Skylake-SP、およびその他のいくつかの行で使用されています)のみです。これにより、L2およびL3アーキテクチャが大幅に変更され、L3にははるかに大きなL2が含まれなくなりました。このチップの場合、prefetchntabothL2キャッシュとL3キャッシュをスキップするようです。したがって、このアーキテクチャでは、キャッシュの汚染はL1に限定されます。

この動作は ユーザーMysticialがコメントで報告 でした。これらのコメントで指摘されているように、欠点は、これによりprefetchntaがはるかに脆弱になることです。プリフェッチ距離またはタイミングが間違っている場合(特にハイパースレッディングが含まれ、兄弟コアがアクティブな場合は簡単です)、およびデータ使用する前にL1から削除されると、以前のアーキテクチャのL3ではなくメインメモリに戻ります。


1最近ここではおそらく過去10年ほどのことを意味しますが、以前のハードウェアが非一時的なプリフェッチをサポートしていなかったことを意味するわけではありません。サポートする可能性があります。 prefetchntaの紹介に戻りますが、それをチェックするハードウェアがなく、既存の信頼できる情報源を見つけることができません。

2Normalここでは、WB(ライトバック)メモリを意味します。これは、アプリケーションレベルで圧倒的多数の時間を処理するメモリです。

3 具体的には、NTストア命令は汎用レジスタの場合はmovntiであり、movntd*およびmovntp*SIMDレジスタのファミリ。

5
BeeOnRope

Intel Developer Forumから次の投稿を見つけたので、私は自分の質問に答えます。これは私にとって理にかなっています。それはジョン・マッカルピンによって書かれました:

主流のプロセッサの結果は驚くべきものではありません。真の「スクラッチパッド」メモリがない場合、厄介な驚きの影響を受けない「非一時的な」動作の実装を設計できるかどうかは明らかではありません。 過去に使用された2つのアプローチは、(1)キャッシュラインをロードするが、MRUではなくLRUをマークすることと、(2)キャッシュラインを1つの特定の「セット」にロードすることです。セットアソシアティブキャッシュ。いずれの場合も、プロセッサがデータの読み取りを完了する前にキャッシュがデータをドロップする状況を生成するのは比較的簡単です。

これらのアプローチはどちらも、少数のアレイで動作する場合にパフォーマンスが低下するリスクがあり、ハイパースレッディングを考慮すると「落とし穴」なしで実装するのがはるかに困難になります。

他の文脈では、キャッシュラインの内容全体がアトミックにレジスタにコピーされることを保証する「複数ロード」命令の実装について議論しました。私の推論は、ハードウェアがキャッシュラインがアトミックに移動することを絶対に保証し、キャッシュラインの残りをレジスタにコピーするのに必要な時間が非常に短い(プロセッサの世代に応じて余分な1〜3サイクル)ことを保証しているからです。アトミック操作として安全に実装されます。

Haswell以降、コアは1サイクルで64バイトを読み取ることができるため(256ビットで整列された2つのAVX読み取り)、意図しない副作用への露出はさらに低くなります。

L1データキャッシュからコアへの転送はフルキャッシュラインであり、すべてのデータがターゲットAVX-512レジスタに配置されるため、KNL以降、フルキャッシュライン(整列)ロードは「自然に」アトミックである必要があります。 (これは、Intelが実装の原子性を保証することを意味するものではありません!設計者が説明しなければならない恐ろしいコーナーケースを可視化することはできませんが、ほとんどの場合と結論付けるのは合理的です整列された512ビットのロードはアトミックに発生します。)この「自然な」64バイトのアトミック性により、「非一時的な」ロードによるキャッシュの汚染を減らすために過去に使用されたトリックのいくつかは、別の見方に値するかもしれません。 ...。


MOVNTDQA命令は、主に「Write-Combining」(WC)としてマップされたアドレス範囲からの読み取りを目的としており、「Write-Back」(ライトバック」としてマップされた通常のシステムメモリからの読み取りを目的としていません。 WB)。 SWDMの第2巻の説明では、実装はWB領域のMOVNTDQAで特別なことを「行う可能性がある」と述べていますが、WCメモリタイプの動作に重点が置かれています。

「Write-Combining」メモリタイプは、「実際の」メモリにはほとんど使用されません---メモリマップドIO領域にほぼ排他的に使用されます。

投稿全体については、こちらをご覧ください: https://software.intel.com/en-us/forums/intel-isa-extensions/topic/597075

2
Daniel Langr