クリティカルセクションよりもSRWロックを優先する

この記事では、Win32アプリケーションを開発する際に、従来のクリティカルセクションよりもスリムリーダー/ライターロック(SRWL)メカニズムが好まれている理由について説明します。



軽量



SRWLオブジェクトはx64アーキテクチャのメモリで8バイトしか占有しませんが、クリティカルセクションは40バイトです。 クリティカルセクションでは、OSのカーネル関数の呼び出しによる初期化と初期化解除が必要ですが、SRWLは定数SRWLOCK_INITを割り当てるだけで初期化され、削除の費用はまったくありません。 SRWLを使用すると、よりコンパクトなコードが生成され、作業時に使用するRAMが少なくなります。



内部同期を必要とする100,000個のオブジェクトがある場合、メモリの節約はすでにかなりの量になります。 不要なキャッシュミスを回避することによるパフォーマンスの向上はさらに顕著になります。 最新のプロセッサー(2008年にリリースされたIntel Nehalem以降)では、1つのキャッシュラインが 64バイトを占有します。 同期オブジェクトで40個を使用すると、ソフトウェア内の小さなオブジェクトへのアクセスのパフォーマンスに大きく影響します。



スピード



まず、OSカーネルでのSRWLの実装が過去数年間で大幅に再設計されたことに留意してください。 Windowsのさまざまな同期プリミティブの速度の測定に関するインターネット上のベンチマークを読んだ場合、執筆日に注意してください。



クリティカルセクションとSRWLの両方がユーザーモードのサイクルでスピンし、その後カーネルでスタンバイモードになります。 ユーザーモードでタイムアウトを構成できるのは、クリティカルセクションのみです。



実装の詳細については詳しく調べていません。 また、クリティカルセクションとSRWLの速度を完全に正しく比較するために、正しいベンチマークを実行しようとしませんでした。 理論的に健全で実用的なベンチマークを構築することは非常に困難です。



しかし、さまざまなシナリオで、アプリケーションのクリティカルセクションを約20回SRWLに置き換えました。 SRWLは常に高速で(少なくとも遅くはありません)、しばしば目に見えるパフォーマンスの向上をもたらしました。



ここでは具体的な数字を示しません。 ロックがキャプチャされるときの作業量、ロックの粒度、並列処理のレベル、読み取りと書き込みの比率、キャッシュの使用量、プロセッサの負荷、およびその他の要因は、最終結果に大きく影響します。



SRWLがクリティカルセクションよりも絶対に高速であるとは言いません。 いずれの場合も、全体像を明確にするためにプロファイリングが必要です。



SRWLでの再入力の欠如



これはバグではなく機能です。



ロックの再利益性がない場合、これはすぐにより透明な公共契約につながります。ロックのキャプチャとリリースを決定するときは慎重に検討する必要があり、最終的にデッドロックを回避します。 まあ、少なくともキャプチャされたロック内でコールバックを呼び出すなどの愚かなことをするまでは。



もちろん、再入可能なロックも便利です。 たとえば、古いコードに並列処理を追加しようとして、そのリファクタリングに深く入り込みたくない場合。 元のPOSIXミューテックスは偶然にリエントラントでした。 再入可能な同期プリミティブが主流にならなかった場合に、並列コードとロックに関連する問題をどれだけうまく回避できるか想像することしかできません。



記録のために同じSRWLを2回キャプチャしようとするストリームは、デッドロックに陥ります。 このタイプのエラーは、最初の出現時に簡単に識別して修正できます。 ネクタイを見てください-すべての必要な情報があります。 タイミングと並列フローの影響はありません。



少なくとも、デッドロックを引き起こすために使用される再帰的な読み取りロックも、少なくともその90%は確信しています:)。 誤解がない限り、Microsoftは何らかのアップデートで、またはWin8からWin10に切り替えたときに、動作を静かに変更しており、デッドロックはありません。 残念ながら、これにより再入可能なエラーを見つけるのがより困難になりました。 誤ってネストされた読み取りロックは、内部ロックのリリースが早すぎる場合に不快なバグにつながります。 さらに悪いことに、外部ロックは別のリーダーによってキャプチャされたロックを解除できます。 ロックのMicrosoft SAL注釈は、理論的にはコンパイル段階でこの種の問題を検出するのに役立ちますが、私は個人的に実際にそれらを試したことはありません。



並列読み取り



実際の並列読み取りは非常に頻繁に発生します。 この場合、クリティカルセクションは同時実行性をサポートしません。



パフォーマンスの問題を記録する



同時読み取り利点の逆は、すべての読み取りロックが解放されるまで書き込みロックを取得できないという事実です。 さらに、SRWLは、ロックの権利を付与する順序で、すべての設定または正義での書き込みロックの要求を保証しません(書き込みロックがスタンバイ状態のままである間に、新しい読み取りロックを正常にキャプチャできます)。 この点に関する重要なセクションは、一方では良くありません(そこでの読み取りまたは書き込みのキャプチャの優先順位を設定することも不可能です)が、他方では、読み取りの並列キャプチャの可能性の欠如のため、問題はあまり起こりません。



