今回はWwise 2024.1で導入されたWwiseの新しいデフォルトメモリアロケータ、「AkMemoryArena」について詳しく説明します。この新しいメモリアロケータにより、従来のバージョンと比較してWwiseのメモリリソースの使用が大幅に改善されたほか、ユーザが本メモリシステムを正しく理解した上で設定することにより、多くのプロジェクトでさらなる最適化が期待できます。
メモリの予約とフラグメンテーションのおさらい
Wwise 2019.2では新しいデフォルトメモリアロケータが導入されました。Wwise 2019.2のアロケータ概要を以下に示します:
- すべてのサイズクラスにおいてブロックベースの割り当てを行うことにより、内部メモリのフラグメンテーション(断片化)が緩和されました
- スレッドをキャッシュすることにより、メモリ割り当て・解放の際のスレッド間の競合が回避または低減されました
- 必要に応じてオンデマンドでメモリリソースを増やすことができ、事前にサイズを決めたメモリプールの準備が不要となりました
この新しいメモリアロケータは優れたパフォーマンスを提供し、メモリがオンデマンドでマッピングされることによりWwiseの柔軟性は大幅に向上しました。
ところが改善点が評価される一方、利用するデベロッパが増えるつれ、メモリ使用全般に関するいくつかの欠点が指摘されるようになりました。特に目立った問題は以下の3点でした:
- ブロックベースの割り当てであるため、必要以上にアロケーションが大きくなる傾向があり、多くのタイトルにおいて“Memory Used”に表示される合計が平均で約10~15%も増加しました。
- スレッドキャッシュ動作の結果、“Memory Reserved”が予想以上に増加しましたが、これは単純にメモリ割り当てのスレッド数が増えたためです。極端なケースにおいてはこのアプリケーションで何十にもおよぶスレッドを管理する事態が観察され、複数の固有ヒープがインスタンス化されましたが、それらは使われないことも多く、永遠に解放されませんでした。
- アロケータが1度に長いスパンで新メモリページを要求しますが、同時にこれらのスパンのサブセットがいずれアンマップされるとアロケータが推測するため、最終的に完全に解放されるまで、これらの部分は未使用のままとなりました。
これらの問題点の一部はほかのスレッドキャッシュ型メモリアロケータにもみられますが、Wwiseならではの問題点もあり、その原因はミドルウェアライブラリであるWwiseサウンドエンジンの動作環境にあります。ミドルウェアであるということは、Wwiseがソフトウェアスタックの中間に位置することを意味し、Wwiseは多くの場合、プラットフォームの最下層部分と相互に作用することができません。具体的には実行時に使用する各スレッドの直接制御や、メモリのマップとアンマップのための仮想メモリシステムの直接操作ができません。例えば、Wwiseとゲームエンジンのインテグレーションがメモリを部分的にアンマップする方法に対応していないことが多く、大量のメモリが予約されたまま未使用となることがよくありました。
その後数年間、予約メモリの量を抑えるためにさまざまな対策を導入しましたが、逆にアロケータを使用するためのCPUコストが増加する傾向があり、いったん獲得したメリットの大部分が相殺されました。またこのような努力にもかかわらず、Wwiseサウンドエンジンがリソースを最大限に活用する上で、合理的に回避できない核心的なメモリアロケータ設計に関する判断がありました。
大容量メモリが利用可能なデスクトップアプリケーションやサーバアプリケーションの場合、スレッドをキャッシュするアロケータが優れた成果をもたらすユースケースが多数あることに、ここで言及したいと思います。例えばLLVMプロジェクトでは最近、Windowsでツールチェーンのデフォルトメモリアロケータをrpmallocへ移動したところ、プロセスで大幅なパフォーマンス改善が確認されました。しかしWwiseというソフトウェアにはほかのアプリケーションにない独特の要件が多いため、今回は同じ策略を適用することは最善でないと私たちは考えました。
以上を踏まえ、Wwiseの要件をよりよく満たすと私たちが感じる、新しいメモリ割り当てシステムAkMemoryArenaについて説明します。
AkMemoryArenaの概要
Wwise2024.1にWwiseの新しいメモリアロケータAkMemoryArenaを導入しました。従来のWwiseのメモリアロケータはどれも既製のソリューションをWwiseの用途に合わせて適用させたものでしたが、今回初めて本格的なメモリアロケータをゼロから構築しました。はじめからWwiseの全要件に焦点をあてるためです。
AkMemoryArenaの機能の一部を紹介します:
メモリのスパンを動的にマップ・アンマップする能力
必要に応じて新しいメモリスパンをマッピングする機能と、スパンを構成するアロケーションがすべて解放された時にスパンをアンマップする機能を維持しています。つまり、常にメモリプールのサイズを事前にハードリミット付きで決める必要はありません。私たちの経験では、多くのデベロッパにとってメモリの上限が設定されていない方が、柔軟性と安定性がはるかに高くなると考えています。
優れたCPUおよびメモリフラグメンテーションの性能
AkMemoryArenaはさまざまなアロケーションアルゴリズムを組み合わせており、各種シナリオに合わせてCPUの全体的なパフォーマンスとメモリ使用のバランスを調整しています:
- 256バイト未満の小さなアロケーションにはブロックベースのアロケータ
- これがアリーナの「Small-Block Allocator(SBA)」セクションです
- 中規模アロケーションには「ちょうどよい大きさ」のアロケーションポリシーを使うフリーリスト型アロケータ
- これがアリーナの「Two-level segregated fit(TLSF)」セクションです
- 大型アロケーションには独立したスパン
このようにしてデベロッパは、全体的なメモリフラグメンテーションを時間の経過と共に制御することができます。さらにどのアロケーションアルゴリズムも定数時間のパフォーマンス特性を有するため、ほぼすべてのシナリオにおいてCPUのオーバーヘッドを低く抑えるために効果的です。
またAkMemoryArenaにスレッドローカルキャッシュはなく、代わりに軽いロックメカニズムを使いスレッド間同期を処理します。利用可能なすべてのスレッドでメモリアロケータの状態を共有することにより、マルチスレッドの場面においてもメモリ予約を予測しやすくします。
スレッドローカルキャッシュがないことのデメリットは、複数のスレッドにまたがり頻繁にメモリ割り当てを行うため、パフォーマンスが低下することです。メモリ割り当ての頻度を、特に複数のコアにまたがるオーディオレンダリングで少なくするため、Wwiseは短期間のメモリ割り当てにTempAllocやBookmarkAllocなどの専用メモリアロケータを用いるようにし、これらのアロケータは自分のスレッドローカルステートを活用するため、スレッド間の同期がほぼ不要となりました。
メモリ割り当てのCPUオーバーヘッドのほか、マッピングメモリに2MiBほどのHuge Pagesを利用した方が効果的であることも判明しました。Huge Pagesの利用でCPUにおけるtranslation lookaside bufferミスの頻度を減らすことができます。これは4KiBまたは16KiBの小さなサイズのページを使う場合と比較して、CPUのパフォーマンス全体が通常約10%向上します。
ゲームエンジンとの統合をシンプルに
ゲームエンジンとの統合を簡素化し、メモリを割り当てる際のエラーや不確定要素のポイントとならないよう、AkMemoryArenaがメモリ取得や解放に使用するコールバックを可能な限りシンプルにしました。
各AkMemoryArenaは、メモリを管理するためにユーザが提供するコールバックを2つ必要とします。1つはメモリスパンを割り当てるため、もう1つはメモリスパンを解放するためです。
これらコールバックを実装するための要件は非常に少なく、以下の通りです:
- メモリのマップ・アンマップの方法について、特別な要件はありません
- メモリのアライメントについて、要件はありません
- スパンを解放する際に提供する唯一のデータは、その前のスパンを割り当てた時の呼び出しで返されたものと全く同じデータ、つまりユーザが提供したアロケーションのアドレスと任意のuserDataへのポインタです。
このコールバックがいかにシンプルであるかを示す例として、これらの関数の一部パラメータを、ほかのロジックを使わずに、std::malloc()とstd::free()に転送するような実装も有効です。実際にWindowsやPOSIXベースのプラットフォームでは、これがWwiseサウンドエンジンのフックのデフォルトバージョンです。
柔軟な設定で各種プロジェクトニーズに対応
プロジェクトやチームによってメモリ使用に関する哲学が異なるほか、開発中のゲームコンテンツの性質上、メモリ使用について特定の決まりが必要と判断されるかもしれません。例えば、
- あるチームではメモリの使用を確実に予測可能とするため、アプリケーション全体のすべてのメモリを各システムに起動時に割り当て、完全にバジェットを確定させるかもしれません。
- 逆に別のチームではメモリのマップ・アンマップを非常にきめ細かく行い、ほかのシステムが必要な場合に限りメモリを使用できるようにするかもしれません。
可能な範囲においてさまざまなニーズやユースケースに対応できるよう、私たちは設定オプションを豊富に揃えるように努めました。
例えばAkMemoryArenaで初期化時にすべてのメモリを予約する必要はありませんが、実質的に予約する動作も設定可能であり、具体的には初期スパンを非常に大きいサイズに設定して対応します。この初期メモリスパンをメモリ割り当てのコールバックのソフトリミットとして適用することもでき、最初の呼び出し以降でinvokeされるたびに警告を出すようにします。
プロファイラでメモリフラグメンテーションを監視
私たちが今回AkMemoryArenaをWwiseに組み込む過程において、各アリーナを経時的に細かくモニタリングしてプロファイリングできるシステムを構築するチャンスであると考えました。このような情報はユーザがタイトルのAkMemoryArenas設定を判断する時の貴重な指針となるだけでなく、問題点や最適化の機会を特定する必要のある時、私たちが細かいサポートや支援を提供するために役立つと考えています。
メモリ使用量の削減
以上の機能に加え、AkMemoryArenaが予約する合計メモリ量、さらには使用される合計メモリ量まで、以前よりもはるかに小さくなる傾向があることが分かりました。
AkMemoryArenaのプロファイラについて
新しいAkMemoryArenaのProfilerに、以下の通り各Arenaの各スパンに関する統計とデータが表示されます:
- アロケーションコールバックfnMemAllocSpanを呼び出すたびに返されるAddressとUserData
- メイン画面の1行が、fnMemAllocSpanの1回の呼び出しに相当します
- スパンのサイズ
- スパンのうち、割り当てられた量(Allocs)とフリーである量(Frees)
- スパン内の割り当てられている領域を示す、フラグメンテーションのマップ
AkMemoryArenaごとの集計値もいくつか表示されます。Profilerには合計使用メモリ(Used)や予約メモリ(Reserved)などの単純な累計に加え、1つのアリーナ内の最も大きい使用可能なスペース(Largest Free)が表示されます。これは新しいスパンを要求しなくともそのアリーナ内で対応可能な、最も大きいアロケーションを示す数値です。この値をフリーメモリ合計(Free)と比較することにより、Arena全体のフラグメンテーションの程度を推測することができます。
これらの内容を得るためのランタイムコストはごくわずかであり、このデータを計算するためにアプリケーションの全履歴を把握する必要はありません。たとえゲームがすでに何時間も稼働していたとしても、Authoringアプリケーションはゲームに接続し、ほぼ負担なしでメモリレイアウトの状態を評価することができます。
データ追跡のCPUオーバーヘッドを減らすために、AkMemoryArenasの状態は大まかなレベルでしか把握できないことにご注意ください。メモリ分析用の専門ツールの多くが提供するようなアロケーションごとの詳細な情報はありませんが、ユーザがメモリ使用率を自分で評価して問題の有無を簡単に確認し、問題が見つかった場合の次の方向性を決める参考となり、各アリーナのさらなる構成やメモリ戦略を決定する指針となるには充分なデータであると考えます。
ゲームエンジンへの統合
提供中のUnityやUnrealとWwiseのインテグレーションを利用せず、独自のゲームエンジンを維持しているユーザは、メモリシステムの統合や構成の一部について検討しなおす価値があると思われます。
個々のメモリ割り当てを処理する既存のコールバックはすべて引き続き使用可能であり、これらの動作は変更されていないことを最初に記しておきます。Wwiseからの割り当てについて、すべて別のメモリアロケータを使いたい場合はそのオプションがあります。
ただしこのようなコールバックを使用した場合、AkMemoryArenasや、ここで紹介した新しいProfilerが利用できなくなります。すべてのメモリ割り当てを確認する必要性を感じたとしても、AkMemoryArenaを使用した方が各種ツールが提供されるため、使う価値があるかもしれません。AkMemoryArenasにメモリ割り当ての大部分を任せることにより、使用中のほかのグローバルメモリシステムの負担が軽減される可能性があり、Wwiseのメモリ使用量を追跡するほかのツールを開発しやすくなるかもしれません。
AkMemoryArenaを使用している場合においても、個々のメモリ割り当てのメタデータを一部記録することが可能です。そのためにあるのが以下AkMemSettingsの“Debug”メモリフックです:
これらのメモリフックはWwise内臓のメモリアロケータシステムを使用している場合にも、メモリアロケーションの個別コールバックを使用している場合にもアクティブです。メモリフラグメンテーションの詳細な診断が必要な時、これらのコールバックが役に立つかもしれません。
前述の通り、AkMemoryArenasの初期設定と統合をできる限りシンプルにするように努めました。以下はメモリ割り当てと解放のコールバックの実装例です。
AkMemoryArenaは個別に設定するため、AkMemoryArenaごとに異なるコールバックを使用することも可能です。例えばPrimaryやMediaのアリーナで、それぞれのシステムが使用するアロケーションの寿命やサイズの相対的な違いに基づきローレベルで異なるメモリアロケータを使用したい場合、応用することができます。
また特定のメモリアリーナの使用を無効にしたい場合、以下のようにそのコールバックにnullptrを設定すればよいのです:
これはRelease構成のProfilerアリーナ(AkMemoryMgrArena_Profiler)をデフォルトで無効にするため、あるいはオーディオ処理のためにデバイス固有のメモリを使用しないプラットフォームにおいてDeviceメモリアリーナ(AkMemoryMgrArena_Device)を無効にするために使用します。
同様にWwiseの残りの部分とゲームエンジンの統合の方法次第で、通常のゲームプレイでWwiseがMediaアリーナにメモリを割り当てないことも考えられます。プロジェクトにおいて使用するAPIが、例えばAK::SoundEngine::LoadBankや、AK::SoundEngine::LoadBankMemoryCopyではなく、AK::SoundEngine::SetMediaや、AK::SoundEngine::LoadBankMemoryViewだけである場合などがこれに該当します。ただしその場合においても、プロファイリング中にAuthoringツールが新しいメディアをサウンドエンジンに転送する際、Mediaアロケーションが一部行われるため、このオプションはWwiseのRelease構成を対象としている場合に限り検討してください。
ゲームエンジンの統合方法にもよりますが、Memory Arenaをサウンドエンジン以外で使うことも検討に値するかもしれません。例えばAK::MemoryMgr::Mallocを使用して独自のメモリアロケーションを作成し、そのアロケーションにSoundBankデータをロードし、そのメモリをAK::SoundEngine::LoadBankMemoryViewに提供することができます。メモリは依然としてAkMemoryArenaによって管理されるため、Profilerやその他システムのメリットを享受しつつ、コードがメモリの割り当てを担当し、メモリの寿命もコントロールすることが可能です。
細かい設定や微調整
AkMemSettingsのmemoryArenaSettings配列を使い、Memory Arenaのほかのパラメータを設定します。この配列は最も一般的な状況下でシステムが適切に動作するよう、サウンドエンジンの合理的なデフォルトに設定されていますが、コンテンツやメモリの要件はゲームごとに異なるため、ゲームのコンテンツに合った最適化を行うことでメモリ使用を大幅に改善できます。私たちが疑似ゲームコンテンツを使いテストしたところ、テストごとにAkMemoryArena設定を簡単に微調整しただけで、全体的なMemory Reservedが5~10%ほど削減されることが分かりました。
以下は検討をおすすめするいくつかのポイントです:
- AkMemoryArenaSettings::uTlsfInitSizeを標準的なメモリ使用量、あるいはメモリバジェット目標に合わせて設定します。一般的に大きめの“Base”、つまり初期スパンを設定しておいた方が、多数のメモリスパンを個別につくるよりもメモリフラグメンテーションの性能が最適となる傾向があることが分かりました。初期サイズを大きくすることにより、起動時のシステム全体のメモリ予約を明確にすることもでき、その他のドメインのメモリバジェットを計画する上で参考となります。
- AkMemoryArenaSettings::uSbaInitSizeに、Profilerで特定されているウォーターマークを設定します。SBAには専用の“Base”スパンがあり、構成する各アロケーションのメモリフットプリントが小さいという別のメリットがあります。アロケーションごとに約16バイトです。SBA Base Spanの小ぶりなアロケーションが増えるようにこの値をさらに高く設定することにより、Memory Reservedを削減できるという直接的な効果があるほか、Memory Usedも減少します。
- AkMemoryArenaSettings::uAllocSizeHugeの設定値を小さくし、TLSFスパンのフラグメンテーションを減らします。低く設定することにより、TLSFスパンではなく独立した“Huge”スパンとなるアロケーションが多くなります。ただしfnMemAllocSpanへの呼び出しが増えるため、スパンによる外部フラグメンテーションが問題とならないことが前提であり、その点においてfnMemAllocSpanのMemory Arenaフックの統合方法に大きく依存します。
Wwise SDKドキュメントのConfiguration and Tuning of AkMemoryArenasに記載の設定案も、合わせてご参照ください。
コメント