UDN
Search public documentation:

UnrealScriptStructsJP
English Translation
中国翻译
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

Unreal Script の構造体の使用

ドキュメントの概要: UnrealScript の struct (構造体) の使用がパフォーマンスに及ぼす影響。

ドキュメントの変更ログ: Joe Graf により作成。

概要

UnrealScript (Unreal スクリプト) の struct (構造体) は、関連データメンバをグループにまとめる強力なメカニズムで、C++ における構造体と同様、構造体のデータメンバの宣言や、それらを開いての操作を本質的に遅らせる要素はありませんが、C++ で構造体を値で渡すとパフォーマンスに影響するように、スクリプトの構造体を値で渡すと同様のことが起きます。このドキュメントの目的は、スクリプトインタープリタで値渡しがどのように処理されるかについて考察を加えることにあります。

UnrealScript (Unreal スクリプト) の構造体やその他の機能の詳細は、UnrealScript リファレンス を参照してください。

サンプルケース

ここでは、多数のスクリプト構造体を操作する簡単なクラスを扱うことにしましょう (下記の MySlowness.uc を参照してください)。これは非常に一般的なパターンを使用し、データ型をより複雑なエンティティに集約するカスタム構造体をいくつか宣言し、それらの構造体のインスタンスを MySlowness クラスのメンバとして宣言し、最後に、構造体を操作する関数をいくつか宣言しています。

MySlowness クラスはティックのたびに何かを実行します。まず、値の範囲を反復して MySlowStruct の最大タイプを見つけます。次に、インスタンス化された MyMemoryAbusingStructs のうち、どのバージョンが別の関数のステートとして渡されるかを変更します。これらのインスタンスはアクセサ関数として返される点に注意してください。MySlowness に含まれる関数は、すべて構造体を値で渡します。Actor や Object などより複雑なデータ型の多くもこの方法で渡されているので、これは当然のように思えますが、エンジンは、オブジェクト派生エンティティを、値渡しではなく参照渡しとして暗黙的に処理しています。スクリプト構造体にはこの特別な処理はなく、そのため値渡しにはパフォーマンス面への影響が見られます。

最初に、MySlowStruct サンプルを使用した場合のパフォーマンスへの影響を検討してみましょう。すべての値を初期化する便宜上のメソッドと、その構造体の 2 つのインスタンスを受け取り、2 つの最大値を含む新しい構造体を作成する別のメソッドがあります。このコードは構造体のコピーを目的もなく多数作成し、ゲームを遅くしています。MySlowStruct の流れを見ると、InitMySlowStruct() の戻り値はメモリコピーを実行していることが分かります。これは呼び出しのたびに 1 回発生し、その結果が MaxOfMySlowStruct() メソッドに渡されます。この関数は両方のパラメータで値渡しなので、さらに 2 つのデータコピーが作成されます。最後に、生成されたデータが宛先の変数にコピーされるので、このコード片だけでデータを 5 回ほどコピーしたことになります。

MySlowStruct Example

DoSlowness()
   For 1 to 32
      (1) = InitMySlowStruct(passed in values copied to function)   <--- Return value must make a copy the struct (戻り値は struct のコピーを作成する必要がある)
      (2) = InitMySlowStruct(passed in values copied to function)   <--- Return value must make a copy the struct (戻り値は struct のコピーを作成する必要がある)
      MaxOfMySlowStruct(copies the value (1),copies the value (2))   <--- Return value must make a copy the struct (戻り値は struct のコピーを作成する必要がある)

このような簡単なデータ型では、必要以上にメモリコピーを行うと CPU を圧迫し、データ キャッシュへの影響も出る可能性があります。より複雑なデータ型の場合、パフォーマンスやメモリへの対価はより大きくなります。より複雑なデータ型が関与した場合に何か発生するかを調べるために、MyMemoryAbusingStruct をサンプルとして見てみましょう。MyMemoryAbusingStruct の各メンバは配列であることが分かります。Unreal の配列コンテナは 12 バイトのメモリを消費するので、インスタンスあたり構造体はちょうど 36 バイトを消費します。しかし、これらの配列に項目を追加すると、そのデータを格納するためのメモリを確保する必要があります。構造体は複雑なタイプの配列なので、効率的なメモリコピーを使わずに手動で配列の各要素をコピーする必要があります。

