Peredvizhnikovエンジン: C++20で書かれたロックフリーのゲームエンジン
(github.com/eduard-permyakov)- C++20で書かれた完全なロックフリーゲームエンジンで、言語のコルーチンプリミティブ上に並行計算のアクターモデルを実装
- アクターモデル抽象化を使うことで、スレッド間同期の詳細から切り離された状態で複雑な並列ロジックを開発可能
- 完全なロックフリー実装により、任意のスレッド終了状況でも進行保証、デッドロック防止、重要イベントへの応答の予測可能なレイテンシ、耐障害性を提供
- ワーカースレッドの1つが非同期に終了しても、エンジンが動作を継続することを保証
- 実装には、Software Transactional Memory、ロックフリーキュー、ロックフリー直列化プリミティブ、
std::atomic_shared_ptr、ロックフリースケジューラ、ロックフリーメモリアロケータ、コンパイル時DAG などが含まれる - ロックフリーアルゴリズム、設計根拠、ベンチマークは、Peredvizhnikov Engine: Design and Implementation of a Completely Lock-Free Scheduler 文書で扱われている
- データ指向設計を支援するため、コンポーネント単位のアクセスに最適化され、大規模データセットをサポートするインメモリデータベースを実装
- インメモリデータベースは、Flat Hash MapとBitwise Trie with Bitmapデータ構造を基盤としている
- 現在サポートされるプラットフォームはLinuxのみで、ソースのビルドにはClang++ 16が必要
- ソースコードはGPLv3ライセンスで提供され、一部または全コードを別ライセンスで利用する権利が個別に付与される場合がある
1件のコメント
Hacker Newsのコメント
Actorフレームワークでは、メソッドポインタのキューに普通の
std::dequeを使い、メッセージをキューに入れるときは Benaphore 方式でロックしているもともとはFutexのように、アトミック操作とロックプリミティブを併用するものだが、私のロックプリミティブはリトライ回数に応じてスピンロック/ミューテックスの組み合わせとして動作する。ベンチマーク上、メッセージのpush関数がブロックされることは非常にまれで、OSのコンテキストスイッチが起きる可能性も低いため、たまにロックされたスレッドがスワップアウトされても、ロックフリーアルゴリズムのコストを正当化できるほど頻繁には起こらない
要するに、ロックフリーキューより非ロックフリーキューのほうがはるかに速いが、誰もロックを取得できないコンテキストスイッチによって、ごくまれに長い遅延が発生することは受け入れる必要がある。現代のハードウェアでは、ワーカースレッドごとに毎秒 1,000万メッセージ をキューに入れられる
肝心なのは、カーネルオブジェクト、つまり別個のロックプリミティブが実際には不要だという点にある。このアイデアが「誰でも知っているやり方」から「OSに今すぐ入れるべき機能」へ変わるのはそこだ
Futexの設計では、衝突処理のためのOS同期オブジェクトの代わりに、OSがアドレス→スレッドの対応リストを保持する。スレッドTがアドレスXのfutexで眠ると、XがTを指すようにリストへ入り、Xのfutexを起こせという要求が来ると、OSがリストを走査してTを起こす
違いは制限に現れる。Benaphoreのようなものは高価なシステム全体のリソースなので、BeOSではマシンあたり6万5536個程度しか許可していなかったと記憶している。しかしFutexは単なるメモリなので、制限を設ける理由がない
多くの場合は、単にロックを使って気にしなくてよい、という観察は正しいと思う。ただし、もっと良くできるアプリケーションや状況もある。コンシューマが1回のロック操作でキュー内のすべての項目を取り出し、プロデューサがコンシューマへシグナルを送る方式も、注意すればキューの効率とスループットを高められる。たとえば項目を入れるたびにシグナルを送るのではなく、キューが空の状態から空でなくなったときだけ送るべきだ
ロックフリーのスケジューラは確かに興味深く見えるし、特にイベントブロードキャストの線形化可能性が目を引く。ただし論文のベンチマークでは、actor 12ペア(そして12コア?)での最高が毎秒4万3500メッセージで、単一コアのグラフも毎秒約5000メッセージなので、この種のベンチマークとしては驚くほど低い
エンジンがLinuxと、より重要なことにx86を要求しているため(アセンブリ命令のため)、まだ再現できていないが、actor 1ペアあたり少なくとも毎秒約100万リクエストは期待する。Erlangのような場合を考えると、それより低ければオーバーヘッドが許容できないほど大きくなる
このエンジンはメッセージパッシングに注力しているが、経験上この方式は非常に扱いにくい。状態機械は難しく、複数の下位actorと一緒に作業するとさらに難しくなる。本質的にactorは、メッセージパッシングというより、ロックなしで状態を隔離するものに近いと思う。Swift actorsはうまくやったと思っていて、メッセージの代わりにメソッド呼び出しを使うと推論しやすいだけでなく、実行時にコンテキストが変わり得る箇所を追加で示してくれ、必ずしもスケジューラを介入させる必要もない。共有状態は遅く、スケーラビリティを損なう
最近、C++20コルーチンでSwift actorsに似たものを実装したヘッダーオンリーライブラリを作った。興味があれば「coroactors」で探せばよい。競合がないときは毎秒約 1,000万リクエスト、競合がありスケジューラに依存する場合は毎秒100万〜300万リクエストでも、オーバーヘッドが大きすぎると思った。特に、ミューテックスで保護した共有状態に対する通常のメソッド呼び出しと比べるとそうだ。コルーチンは伝染しやすく、ますます多くの関数が
asyncコルーチンになり、非自明なコードベースではコルーチン呼び出しやメッセージパッシングが増える。だからオーバーヘッドは可能な限り低くなければならず、そうでないと有用な仕事をするよりもタスク切り替えに多くの時間を使うことになるactor ベースとされており、actor にメッセージを送ることは、ミューテックスの下で actor 関数を実行するのと同等だと説明している。つまり N 個のスレッドがメッセージを送っても、actor のコードを実行するスレッドは 1 個なので、ミューテックスのように直列化される。
だから技術的には「完全にロックフリー」ではあり得ても、actor を使う以上、並列化の改善はない。
この実装は、すでに進行中だが中断された actor の作業を、別の並列スレッドが拾って続行できるように、再開可能な関数に大きく依存している。優れた設計ドキュメントの 3 ページを見ればよい: https://github.com/eduard-permyakov/peredvizhnikov-engine/bl...
だから厳密には「より並列的」ではないかもしれないが(actor の数は同じなので)、同じ作業集合を完了するために、より多くの並列性をうまく活用しているように見える。
考えやすければ、同じリソース上でどこに競合が生じるかも見えやすくなり、実際に潜在的な並列性を改善する助けになる。SMP が高速化できる特定の機会に気づいたら、actor モデルから少し外れて、メッセージキューを複数スレッドが受け取るようにできるし、それが不可能なら actor をさらに追加してデータをよりよく分割すればよい。
STM の競合が激しいクリティカルセクションを、従来のミューテックス実装と比較してデバッグしたりプロファイリングしたりした人はいるだろうか? 結局、共有メモリへの同時アクセスを仲裁する何かは必要で、ただ飯はない。ミューテックスは最適化、プロファイリング、理解が非常によく進んでいる。
一方で STM も同じ水準なのかはよく分からない。トランザクションが無限に(?)再試行されることもあるのでは?
中核は
scheduler.cppで、std::coroutinesを使っている。他の言語の
async/awaitに似ている。スケジューラには作業(コルーチン)のキューと、それを実行するスレッドプール(N>0)がある。ここではデータを持った作業同士がメッセージをやり取りする。メモリ使用量が増える代わりに、ロックは必要ない。
BEAM っぽい?
https://youtu.be/bo5WL5IQAd0?feature=shared
こういうエンジンをデバッグするのがどれほど難しいかについての言及は見当たらなかった。
実装を読む時間はないが、README だけを見ると、ゲームスレッド間の古典的な分散システムのように聞こえる。リトライ・バックオフのようなパターンがよく出てきそうだ。
https://github.com/eduard-permyakov/peredvizhnikov-engine/bl...
ロックフリーは格好よく聞こえるが、意味のあるレベルでアトミック操作を使うコードには、形式的で、できれば機械検証された証明が伴うべきだと思う。逐次一貫性ではないアトミック順序を正しく使うのは難しすぎる。間違って書かれたコードを何度も見てきたし、そこから生じるバグは最悪だ。
ゲームデモはどこにあるのか? 今どきゲームエンジンとして見るなら、実際のツール、Maya や 3DSMax のようなエクスポータ、そしてコラボレーション用のツール・指標・通知のようなものも必要だ。
「ロックフリー」と言っているが、まだそうではなさそうだ。
export std::mutex iolock{};export std::mutex errlock{};SDL_PollEvent