インテル® C++ コンパイラー 17.0 デベロッパー・ガイドおよびリファレンス

フォルス・シェアリング

キャッシュラインに分割された複数のレベルのキャッシュは、最近のコンピューターのメモリーに高速アクセスします。フォルス・シェアリングは、メモリー共有型の並列処理でよく起こる問題です。2 つ以上のコアが同じキャッシュラインのコピーを保持する場合に発生します。

1 つのコアがキャッシュラインの変数に書き込むと、その他のすべてのコアでそのキャッシュライン全体が無効になります。別のコアがそのデータを使用 (読み取り/書き込み) していなくても、同じキャッシュラインにある別のデータ要素を使用している可能性があります。別のコアは、データにアクセスする前にキャッシュラインをリロードする必要があります。

キャッシュ・ハードウェアはデータの一貫性を保証しますが、フォルス・シェアリングが頻繁に発生する場合は、パフォーマンスが低下します。フォルス・シェアリングの問題は、ハードウェア・カウンターやその他のパフォーマンス・ツールを使用して、予期できない最終レベルのキャッシュミスが増加するかどうか見分けることで発見できます。

以下の簡単な例について考えてみます。

…
int a, b, c;
cilk_spawn f(&b);
c++;
…

コンパイラーは、変数 a、bc を同じキャッシュラインに配置する可能性があります。 変数が同じキャッシュラインに配置されると、スポーンされた関数 fb にアクセスし、親が c にアクセスするときに、変数 bc の間でフォルス・シェアリングが発生します。 このような場合、avoid_false_share 属性/declspec を使用します。あるいは、アライメント・ディレクティブを使用して宣言を再構築するか、フィールド・アライメントを指定して変数を struct に格納し、キャッシュラインの共有を回避します。

以下は、avoid_false_share 属性を使用した同じコードです。

…
int a;
__attribute__((avoid_false_share)) int b; 
// For Windows® use __declspec(avoid_false_share) int b;
int c;
cilk_spawn f(&b);
c++;
…

別の簡単な例として、配列要素の値をインクリメントする for ループを含む関数がスポーンされる状況について考えてみましょう。 この配列は volatile であるため、コンパイラーはレジスターに値を保持したり、ループを最適化する代わりに、ストア命令を生成します。

volatile int x[32];

void f(volatile int *p)
{
   for (int i = 0; i < 100000000; i++)
   {
     ++p[0];
     ++p[16];
   }
}

int main()
{
   cilk_spawn f(&x[0]);
   cilk_spawn f(&x[1]);
   cilk_spawn f(&x[2]);
   cilk_spawn f(&x[3]);
   cilk_sync;
   return 0;
 }

x[] の要素は 4 バイトです。64 バイトのキャッシュラインでは、16 個の要素を保持できます。 データ競合はなく、ループ完了時の結果は正しいものです。ただし、個々のストランドが隣接する配列要素を更新するためキャッシュライン競合が発生し、場合によってはパフォーマンスが大幅に低下します。

関連情報