Windowsタスクスケジューラ 、すべてのスレッドにリソースを提供するという点である程度の公平性を提供します。 これにより、1つのスレッドで一部のリソースをブロックしながら、他のすべてのスレッドでユーザーモードの待機ループを完了できます。 ただし、スケジューラー操作アルゴリズムはパブリックコントラクトの一部ではないため、現在の実装に基づいてコードを記述する価値はありません。



記録の進行の継続性が重要な場合、クリティカルセクションもSRWLも同期メカニズムとして適切ではありません。 読み取り/書き込みキューなどの他の構成体が、推奨されるメカニズムです。



ランタイムコンペティション



concurrency :: reader_writer_lockは、SRWLよりも優先度の厳密な保証を提供し、頻繁なキャプチャの条件で動作するように特別に設計されています。 それは代償です。 私の経験では、この同期プリミティブはクリティカルセクションやSRWLよりも大幅に遅く、またより多くのメモリスペース(72バイト)を占有します。



個人的には、特定のタスク(ジョブ)を実行してロックをキャプチャしようとするだけでは冗長すぎると思いますが、おそらくそれは誰かに合うでしょう。



不正なキャッシュヒット



誤ったキャッシュヒットは 、サイズの違い(8バイトと40バイト)により、クリティカルセクションよりもSRWLの方はるかに高い可能性があります。 クリティカルセクションがキャッシュに入ると、その40バイトが64バイトキャッシュラインのほとんどを占めるため、別のクリティカルセクションがキャッシュラインに入る可能性がなくなります。 ロックの配列を作成する場合は、プラットフォームのキャッシュラインのサイズを検討してください。



ただし、事前にこれに集中しないでください。 SRWLでさえ、同じキャッシュラインに入ることはめったにありません。 これは、非常に多数のスレッドが比較的少数のオブジェクトを同時に変更する場合にのみ発生します。 たとえば、数千個の小さなオブジェクトがある場合、キャッシュ内のロックが誤ってヒットする可能性があるため、サイズを大幅に変更することはほとんど意味がありません。原則として、ゲームはろうそくに値しません。 もちろん、個々の状況をプロファイリングした後にのみ、それを述べることができます。



OSカーネルのバグ



Windows OSのカーネルのバグに言及する必要があります。これにより、SRWLおよび実際にはWindowsへの信頼をわずかに失うことを余儀なくされました。 数年前、同僚と私は、一部のストリームが特定のSRWLをキャプチャできなかったときに奇妙なバグに気付き始めました。 これは主にデュアルコアプロセッサで発生しましたが、シングルコアプロセッサでも非常にまれに発生することがありました。 デバッグの結果、キャプチャの試行時に、他のスレッドがこのロックを保持していないことがわかりました。 さらに驚くべきことに、同じスレッドのすぐ後で、同じロックをキャプチャする試みはすでに成功していました。 長い研究の後、このバグの再生時間を数日から30分に短縮することができました。 最後に、これはOSのカーネルの問題であり、IOCPにも当てはまることを証明しました。



修正プログラムにバグが発見されてから8か月かかり、もちろん、ユーザーPCに更新プログラムを配布するのに時間がかかりました。



結論



ほとんどのロックは、さまざまなストリームからの偶発的な同時アクセスから一部のオブジェクトの情報を保護します。 ここでのキーワードは「ランダム」です。正確な同時アクセスの要件が意図的にプログラムされることはめったにないからです。 クリティカルセクションとSRWLの両方は、現在解放されているロックのキャプチャと解放において優れたパフォーマンスを発揮します。 この場合、保護されたオブジェクトの全体的なサイズが前面に出てきます。 オブジェクトがロックと同じキャッシュラインに入るのに十分なほど小さい場合、すぐにパフォーマンスが向上します。 この目的で使用する主な理由は、32バイト小さいSRWLです。



コードシナリオでは、キャプチャの試行時にほとんどの場合ロックが既に取得されているため、このような明確な結論を引き出すことはできません。 各最適化の測定が必要です。 ただし、この場合、ロック自体の取得と解放の速度がコードのボトルネックになることはほとんどありません。 ロック内のコードランタイムを削減することに焦点が置かれます。 ブロッキングの前または後に実行できることはすべてそこで行う必要があります。 1つではなく複数のロックを使用することを検討してください。 ロックを呼び出す前に必要なデータをプルしてみてください(これにより、ロック内のコードがキャッシュから取得されるため、ロック内のコードが高速に動作する可能性があります)。 ロック内のグローバルヒープにメモリを割り当てないでください(メモリの予備割り当てでアロケータを使用してみてください)。 などなど。



そして最後に、応答しないロックはコードで読むのがはるかに簡単です。 再入可能ロックは、ロックの現在の状態とデッドロックの原因の理解を複雑にするため、一種の「同時実行性」です。



All Articles