最初に、コードはアクセサ関数を呼び出して Instance1 に格納されているデータを取得します。戻り値はすべて参照で渡すことができず、コピーしなければなりません。これにより、構造体のサイズ (36 バイト) を格納するためのメモリ割り当てが発生し、その後で各配列がデータ型のサイズ x 項目数を格納するためのメモリ割り当てを実行します。最後に、各配列の各要素がコピーされ、文字列配列がさらに割り当てとコピーを生成します (文字列は文字の配列であるため)。続いてこの構造体は SetGroupOfStuff() に渡され、それにより完全なコピーシーケンスがもう一度発生します。次の GetInstance1() の呼び出しも、また同じコピー プロセスを実行します。スクリプトコンパイラは最適化のないコンパイラであるためです。他の関数呼び出しでもこのパターンが繰り返されてデータのコピーがさらに作成され、結果として Instance1 は 6 つのコピー (native の場合は 9)、Instance2 は 2 つのコピーになります。これらの割り当てはすべて、下層のメモリマネージャを圧迫し、データキャッシュが強打され、何の恩恵もなく CPU 時間を無駄に使いつくしています。

MyMemoryAbusingStruct Example



DoAbuse()
   (1) = GetInstance1()                  <--- Return value must make a copy the struct
   SetGroupOfStuff(copies the value (1))
   (2) = GetInstance1()                  <--- Return value must make a copy the struct
   (3) = GetInstance2()                  <--- Return value must make a copy the struct
   SetGroupOfStuffEx(copies the value (2),copies the value (3))
   (4) = GetInstance1()                  <--- Return value must make a copy the struct
   SetGroupOfStuff(copies the value (4))

幸いなことに、このコードの最適化は非常に簡単ですが、その恩恵は非常に大きいものです。このドキュメントの最後に MySlowness クラスの最適化バージョンがありますが、これは naive 実装より全体的に 30 倍速くなっています。

2 番目のバージョンの DoSlowness() には、重要な最適化が施されています。まず、InitMySlowStruct() メソッドの呼び出しをやめました。この戻り値が不要なコピーを実行するからで、おまけに、スクリプトインタープリタがすべてのローカル変数をゼロにするという事実も活用せず、既にゼロの値にゼロを割り当てているためです。ループ内では、変更される値だけが構造体のメンバに割り当てられる点に注意してください。2 番目の最適化は、MaxOfMySlowStruct() メソッドがすべてのデータを参照渡しにしていることです。これは、メモリコピーによって各要素の最大値を判定することも、2 つの構造体から生み出される値を返すこともないことを意味します。これらの簡単な変更のお陰で、コードは 2.5 倍速く実行されるようになりました。

/** 遅いコードを呼び出してみる */
function DoSlowness()
{
   local int Counter;
   local MySlowStruct First,Second,MaxSlowStruct;
   First.A = 1.0;
   Second.C = 1.0;

   for (Counter = 0; Counter < 32; Counter++)
   {
      First.D = float(Counter);
      Second.A = float(Counter);
      MaxOfMySlowStruct(MaxSlowStruct,First,Second);
      // MaxSlowStruct で何か実行する
   }
}

他の関数にも同じ最適化を適用することで、より目覚しい成果が得られます。アクセサ関数は必ずコピーを実行するので、それらを除外しました。SetGroupOfStuff() と SetGroupOfStuffEx() メソッドは、値ではなく参照渡しに変更されています。最終結果は圧倒的で、多量のデータ配列を使用していた古いやり方と比べて 364 倍速くなり、データのコピー回数はゼロになりました。その他のオーバーヘッドをすべて含めても、この簡単な変更だけで最終バージョンのコードは 30 倍速くなりました (Tick() 時間を含む)。

