ホームページ >バックエンド開発 >C++ >マルチスレッドアプリケーションにおけるフォールスシェアリングを理解して解決する実際の問題

マルチスレッドアプリケーションにおけるフォールスシェアリングを理解して解決する実際の問題

DDD
DDDオリジナル
2024-12-06 02:08:16196ブラウズ

Understanding and Solving False Sharing in Multi-threaded Applications with an actual issue I had

最近、私はポアソン分布 (amath_pdist) を計算する関数のマルチスレッド実装に取り​​組んでいました。目標は、ワークロードを複数のスレッドに分割して、特に大規模なアレイのパフォーマンスを向上させることでした。ただし、期待した速度向上を達成する代わりに、配列のサイズが増加するにつれて速度が大幅に低下することに気付きました。

いくつかの調査の結果、犯人は 誤った共有であることがわかりました。この投稿では、フォールス シェアリングとは何かを説明し、問題の原因となっている元のコードを示し、大幅なパフォーマンスの向上につながった修正を共有します。


問題: マルチスレッドコードにおけるフォールスシェアリング

偽共有は、複数のスレッドが共有配列の異なる部分で動作するが、それらのデータが同じキャッシュラインに存在する場合に発生します。キャッシュ ラインは、メモリと CPU キャッシュの間で転送されるデータの最小単位です (通常は 64 バイト)。 1 つのスレッドがキャッシュ ラインの一部に書き込むと、他のスレッドが論理的に独立したデータを処理している場合でも、そのラインは無効になります。この不必要な無効化は、キャッシュ ラインの繰り返しの再ロードにより大幅なパフォーマンスの低下を引き起こします。

これは私の元のコードの簡略版です:

void *calculate_pdist_segment(void *data) {
    struct pdist_segment *segment = (struct pdist_segment *)data;
    size_t interval_a = segment->interval_a, interval_b = segment->interval_b;
    double lambda = segment->lambda;
    int *d = segment->data;

    for (size_t i = interval_a; i < interval_b; i++) {
        segment->pdist[i] = pow(lambda, d[i]) * exp(-lambda) / tgamma(d[i] + 1);
    }
    return NULL;
}

double *amath_pdist(int *data, double lambda, size_t n_elements, size_t n_threads) {
    double *pdist = malloc(sizeof(double) * n_elements);
    pthread_t threads[n_threads];
    struct pdist_segment segments[n_threads];
    size_t step = n_elements / n_threads;

    for (size_t i = 0; i < n_threads; i++) {
        segments[i].data = data;
        segments[i].lambda = lambda;
        segments[i].pdist = pdist;
        segments[i].interval_a = step * i;
        segments[i].interval_b = (i == n_threads - 1) ? n_elements : (step * (i + 1));
        pthread_create(&threads[i], NULL, calculate_pdist_segment, &segments[i]);
    }

    for (size_t i = 0; i < n_threads; i++) {
        pthread_join(threads[i], NULL);
    }

    return pdist;
}

問題が発生する場所

上記のコード内:

  • 配列 pdist はすべてのスレッドで共有されます。
  • 各スレッドは、特定の範囲のインデックス (interval_a から interval_b) に書き込みます。
  • セグメント境界では、隣接するインデックスが同じキャッシュラインに存在する場合があります。たとえば、pdist[249999] と pdist[250000] がキャッシュ ラインを共有する場合、スレッド 1 (pdist[249999] で作業) とスレッド 2 (pdist[250000] で作業) は互いのキャッシュ ラインを無効にします。

この問題は、大きな配列ではうまく対応できませんでした。境界の問題は小さいように見えるかもしれませんが、反復回数が膨大であるため、キャッシュの無効化のコストが増大し、数秒間の不必要なオーバーヘッドが発生します。


解決策: メモリをキャッシュライン境界に合わせる

問題を解決するために、posix_memalign を使用して、pdist 配列が 64 バイト境界 に合わせて配置されていることを確認しました。これにより、スレッドが完全に独立したキャッシュ ラインで動作することが保証され、誤った共有が排除されます。

更新されたコードは次のとおりです:

double *amath_pdist(int *data, double lambda, size_t n_elements, size_t n_threads) {
    double *pdist;
    if (posix_memalign((void **)&pdist, 64, sizeof(double) * n_elements) != 0) {
        perror("Failed to allocate aligned memory");
        return NULL;
    }

    pthread_t threads[n_threads];
    struct pdist_segment segments[n_threads];
    size_t step = n_elements / n_threads;

    for (size_t i = 0; i < n_threads; i++) {
        segments[i].data = data;
        segments[i].lambda = lambda;
        segments[i].pdist = pdist;
        segments[i].interval_a = step * i;
        segments[i].interval_b = (i == n_threads - 1) ? n_elements : (step * (i + 1));
        pthread_create(&threads[i], NULL, calculate_pdist_segment, &segments[i]);
    }

    for (size_t i = 0; i < n_threads; i++) {
        pthread_join(threads[i], NULL);
    }

    return pdist;
}

なぜこれが機能するのでしょうか?

  1. アライメントされたメモリ:

    • posix_memalign を使用すると、配列はキャッシュ ライン境界で開始されます。
    • 各スレッドに割り当てられた範囲はキャッシュラインときちんと整列し、重複を防ぎます。
  2. キャッシュライン共有なし:

    • スレッドは個別のキャッシュラインで動作し、偽共有による無効化を排除します。
  3. キャッシュ効率の向上:

    • シーケンシャル メモリ アクセス パターンは CPU プリフェッチャーとうまく連携し、パフォーマンスをさらに向上させます。

結果と要点

修正を適用した後、amath_pdist 関数の実行時間が大幅に減少しました。私がテストしていたデータセットでは、実測時間が 10.92 秒から 0.06 秒 に減少しました。

重要な教訓:

  1. 誤った共有は、マルチスレッド アプリケーションにおける微妙だが重大な問題です。セグメント境界での小さな重複でもパフォーマンスが低下する可能性があります。
  2. posix_memalign を使用したメモリ アライメントは、偽共有を解決する簡単かつ効果的な方法です。メモリをキャッシュライン境界に合わせることで、スレッドが独立して動作するようになります。
  3. 大規模な配列または並列処理を使用する場合は、キャッシュ関連の問題がないかコードを常に分析してください。 perf や valgrind などのツールは、ボトルネックを正確に特定するのに役立ちます。

読んでいただきありがとうございます!

コードに興味がある人は、ここで見つけることができます

以上がマルチスレッドアプリケーションにおけるフォールスシェアリングを理解して解決する実際の問題の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。