7.5K Views
November 27, 23
スライド概要
■概要
RE ENGINEでは全てのゲームロジックがC#言語で記述され、REVMと呼ばれる独自の仮想マシン上で動作しています。
長らく維持されていたC#7.3/.NET Framework4.8相当の環境をC#8.0/.NETに載せ替えたことで得られたメリットや今後についてREVMの実装を交えて解説します。
※CAPCOM Open Conference Professional RE:2023 で公開された動画を一部改変してスライド化しております。
■想定スキル
C#言語の基礎知識
詳細は下記公式サイトをご確認ください。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CAPCOM Open Conference Professional RE:2023
https://www.capcom-games.com/coc/2023/
カプコンR&Dの最新情報は公式Twitterをチェック!
https://twitter.com/capcom_randd
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
株式会社カプコンが誇るゲームエンジン「RE ENGINE」を開発している技術研究統括によるカプコン公式アカウントです。 これまでの技術カンファレンスなどで行った講演資料を公開しています。 【CAPCOM オープンカンファレンス プロフェッショナル RE:2023】 https://www.capcom-games.com/coc/2023/ 【CAPCOM オープンカンファレンス RE:2022】 https://www.capcom.co.jp/RE2022/ 【CAPCOM オープンカンファレンス RE:2019】 http://www.capcom.co.jp/RE2019/
ゲームプログラムの C#8.0/.NET対応とその未来 ゲームプログラムのC#8.0/.NET対応とその未来のセッションを始めます。 よろしくお願いいたします。 ©CAPCOM 1
はじめに C#言語バージョンを上げると、数多くのメリットがあるため 最新バージョンを使用可能な環境が望ましい C#スクリプトシステムの言語バージョンを上げるための対応 • • .NETのクラスを対応する実装への置き換え 新規言語機能の実装 .NET Framework→.NETの移行が 発生するため対応コスト大 このセッションでは、RE ENGINEのC#スクリプトシステムについてお話しします。 C#言語バージョンを上げると、記述方法が増える等 数多くのメリットがあるため、 最新バージョンを使用可能な環境が望ましいと考えています。 2 では、何故今回C#8.0に移行したのかですが、 C#スクリプトシステムの言語バージョンを上げるには、 .NETのクラスを対応する.NETバージョンの実装に置き換える必要があり新規言語機能を実装する必要があります。 移行前は.NET FrameworkのC#7.3を採用していたため、 C#8.0以降の言語バージョンに更新するには、.NETへの移行が必須でした。 そのため、言語バージョンも最新化すると更に対応コストが高くなってしまうため、 .NETで初めて使用可能になったC#8.0に移行することになったという経緯があります。 ©CAPCOM 2
アジェンダ C#スクリプトシステム C#8.0/.NETへの移行前の環境・移行の課題について 移行後の環境について まとめと今後について アジェンダです。 前半で、RE ENGINEのC#スクリプトシステムの解説と、C#8.0/.NETへ移行する際に存在した課題についてお話しします。 3 後半で、移行した結果得られたメリットと今後の展望についてお話しします。 ©CAPCOM 3
C#スクリプトシステムについて まずは、RE ENGINEのC#スクリプトシステムについて少しお話しします。 4 ©CAPCOM 4
C#スクリプトの動作環境について 全ゲームロジックはC#言語で記述 タイトル固有のC++コードは存在せず、unsafeコードは禁止 • • クラッシュ:基本的に原因はエンジン側 例外のthrow:原因はタイトル側 独自の仮想マシン(REVM)で動作 独自のガベージコレクション(FrameGC)で動作 C#スクリプトの動作環境についてです。 RE ENGINEで開発される全てのゲームロジックはC#言語で記述されています。 タイトル固有のC++コードは存在せず、unsafeコードは禁止です。 5 これはクラッシュは基本的にエンジン側が原因、例外のthrowはタイトル側が原因と区別する為で、 クラッシュの危険があるコードはタイトル側で書けないようになっています。 また独自の仮想マシン『REVM』上で動作しており、ガベージコレクションも独自実装しています。 ©CAPCOM 5
REVMとは JITコンパイル非サポート C#コードをILに変換し独自AOTコンパイル ジェネリクス等を事前展開し静的にリンクする事で実行速度を高速化 開発時:イテレーション速度を優先し、MicroCodeを出力 製品時:実行パフォーマンスを優先し、C++コードを出力(IL2CPP) C# C#コンパイル (Roslyn) 開発時 MicroCode 製品時 C++ AOTコンパイル REVMはJITコンパイルは非サポートです。 その代わりにC#コードをILに変換した後、独自のAOTコンパイルを実行することで実行速度を高速化しています。 6 開発時はイテレーション速度を優先し、MicroCodeを出力しインタプリタ上で実行します。 製品時は実行パフォーマンスを優先し、C++コードを出力しビルド後に実行します。 ©CAPCOM 6
C#スクリプトシステムについて 参考資料 【CEDEC2016】ラピッドイテレーションを実現するゲームエンジンの設計 【CAPCOM RE:2019】複数のAAAタイトル開発に耐える汎用ゲームエンジンの設計 と運用 REVMについては、過去にCEDEC2016やRE:2019でも発表されていますので詳しく知りたい方はそちらをご覧ください。 7 ©CAPCOM 7
C#8.0/.NET移行前の環境・課題について 続いてC#8.0に移行する前の環境はどうだったのか、移行に対する課題はどのようなものだったのかをお話しします。 8 ©CAPCOM 8
C#8.0/.NET移行前の環境・課題について 2018年5月よりC#7.3/.NET Framework4.8相当の環境 .NET Framework時代の標準ライブラリ CoreCLR・CoreFxから必要な機能をインポート 移行の課題 • .NET Core 3.1の内部実装に置き換え • 必須のJITコンパイル前提の型 • 全ての既存タイトルへの影響 移行以前の環境は、 C#7.3/.NET Framework 4.8がリリースされた2018年5月より同等の環境が維持されていました。 これは.NET Framework時代のCoreCLR, CoreFxから必要な機能をインポートして実現しています。 9 このような環境をC#8.0/.NETに移行するには様々な課題がありました。 それぞれの課題について詳しく説明致します。 ©CAPCOM 9
課題① .NET Core 3.1の内部実装に置き換え C#8.0は.NET Framework環境では使用不可 C# .NET Framework .NET 全てのバージョン .NET Standard 1.x .NET Standard 2.0 .NET Core 2.x 該当無し .NET Standard 2.1 .NET Core 3.x 7.3 8.0 C#8.0環境の最新である.NET Core 3.1を採用 課題その1 .NET Core3.1の内部実装に置き換え C#8.0の機能を実装するには、.NET Frameworkでは使用不可なため.NET Standard2.1以上の環境に乗り換える必要がありました。 10 実際に採用した環境は、C#8.0が使用可能な最新の環境である.NET Core 3.1です。 RE ENGINEのC#スクリプトシステムをこの環境に乗り換えるには、 ©CAPCOM 10
課題① .NET Core 3.1の内部実装に置き換え CoreCLR・CoreFxからインポートしている約300ファイルを .NET Core3.1実装に置き換える必要がある .NET FrameworkのC#7.3では使用出来ない言語機能も一部存在する Span<T>型に対してのstackalloc .NET FrameworkにはSpan<T>が存在しないためエラー CoreCLR・CoreFxからインポートしているファイル全てを置き換えることになります。 約300ファイルを置き換えることになるのですが、そのまま上書きすれば完了という訳ではなく、 .NET FrameworkのC#7.3では必要なクラスが存在せず使用出来ない言語機能が存在するため、 .NETで新しく追加されたクラスや関数を追加する対応が必要でした。 ©CAPCOM 11 11
課題① .NET Core 3.1の内部実装に置き換え 高速な動作や独自のVM・GCを実現するための独自実装が存在 例:System.Arrayの基本機能はC++実装 IL2CPP 関数を置き換え また、高速な動作や独自の仮想マシン・ガベージコレクションを実現するための、 独自実装も混在しているため それら全てを考慮しながら進める必要もありました。 例としてSystem.Arrayの基本機能はInternalCallでC++で実装されており、 IL2CPP時には関数自体を置き変えることで高速に実行されるものも存在します。 ©CAPCOM 12 12
課題② 必須のJITコンパイル前提のクラス REVMはJITコンパイル非サポート System.Span<T>, System.ReadOnlySpan<T> 本来使用出来ない機能(参照フィールド)を実装 https://github.com/dotnet/coreclr/blob/v3.1.27/src/System.Private.CoreLib/shared/System/Span.Fast.cs 実装が存在しない https://github.com/dotnet/coreclr/blob/v3.1.27/src/System.Private.CoreLib/shared/System/ByReference.cs 課題その2必須のJITコンパイル前提クラス おさらいになりますが、REVMではJITコンパイルが非サポートです。 13 この環境でJITコンパイルに依存しているクラスを動作させる必要がありました、 言語機能に必須なSystem.Span<T>クラス, ReadOnlySpan<T>クラスがJITコンパイルに依存しています。 Span<T>クラスがメンバに持っているByReference<T>クラスが、 C#8.0では使用出来ない機能を実現するためのクラスなためJITコンパイルに依存しています。 そのままREVMで動作させようとするとSpan<T>クラスを生成したタイミングで例外が発生し正しく動作しなくなります。 そのため何らかの対応が必要でした。 ©CAPCOM 13
課題③ 全ての既存タイトルへの影響 RE ENGINEでは全ての既存タイトルの互換性をサポート 互換性の維持:挙動変更が起きてはならない .NETは破壊的変更を許容している パフォーマンス:速度・メモリ共に悪影響を与えてはならない REVMの独自実装により.NETのパフォーマンスと異なる場合がある 課題その3 全ての既存タイトルへの影響 RE ENGINEでは全ての既存タイトルの互換性をサポートする運用をしています。 14 そのため挙動変更が起きないように実装する必要があるのですが、 .NETは破壊的変更を許容しているため、インポートしている機能が変更されている場合は元の挙動を再現する必要がありました。 また開発中タイトルに迷惑をかけることになるので、パフォーマンスに悪影響があってはなりません。 .NETではパフォーマンスが向上していたとしても、 REVMの独自実装により元の実装の方がパフォーマンスが高いといったことが起こりえます。 そのため独自実装している機能が関連する箇所はそれぞれ検証が必要でした。 ©CAPCOM 14
課題まとめ C#7.3からC#8.0に移行するには .NET Core 3.1の実装に置き換える必要がある • • • 独自実装の変更を考慮しパフォーマンスが改善する必要がある JITコンパイルに依存しているSpan<T>を実装する必要がある 互換性を維持する必要がある 課題のまとめです。 C#7.3からC#8.0に移行するには.NET Core3.1の実装に置き換える必要があります。 15 そのためには現在開発中のタイトルに影響を及ぼさないために、 REVMの独自実装を考慮しパフォーマンスが改善される必要があります。 言語機能に必要なため、JITコンパイルに依存しているクラスを独自実装する必要があります。 全てのタイトルの互換性を維持する運用上 挙動を維持する必要があります。 このように多くの課題が存在し、長らく移行出来ていませんでした。 ©CAPCOM 15
移行後の環境について ここからは全ての問題に対応し移行した結果 得られたメリットをお伝えしていきます。 16 ©CAPCOM 16
パフォーマンス比較 ゲームロジックは一切変更せずパフォーマンス比較 比較対象:デビル メイ クライ 5 スペシャルエディション(PS5) デビル メイ クライ 5 スペシャルエディションで、ゲームロジックは一切変更せずパフォーマンスを比較してみます。 既に発売されていますので、ゲームロジックはかなり最適化されている状態です。 ©CAPCOM 17 17
パフォーマンス比較 速度編 自動プレイで、序盤のユリゼン戦 C#8.0/.NET移行後 移行前 Max Time:2.72 ms 約4%高速 Max Time:2.83 ms まずは実行速度を見てみます。 序盤のユリゼン戦を自動プレイで動作させ、ほぼ同タイミングを比較します。 18 C#スクリプトの処理時間を表すUpdateBehaviorの最大時間が2.83msから2.72msと約4%高速になったことが分かります。 ゲームロジックは一切変更していないのにも関わらず、高速になった理由は、 ©CAPCOM 18
.NETライブラリの最適化 例:string.Format()が高速に 10万回の関数呼び出し(PS5 IL2CPP) 書式指定文字列, 型 移行前 C#8.0/.NET移行後 “{0:D2}”, int型 31.7243ms 15.8984ms 約50%高速 “{0:F2}“, float型 51.4866ms 32.4636ms 約37%高速 結果 ゲームロジックを一切変更することなく高速化される ※一部 独自実装に置き換えた状態での計測結果 .NETライブラリ内の実装が最適化された影響です。 例としてstring.Format()を比較すると、10万回 書式指定文字列付きで関数を呼び出した際、 int型の場合は約50%, float型の場合は約37% 実行速度が改善しています。 19 このような最適化が数多く存在するため ゲームロジックを一切変更することなく高速化されます。 この結果や先程のデビルメイクライ5 スペシャルエディションでの結果は、 REVMでは遅くなってしまった個所を独自実装に置き換えた状態での計測結果です。 実際に行った独自実装を一部紹介します。 ©CAPCOM 19
独自実装への置き換え 例:Span<T>, ReadOnlySpan<T>を独自実装に アドレスをメンバに持つ形に変更 データはC++から返すように変更 1000万回の要素アクセス(PS5 IL2CPP) int[] Span<int> 書き込み 6.30073ms 8.71613ms 約38%低速 読み込み 2.13456ms 8.97972ms 約420%低速 例としてJIT依存で対応が必須だったSpan<T>クラス、ReadOnlySpan<T>クラスを見てみます。 このクラスは配列とほぼ同様に扱えるクラスで、.NET以降の内部実装では積極的に使用されています。 20 ポインタのアドレスをメンバに持つ形に変更し、データはC++から返すように変更することで、JIT依存を回避しました。 1000万回の要素アクセスを配列とSpan<T>クラスで比較すると、 書き込み時は約38% 読み込み時は約420% Span<T>クラスの方が低速です。 これは、REVMの配列が最初期から独自実装なことが影響しています。 ©CAPCOM 20
Span<T>, ReadOnlySpan<T>の高速化 IL2CPP時に更なる高速化(C#11のSpan<T>とほぼ同等の実装に) IL2CPP 明示的にinline展開 1000万回の要素アクセス(PS5 IL2CPP) int[] Span<int> 書き込み 6.30073ms 6.14248ms 約3%高速 読み込み 2.13456ms 2.12426ms 約0.5%高速 .NETで積極的に使用しているSpan<T>クラスが遅い状態だとゲーム全体が遅くなり兼ねないため、 IL2CPP時に更に高速化されるように変更しました。 21 Span<T>クラスの要素にアクセスするようなコードを書いた際は、明示的にinline展開することで高速に動作します。 C#11のSpan<T>クラスとほぼ同等の実装を実現しています。 先程と同じように1000万回の要素アクセスを配列とSpan<T>クラスで比較すると、 書き込み時は約3% 読み込み時は約0.5% Span<T>クラスの方が高速になりました。 このように様々な独自実装を部分的に行うことで、ゲーム全体の高速化を実現しています。 ©CAPCOM 21
パフォーマンス比較 メモリ編 自動プレイで初回起動からゲームクリアまで動作後のメモリ最大使用量 MaxUsed : 約0.3%改善 MaxUsed : 約0.4%改善 MaxUsed : 約1.8%改善 移行前 C#8.0/.NET移行後 続いてメモリを見てみます。 自動プレイで初回起動からゲームクリアまで動作した後のメモリ最大使用量を比較してみます。 22 RE ENGINEはメモリアロケーターも自作しており、使用用途に応じてカテゴリーが分かれているのですが、 C#スクリプトで確保したメモリが載るDefaultは最大使用量が約0.3%改善され、 静的なメモリが載るPermanentは最大使用量が約0.4%改善され、 CPUメモリ全体を合計した値を表したPhysicalは最大使用量が約1.8%改善していることが分かります。 DefaultとPermanentで改善している値の合計値より、多いことから断片化も改善していることも分かります。 メモリが改善している理由は、 ©CAPCOM 22
stackallocが使用可能 Span<T>に対してstackallocが使用可能になり断片化対策が可能に 変更はこの行のみ 1000万回の配列確保(PS5 IL2CPP) 速度 メモリ確保量 ArrayAlloc 180.647ms 4.9GB SpanAlloc 170.618ms 約5.5%高速 メモリ確保無し Span<T>クラスを実装したことで、Span<T>クラスに対してstackallocが使用可能になったことが影響しています。 .NETライブラリ内の実装でもstackallocを積極的に使う最適化が行われているため断片化が改善しています。 23 配列を確保しfor文を回すArrayAlloc(), SpanAlloc() この2つの関数を比較します。 1行変更しただけですが、 それぞれの関数を1000万回実行した場合、SpanAlloc()の方が実行速度は5.5%高速になり,4.9GBのメモリ確保が不要になります。 ゲームでは断片化が問題になる事が多いためメモリ確保回数を減らせるようになったのは大きなメリットです。 こちらはゲームロジック内でも使用可能になっているため現在開発中のタイトルでは更に断片化が改善することが期待出来ます。 ©CAPCOM 23
C#コンパイラの最適化 例:静的なReadOnlySpan<T>を使用することでメモリ削減可能に InitializeArray:コピーされる コンストラクタ:コピーされない(読み取り専用と判定出来るため) また新しいC#コンパイラの最適化の影響でもメモリ使用量が改善しています。 静的なbyte配列と静的なReadOnlySpan<byte>クラスを比較します。 24 記述している関数の中身は同一ですが、ILを見ると呼び出されている関数が全く違います。 配列で呼び出されるInitializeArray()は初期値用データからコピーされるのに対して、 ReadOnlySpan<byte>クラスの方ではコピーが無くなります。 これはC#コンパイラが読み取り専用と判定出来るため、 初期値用データを直接参照する形に最適化され、メモリ使用量が初期値用データのみになります。 ©CAPCOM 24
新規実装時のみ得られるメリット 新しいクラス・関数の追加 新しい記述方法の追加 今まで挙げてきたものだけでも 数多くのメリットがあり新規実装する際にも活用出来るのですが、 新規実装時のみ得られるメリットに焦点を当ててみていきます。 25 ©CAPCOM 25
新しいクラス・関数の追加 例:string.Create() 文字列の長さ指定生成と同時に書き込みが出来る関数 .NETで追加されたSpan<T>, SpanAction<char,TState>が必要 10万回の関数呼び出し(PS5 IL2CPP) 速度 メモリ確保量 StringConcat 48.9616ms 約74.2MB StringCreate 4.87977ms 約90%高速 約4.9MB 約95%削減 新しいクラス・関数が追加されています。 例として、stringクラスに追加されたCreate関数をみてみます。 この関数は文字列の長さ指定生成と同時に書き込みが出来る関数です。 26 Span<T>クラス, SpanAction<char,TState>クラスが.NETで新しく追加されたため実装されています。 “0123456789”の文字列をfor文で作成する StringConcat(), StringCreate()の2つの関数を比較します。 10万回関数を呼び出した際、速度は約90%高速に,メモリ確保量は約95%削減と大幅にパフォーマンスが改善しています。 このように新しいクラスや関数は処理が洗練されているため、新規実装時に利用するだけで大きなメリットが得られます。 ©CAPCOM 26
新しい記述方法 例:記述量が削減可能に C#7.3以前 C#8.0以降 null合体代入演算子 範囲演算子 プロパティパターン マッチング また新しい記述方法も使用可能になりました。 null合体代入演算子 27 範囲演算子 プロパティパターンマッチング のように様々な記述量を減らせる機能が追加されています。 1つ1つはちょっとした変更ですが、少しでも記述量が減らせると追加要素の実装や、 デバッグ等に時間を充てることが出来るため ゲームのクオリティを上げることに繋がります。 ©CAPCOM 27
まとめと今後について 最後に、まとめと今後についてです。 28 ©CAPCOM 28
まとめ 新しいC#/.NETに乗り換えるだけでパフォーマンスが改善する .NETライブラリの内部実装変更による最適化 C#コンパイラによる新しい最適化 言語の新機能 新しいクラスや関数でパフォーマンス改善の可能性が広がる 言語の新機能により記述が楽になる 独自実装がある環境では移行コストが発生するが それを上回るメリットがたくさん得られる まとめです。 C#/.NETのバージョンを新しいものへ乗り換えるだけでパフォーマンスが改善します。 29 これは.NETライブラリ内の実装の最適化やC#コンパイラによる最適化や言語の新機能 等様々な影響で改善します。 また、新しいクラスや関数を利用可能になることでパフォーマンス改善の可能性が広がります。 更に言語の新機能で記述量が減らせることが出来、実装が楽になります。 RE ENGINEのような独自実装がある環境では移行コストが発生しますがそれを上回るメリットがたくさん得られます。 ©CAPCOM 29
今後について 今年度中に現在最新のC#11/.NET 7に移行予定 C#9.0 record型によるボイラープレートコードの削減 global using, ファイルスコープ名前空間による記述量の削減 C#10 ジェネリック型数値演算による実装の共通化 C#11 .NET5~7 更なるクラス・関数の追加 Collections, LINQ等 既存機能の高速化 今後についてですが、RE ENGINEは今年度中に現在最新のC#11/.NET 7環境に移行予定です。 移行することで、 Record型によるボイラープレートコードの削減 30 global using, ファイルスコープ名前空間による記述量の削減 ジェネリック型数値演算による数値演算の処理の共通化 更なるクラス・関数の追加 Collections, LINQ等 既存機能の高速化 等のようなメリットが得られるため、今後のゲーム開発に置いて役立つのは間違いありません。 ©CAPCOM 30
以上 以上になります。 ご清聴ありがとうございました。 31 ©CAPCOM 31