/** メモリの乱用を実証する */
function DoAbuse(bool bShouldUse2ndSet)
{
   if (bShouldUse2ndSet)
   {
      SetGroupOfStuff(Instance1,1.0);
      SetGroupOfStuffEx(Instance1,Instance2);
      SetGroupOfStuff(Instance1,0.0);
   }
   else
   {
      SetGroupOfStuff(Instance3,1.0);
      SetGroupOfStuffEx(Instance3,Instance4);
      SetGroupOfStuff(Instance3,0.0);
   }
}

注意: さらに大切なのは、スクリプトの "const out" 構文を使って native 関数が適切に宣言されていることの確認です。スクリプトだけの場合より、関数パラメータあたりコピーが 1 回余計に発生するされるからです。これは、スクリプトはコピーを作成して C++ コードに渡し、C++ レイヤーも (構造体が参照で渡されることから) コピーを作成するために発生します。スクリプトから C++ に何かを渡すのが遅くなるのは、このときだけかもしれません。

MySlowness.uc

class MySlowness extends Actor;

/** この構造体が何度も不要にコピーされる */
struct MySlowStruct
{
   var float A, B, C, D;
};

/** この構造体はアロケーターを圧迫し、キャッシュを不要に圧倒する */
struct MyMemoryAbusingStruct
{
   var array<MySlowStruct> SlowStructs;
   var array<string> SomeStrings;
   var array<vector> SomeVectors;
};

// メモリを乱用する多数のインスタンス
var MyMemoryAbusingStruct Instance1;
var MyMemoryAbusingStruct Instance2;
var MyMemoryAbusingStruct Instance3;
var MyMemoryAbusingStruct Instance4;

var float ElapsedTime;

/** instance 1 へのアクセサ */
simulated function MyMemoryAbusingStruct GetInstance1()
{
   return Instance1;
}

/** instance 2 へのアクセサ */
simulated function MyMemoryAbusingStruct GetInstance2()
{
   return Instance2;
}

/** instance 3 へのアクセサ */
simulated function MyMemoryAbusingStruct GetInstance3()
{
   return Instance3;
}

/** instance 4 へのアクセサ */
simulated function MyMemoryAbusingStruct GetInstance4()
{
   return Instance4;
}

/**
 * 遅い構造体の初期化
 */
function MySlowStruct InitMySlowStruct(float A,float B,float C,float D)
{
   local MySlowStruct MSS;
   MSS.A = A;
   MSS.B = B;
   MSS.C = C;
   MSS.D = D;
   return MSS;
}

/** 不要なコピーを実行 */
function MySlowStruct MaxOfMySlowStruct(MySlowStruct SlowA,MySlowStruct SlowB)
{
   local MySlowStruct MSS;
   MSS.A = Max(SlowA.A,SlowB.A);
   MSS.B = Max(SlowA.B,SlowB.B);
   MSS.C = Max(SlowA.C,SlowB.C);
   MSS.D = Max(SlowA.D,SlowB.D);
   return MSS;
}

/** 遅いコードを呼び出してみる */
function DoSlowness()
{
   local int Counter;
   local MySlowStruct MaxSlowStruct;

   for (Counter = 0; Counter < 32; Counter++)
   {
      MaxSlowStruct = MaxOfMySlowStruct(InitMySlowStruct(1.0,0.0,0.0,float(Counter)),InitMySlowStruct(float(Counter),0.0,1.0,0.0));
      // MaxSlowStruct で何か実行する
   }
}

/** メモリの乱用を示すため */
function SetGroupOfStuff(MyMemoryAbusingStruct MemAbuse,float SomeAbuseValue)
{
}

