脱・低速ロック!スピンロックを極限まで最適化するテクニック
Optimizing a Spin-Lock
Optimizing a Spin-Lock
並列処理の要であるスピンロック、正しく実装できていますか?パフォーマンスを限界まで引き出すための最適化手法を解説します。CPU負荷を抑えつつ、競合を効率よく捌くためのベストプラクティスを共有します。
共有してくれてありがとう!フィードバックをもらえるのは嬉しいよ :) 注意点として、ほとんどのケースではスピンロックはおすすめしない。スレッドと物理CPUコアが1対1でマッピングされている環境で、かつ計測をした上で使う場合に限るよ。
数年前にLMAX Disruptorを使い始めるまで、実際に本番コードでスピンロックを使っている人なんて聞いたことがなかったよ。ずっとアンチパターンだと教わってきたし、大抵の場合はそれが良い経験則だと思う。でも、コンピュータサイエンスの他のことと同じで、「良い経験則」にも例外はつきものだね。いまだに自分自身でスピンロックを本番コードに書いたことはないけれど、Disruptorのおかげで、それが必要なケースもあるんだってことを学べたよ。
これって、もっと密にキャッシュを共有しているマシン上で動かすかによって回答が変わるよね?例えば、4コアがL2を共有しているIntelのEコア(効率コア)クラスターとかさ。
競合が予想されるなら、交換(exchange)の前にリラックスした読み込み(relaxed read)を先に行った方がいいんじゃないかな?例えばこんな感じ:
auto lock() noexcept -> void {
auto backoff = 1;
do {
while (locked_.load(std::memory_order_relaxed)) {
for (auto i = 0; i < backoff; ++i) _mm_pause();
backoff = backoff < 64 ? backoff << 1 : 64;
}
} while (locked_.exchange(true, std::memory_order_acquire);
}
ロックのパフォーマンスをマイクロベンチマークで測るのはすごく危険だよ。小さなベンチマークだと、CPUとメモリが非常に限定的で特殊な状態(ロック以外は何もない静かな状態)に置かれてしまうからね。現実世界でのロックは、常に100%の勢いで競合しているわけじゃなくて、CPUが実際の処理をしてメモリにアクセスしている状況と、競合が組み合わさっているものだから。これまでの経験から言うと、そうした現実的なシナリオでは、マイクロベンチマークで最高の結果を出したロックが、全く予想外の別のアルゴリズムに完敗してしまうことがよくあるんだ。