少し前に Interlocked を使った排他制御の記事を書きましたがこれと SpinWait という極小時間待機向けの待機機能を使って lock に代わる排他制御処理を実装してみたいと思います。
前に書いた記事はこれ ↓
【C#】Interlockedを使って同時実行数を制限する - PG日誌
確認環境
- Windows11
- VisualStudio 2022
- AMD Ryzen 9 5900X(12コア24スレッド)
- .NET 8(C# 12)
- BenchmarkDotNet(0.15.8)
実装例
各々を実装して、最後に100万回実行してベンチーマークを取得します。
処理条件ですが
- 1つ1つの処理が凄く短くて軽い
- 一気に処理がタスクに積まれる
- Parallel.Forで100万件の処理が一気に発生
のような状況を想定して実装します。
lock構文を使った排他制御
まずはいつもの見慣れた lock 構文での排他制御です。
[Benchmark] public void CaseLock() { object lockObj = new(); int count = 0; long sum = 0; int min = int.MaxValue; int max = int.MinValue; // 大量に並列実行を行って軽い処理を実行する Parallel.For(0, loops, i => { // 1023までのランダムっぽい数字を仮生成 int value = ((i * 17) ^ (i >> 3)) & 1023; lock (lockObj) { count++; // ちょっとした軽い処理 sum += value; if (value < min) { min = value; } if (value > max) { max = value; } } }); //Console.WriteLine($"total={total}"); }
Interlocked+SpinWaitで排他制御
こっちはタイトルの通り組み合わせで排他制御を実行します。
SpinWaitを使用すると「短時間の待機をカーネル上で実行しないでユーザーモードで実行するので切り替えが発生しない分lockに比べて高速」MSDN だそうですが、イメージ的には while(true) の無限ループで一瞬待つに近いと思います。
[Benchmark] public void CaseSpin() { int flagIndex = 0; int count = 0; long sum = 0; int min = int.MaxValue; int max = int.MinValue; // 大量に並列実行を行って軽い処理を実行する Parallel.For(0, loops, i => { // 短時間待機用 SpinWait wait = new(); // 1023までのランダムっぽい数字を仮生成 int value = ((i * 17) ^ (i >> 3)) & 1023; while (Interlocked.CompareExchange(ref flagIndex, 1, 0) != 0) { wait.SpinOnce(); } try { count++; // ちょっとした軽い処理 sum += value; if (value < min) { min = value; } if (value > max) { max = value; } } finally { Interlocked.Exchange(ref flagIndex, 0); } }); //Console.WriteLine($"total={total}"); }
計測結果
最後に計測結果が以下の通りです。
| Method | Mean | Error | StdDev | Rank | Allocated | |--------- |---------:|---------:|---------:|-----:|----------:| | CaseLock | 75.78 ms | 1.160 ms | 1.085 ms | 2 | 7.64 KB | | CaseSpin | 32.14 ms | 0.636 ms | 1.314 ms | 1 | 19.84 KB |
自分のPC環境ではこのような結果になりました。 100万回で 43ms 時間が稼げるのでこの間に他の計算が実行できそう(かも?)となりました。
実装した感想
かなり極端な条件を設定したので実用が怪しいとは書いてて思いました (^_-)-☆
じゃなくて、条件に合う処理系で処理時間を非常に気にするほどのレベルになれば一考の価値があるのかもしれません(逆に排他処理区間の処理内容が重いとSpinWaitの待機負荷がかかって逆に期待通りにならないなんてことがMSDNの書き方から薄っすら想像できます)
とはいえ実際の業務の実装では、こんなことにする前にこの競合区間の整理などで、他にやるべきこといくらでもある気がしないでもないです。が、面白かったのでよしとします。
あと、この Interlocked 版は実行順序が全く保証されないので順序依存がある処理には適用できません(まぁでも lock も厳密な順序保証がないので同じと言えば同じですね、というか lock のそれを知らないでアテにしてると非常に難しい問題に直面するので常に注意するべきですが、、、)
短いですが以上です。