/** メモリの乱用を示すため */
function SetGroupOfStuffEx(MyMemoryAbusingStruct MemAbuse,MyMemoryAbusingStruct MoreAbuse)
{
}

/** メモリの乱用を実証する */
function DoAbuse(bool bShouldUse2ndSet)
{
   if (bShouldUse2ndSet)
   {
      SetGroupOfStuff(GetInstance1(),1.0);
      SetGroupOfStuffEx(GetInstance1(),GetInstance2());
      SetGroupOfStuff(GetInstance1(),0.0);
   }
   else
   {
      SetGroupOfStuff(GetInstance3(),1.0);
      SetGroupOfStuffEx(GetInstance3(),GetInstance4());
      SetGroupOfStuff(GetInstance3(),0.0);
   }
}

/** フレーム単位で暴れてみる */
event Tick(float DeltaTime)
{
   DoSlowness();

   ElapsedTime += DeltaTime;
   if (ElapsedTime > 0.5)
   {
      ElapsedTime = 0.0;
      DoAbuse(true);
   }
   else
   {
      DoAbuse(false);
   }
}

MySlownessOptimized.uc

class MySlownessOptimized extends Actor;

/** この構造体が何度も不要にコピーされる */
struct MySlowStruct
{
   var float A, B, C, D;
};

/** この構造体はアロケーターを圧迫し、キャッシュを不要に圧倒する */
struct MyMemoryAbusingStruct
{
   var array<MySlowStruct> SlowStructs;
   var array<string> SomeStrings;
   var array<vector> SomeVectors;
};

// メモリを乱用する多数のインスタンス
var MyMemoryAbusingStruct Instance1;
var MyMemoryAbusingStruct Instance2;
var MyMemoryAbusingStruct Instance3;
var MyMemoryAbusingStruct Instance4;

var float ElapsedTime;

/** 不要なコピーを実行 */
function MaxOfMySlowStruct(out MySlowStruct MaxStruct,const out MySlowStruct SlowA,const out MySlowStruct SlowB)
{
   MaxStruct.A = Max(SlowA.A,SlowB.A);
   MaxStruct.B = Max(SlowA.B,SlowB.B);
   MaxStruct.C = Max(SlowA.C,SlowB.C);
   MaxStruct.D = Max(SlowA.D,SlowB.D);
}

/** 遅いコードを呼び出してみる */
function DoSlowness()
{
   local int Counter;
   local MySlowStruct First,Second,MaxSlowStruct;
   First.A = 1.0;
   Second.C = 1.0;

   for (Counter = 0; Counter < 32; Counter++)
   {
      First.D = float(Counter);
      Second.A = float(Counter);
      MaxOfMySlowStruct(MaxSlowStruct,First,Second);
      // MaxSlowStruct で何か実行する
   }
}

/** For showing the memory abuse */
function SetGroupOfStuff(const out MyMemoryAbusingStruct MemAbuse,float SomeAbuseValue)
{
}

/** メモリの乱用を示すため */
function SetGroupOfStuffEx(const out MyMemoryAbusingStruct MemAbuse,const out MyMemoryAbusingStruct MoreAbuse)
{
}

/** メモリの乱用を実証する */
function DoAbuse(bool bShouldUse2ndSet)
{
   if (bShouldUse2ndSet)
   {
      SetGroupOfStuff(Instance1,1.0);
      SetGroupOfStuffEx(Instance1,Instance2);
      SetGroupOfStuff(Instance1,0.0);
   }
   else
   {
      SetGroupOfStuff(Instance3,1.0);
      SetGroupOfStuffEx(Instance3,Instance4);
      SetGroupOfStuff(Instance3,0.0);
   }
}

/** フレーム単位で暴れてみる */
event Tick(float DeltaTime)
{
   DoSlowness();

   ElapsedTime += DeltaTime;
   if (ElapsedTime > 0.5)
   {
      ElapsedTime = 0.0;
      DoAbuse(true);
   }
   else
   {
      DoAbuse(false);
   }
}