記事の要約
- パフォーマンスチューニングの手数は沢山あった方が良い
- _Process()内部で文字列リテラル引数を使う事に注意しよう
- 計測してみた
はじめに
パフォーマンスチューニングを行う目的は、より多くのデバイス上での実行を可能とするためです。開発に使用しているPCのスペックが高く自分の環境では問題なく実行出来ても、他のデバイス(例えば5年前に使っていたPC)でも同じように動作するでしょうか。スペックの低いデバイスでも快適に実行できるなら、より多くの人に遊んでもらえる可能性が増します。
Unityのパフォーマンスチューニングバイブルは知見に富んだドキュメントです。Unity固有のテクニックもありますが、一般的にゲームエンジンを用いて作ったゲームのボトルネックとなる原因を示してくれます。
Godot固有の機能を用いた性能向上の手法としてueshitaさんのスライドが参考になります。大量のスプライト表示問題に対して
- 方式での改善(ノード版からサーバAPI版)
- 言語面での改善(GDScriptからC++(GDExtention))
といった解決アプローチと実測結果を掲載されています。
触発されて私もC#を用いたノード版とサーバAPI版の2方式を実測してみました。実行環境が異なるので比率でも比較になりますが、C#はおおよそGDScriptとC++の中間位です。
開発言語を変える事でも性能向上を行えるので、これも選択肢の一つになります。
今回のテーマ
チューニング手法を沢山しっていれば、取りうる対策を組み合わせて使う事が出来ます。一般的なコンピュータ利用において、画面描画とメモリ確保はコストの高い処理です。画面描画についてはueshitaさんのスライドで一つの手法が示されましたので、私はメモリ確保に関する手法を取り上げてみようと思います。
ということで今回は、多くの言語で性能に影響のあるプログラム記述方法「メソッドの引数に文字列リテラルを使う」の性能影響の計測です。
環境
Godot 4.1(mono)
Microsoft .Net SDK7.0
比較方法と計測結果
比較方法
前出のスプライト描画ベンチマークを改造して、スプライトのシェーダーに毎フレームパラメータ値を与えるプログラムを使用しました。シェーダーに変更を加えるSet()メソッドの引数はStringNameクラス、またはStringNameクラスに変換可能な文字列リテラルになります。
計測に先立ちStringNameクラス型の引数となるCS.shader_parameter_fillを、別のクラスで予め定義しておきます。
public partial class CS : Node
{
// general
public static NodePath Sprite2D = new NodePath("Sprite2D");
// ship shader param
public static StringName shader_parameter_fill = new StringName("shader_parameter/fill");
}
そして比較対象は以下のコードになります。
public override void _Ready()
{
_sprite = GetNode(CS.Sprite2D) as Sprite2D;
}
public override void _Process(double delta)
{
_sprite.Material.Set(CS.shader_parameter_fill, _ratio);
move(delta);
}
public override void _Ready()
{
_sprite = GetNode(CS.Sprite2D) as Sprite2D;
}
public override void _Process(double delta)
{
_sprite.Material.Set("shader_parameter/fill", _ratio);
move(delta);
}
余談ですが_Process()で毎回GetNode()を実行して対象ノードを特定する探索処理もコストが高そうです。毎回参照する事が予め分かっているなら_Ready()で1回だけ探索してメンバー変数(_sprite)に保持しておき、_Process()ではキャッシュされたメンバー変数を介してノードにアクセスします。(これも性能向上手法の1つです)
計測結果
該当箇所だけに限定した効果ですが、およそ1.5倍の性能向上が期待できます。
「文字列リテラルを使うと遅くなる」今日はこれだけ覚えて帰ってください。
詳細
文字列リテラルからStringオブジェクトへの変換
Godotのリファレンスを読んでいるとコードサンプルに文字列リテラルがよく登場します。シーン内のノードを探索するGetNode("Sprite2D")や、外部からマテリアルにシェーダーパラメータを与えるMaterial.Set("shader_parameter/fill", param)等です。
大抵のプログラミング言語では文字列リテラルをStringクラスに変換する記法が一般的にサポートされていますが、その裏側では複数回のメモリ確保が実行されています。
ゲームオブジェクトの初期化(UnityのStart(), Godotの_Ready()メソッド)で文字列リテラルをStringオブジェクトに1回変換する程度ならゲーム全体への影響はほとんどありませんが、毎フレーム呼び出される処理(UnityのUpdate(), Godotの_Process()メソッド)で文字列リテラルからStringオブジェクトへの変換を行うのは少々考えた方が良いでしょう。
GodotのStringクラス
GodotのStringクラスはC#のSystem.StringクラスをもとにしたStringExtensionsです。GDScript用のStringクラスメソッドと同様のメソッドを提供するため、C#のSystem.Stringを拡張したのでしょう。
Use System.String (string). Most of Godot's String methods have an equivalent in System.String or are provided by the StringExtensions class as extension methods.
GetNode(NodePath)とSet(StringName)
Node.GetNode()やObject.Set()の引数型はNodePathクラスやStringNameクラスであり、String クラスではありません。またリファレンスにも以下のような説明があります。
NodePath
You will usually just pass a string to Node.get_node and it will be automatically converted,
StringName
You will usually just pass a String to methods expecting a StringName and it will be automatically converted,
(最初は小文字stringと大文字Stringで文字列リテラルとStringオブジェクトを使い分けているのかしら?と思いましたが)どちらも後に続く説明文に"literal"が登場するので、ボールドで示したStringは文脈からStringオブジェクトを指していると考えられます。説明を読む限り「引数に与えられたString(オブジェクト)は自動的に(NodePathやStringNameに)変換されます」と読み取れます。
リファレンスの説明文からの解釈ですが、Node.GetNode("Sprite2D")というコードは文字列リテラルからNodePathオブジェクトへの変換において、複数回の型変換と、それに伴うメモリ確保が実行されています。
文字列リテラル"Sprite2D" → Stringオブジェクト → StringNameやNodePathオブジェクト
「自動的に変換する」という言葉で解説されていますが、これはC#の代入オペレーターユーザー定義変換演算子(=)を用いて実装する機能です。リファレンスにはユーザー定義変換演算子の記載はありませんが、以下のコードがコンパイル可能なので、こっそりユーザー定義変換演算子も実装されているのでしょう。
String s = "aaa";
StringName sn = s;// OK
NodePath np = s;// OK
s = sn;// OK
s = np;// OK
sn = np;// compile error
np = sn;// compile error
なおNodePathとStringName同士のユーザー定義変換演算子は実装されていないようです。
メモリ確保の回数を減らそう
_Process()とそこから呼び出されるメソッド内部は毎フレーム実行されます。せめてこの実行経路上のボトルネックは軽減しておきましょう。
Node.GetNode( NodePath path ) や Object.Set( StringName property, Variant value )の引数は(あえて構造体にする事にメリットが無さそうなので)クラスでしょう。
クラスという事はメソッドデザインの時点で参照型の値渡しを想定しています。であれば該当クラスではない別のクラススコープ(先出のCS.cs)で引数のクラス型に合わせた定数文字列を格納するオブジェクトを作成し、該当コード箇所でこれを参照します。
あとがき
技術書典15にてGodot本を書きました。Godot初心者の私ですが、道具に慣れてしまうと忘れてしまうような、初心者の視点でつまずいた事の記録と解決の過程です。サンプルページに目次を掲載していますので、どんな内容か一読頂けると幸いです。
追記
コメントで指摘頂いて認知しましたが、C#では代入オペレーターという用語は使われてないのですね。該当箇所をユーザー定義変換演算子に差し替えました。