1. 導入:なぜ並列処理で速度が出ないのか?
マルチスレッドプログラミングにおいて、「複数のスレッドで別々の変数を更新しているはずなのに、なぜか全体の処理速度が上がらない」という経験はありませんか?その原因の多くはFalse Sharing(偽の共有)という現象にあります。これは、CPUのキャッシュメモリの仕組みにより、プログラムの意図に反してデータが「共有」状態とみなされ、CPU間の通信が過剰に発生してしまう問題です。これを解決することで、並列処理のパフォーマンスを劇的に改善できます。
2. 基礎知識:キャッシュラインとFalse Sharing
CPUはメモリからデータを読み込む際、小さな単位(キャッシュライン)でまとめて取得します。一般的なCPUではこのサイズは64バイトです。
もし、隣り合う2つの変数がたまたま同じ64バイトの領域に配置されると、CPUは「同じキャッシュライン内に更新がある」と判断します。スレッドAが変数1を更新すると、スレッドBが使う変数2までキャッシュ上で無効化されてしまい、CPU同士で何度もデータの同期(キャッシュの一貫性維持)が発生します。これがFalse Sharingです。
3. 実装:hardware_destructive_interference_sizeの活用
C++17からは、このキャッシュラインサイズを自動的に取得できる定数 std::hardware_destructive_interference_size が導入されました(<new>ヘッダ)。これと alignas を組み合わせることで、変数を強制的に異なるキャッシュラインに配置できます。
4. サンプルプログラム
以下のコードは、False Sharingを防ぐためにパディングを挿入した構造体の例です。
include
include
include
// 構造体の各メンバを別々のキャッシュラインに配置する
struct CounterData {
// alignasを使うことで、この変数をキャッシュラインの先頭に合わせる
alignas(std::hardware_destructive_interference_size) std::atomic
alignas(std::hardware_destructive_interference_size) std::atomic
};
int main() {
CounterData data;
data.counterA = 0;
data.counterB = 0;
std::cout << "counterAのアドレス: " << &data.counterA << std::endl; std::cout << "counterBのアドレス: " << &data.counterB << std::endl; // これにより、counterAとcounterBは物理的に異なるキャッシュラインに配置され、 // 並列更新時のパフォーマンス低下を防ぐことができます。 return 0; }
5. 応用・注意点:現場での実用的な判断
便利な定数ですが、一部のコンパイラや環境では未定義であったり、ABI(バイナリ互換性)の問題で利用が制限されることがあります。
現場のプロフェッショナルな環境では、以下の点に注意してください。
・ハードコードの検討: 環境依存を避けたい場合や、特定のCPUアーキテクチャ(x86/ARM)に特化している場合は、標準定数を使わず直接 `alignas(64)` と記述することも一般的です。
・過剰なパディング: あらゆる変数にこのパディングを適用すると、メモリ使用量が大幅に増え、かえってキャッシュ効率が悪化することがあります。「頻繁に更新される独立した変数」に対してのみ使用するのが鉄則です。
False Sharingは目に見えないバグですが、これを意識するだけで、あなたの書く並列処理コードはより洗練されたものになるはずです。

コメント