UDN
Search public documentation:

UnrealScriptStructs
日本語訳
中国翻译
한국어

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

UE3 Home > UnrealScript > Unreal Script Struct Usage

Unreal Script Struct Usage


Overview


UnrealScript structs are powerful mechanisms for grouping related data members together. Just as in C++, there's nothing inherently slow about declaring or operating open the struct's data members. However, just like passing structs in C++ by value affects performance, the same is true for passing script structs by value. The purpose of this document is to look at how pass by value is handled by the script interpreter.

See the UnrealScript Reference for more information on structs and other features of UnrealScript.

Example Case


Let's take a simple class that manipulates a number of script structs (see MySlowness.uc below). It uses a pretty common pattern: it declares some custom structs for aggregating types into a more complex entity; it declares instances of those structs as members of the MySlowness class; and finally, it declares some functions to operate on those structures.

The MySlowness class performs some work each tick. First, it iterates over a range of values to find the max type of a MySlowStruct. Second, it changes which version of the instanced MyMemoryAbusingStructs are passed in as state to other functions. Note that the instances are returned via accessor functions. All of the functions listed in MySlowness pass their structs by value. This seems natural as many more complex data types are passed this way, such as Actor, Object, etc. However, the engine implicitly treats all Object derived entities as pass by reference and not pass by value. Script structs do not have this special handling and, as such, there are performance implications with passing by value.

First let's examine the performance implications of using the MySlowStruct example. There's a convenience method for initializing all of its values. There's another method that given two instances of that struct will create a new struct with the max values of the two. The code creates a number of unintended copies of the struct, slowing the game down. The flow in MySlowStruct shows that the return value from InitMySlowStruct() performs a memory copy into a destination. This happens once for each call of it with the results passed into the MaxOfMySlowStruct() method. Because that function is pass by value on both parameters, it creates 2 more copies of the data. Finally, the resulting data is copied into the destination variable. So in one chunk of code we've managed to copy data roughly 5 times.

MySlowStruct Example

DoSlowness()
   For 1 to 32
      (1) = InitMySlowStruct(passed in values copied to function)   <--- Return value must make a copy the struct
      (2) = InitMySlowStruct(passed in values copied to function)   <--- Return value must make a copy the struct
      MaxOfMySlowStruct(copies the value (1),copies the value (2))   <--- Return value must make a copy the struct
      

On a simple data type such as this, it is putting pressure on the CPU to copy more memory around than needed and potentially affecting the data cache. On more complex types, the performance and memory considerations are much greater. Let's take a look at the MyMemoryAbusingStruct as an example of what happens when more complex types are involved. Notice that each member of the MyMemoryAbusingStruct is an array. Our array container consumes 12 bytes of memory, so the struct uses just 36 bytes per instance. However, once you add items to those arrays, the arrays must allocate memory to hold that data. Because the structs are complex types of arrays, each element of those arrays needs to be copied manually instead of using an efficient memory copy.

First, the code calls the accessor function to get the data held in Instance1. All return values cannot be passed by ref and must be copied. This generates a memory allocation to hold the size of the struct (36 bytes); followed by a each array performing an allocation to hold data type size * number of items; and finally, each element in each array is copied with the string arrays generating additional allocations and copies since strings are arrays of characters. That struct is then passed into SetGroupOfStuff() causing another full copy sequence to happen. The next call to GetInstance1() also performs the same copy process because the script compiler is a non-optimizing compiler. This pattern repeats with the other function calls making more and more copies of the data with results being 6 separate (9 if native) copies of Instance1 and 2 (4 if native) copies of Instance2. All of these allocations cause pressure on the underlying memory manager, thrash the data cache, and use up tons of CPU time for no real benefit.

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))

Fortunately, the optimization of this code is pretty straight forward to do and generates huge benefits. At the end of this document, there is an optimized version of the MySlowness class that is overall 30 times faster than the naive implementation.

The second version of DoSlowness() has some important optimizations. First, it no longer calls the InitMySlowStruct() method as the return value performs an unneeded copy. It also doesn't leverage the fact that the script interpreter zeros all local variables, so it was assigning values to zero that were already zero. Note in the loop only the changing values are assigned to members in the struct. The second optimization is that the MaxOfMySlowStruct() method is passing all data by reference. This means that there is no memory copying going on to determine the max of each element and there is no copying of memory to return the value that is built from the two structs. These simple changes made the code run 2.5 times faster.

/** Let's call our slow code */
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);
      // Do something with MaxSlowStruct
   }
}

Applying the same optimizations to the other functions yeilds more impressive gains. Because the accessor functions guarantee a copy, they were removed. The SetGroupOfStuff() and SetGroupOfStuffEx() methods were changed to pass by reference instead of value. The final result is a mind boggling 364 times faster than the old way when using arrays with large amounts of data in them and now copies the data zero times. Including all other overhead, the final version of the code is 30 times faster (Tick() time inclusive) with very simple changes.

/** Illustrates memory abuse */
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);
   }
}

NOTE: It is even more important to make sure your native functions are declared properly using scripts "const out" syntax because that incurs one more copy per function parameter than in the script only case. The reason for this is that script makes a copy to pass to the C++ code and the C++ layer also makes a copy because the struct is passed by reference. This may be the only time it's slower to move something to C++ from script.

MySlowness.uc


class MySlowness extends Actor;

/** This struct will be needlessly copied many, many times */
struct MySlowStruct
{
   var float A, B, C, D;
};

/** This struct will put pressure on the allocator and thrash the cache needlessly */
struct MyMemoryAbusingStruct
{
   var array<MySlowStruct> SlowStructs;
   var array<string> SomeStrings;
   var array<vector> SomeVectors;
};

// A bunch of instances of memory abuse
var MyMemoryAbusingStruct Instance1;
var MyMemoryAbusingStruct Instance2;
var MyMemoryAbusingStruct Instance3;
var MyMemoryAbusingStruct Instance4;

var float ElapsedTime;

/** Accessor to instance 1 */
simulated function MyMemoryAbusingStruct GetInstance1()
{
   return Instance1;
}

/** Accessor to instance 2 */
simulated function MyMemoryAbusingStruct GetInstance2()
{
   return Instance2;
}

/** Accessor to instance 3 */
simulated function MyMemoryAbusingStruct GetInstance3()
{
   return Instance3;
}

/** Accessor to instance 4 */
simulated function MyMemoryAbusingStruct GetInstance4()
{
   return Instance4;
}

/**
 * Initializes my slow struct
 */
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;
}

/** Let's do some needless copies */
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;
}

/** Let's call our slow code */
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));
      // Do something with MaxSlowStruct
   }
}

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

/** For showing the memory abuse */
function SetGroupOfStuffEx(MyMemoryAbusingStruct MemAbuse,MyMemoryAbusingStruct MoreAbuse)
{
}

/** Illustrates memory abuse */
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);
   }
}

/** Do the bad stuff once per frame */
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;

/** This struct will be needlessly copied many, many times */
struct MySlowStruct
{
   var float A, B, C, D;
};

/** This struct will put pressure on the allocator and thrash the cache needlessly */
struct MyMemoryAbusingStruct
{
   var array<MySlowStruct> SlowStructs;
   var array<string> SomeStrings;
   var array<vector> SomeVectors;
};

// A bunch of instances of memory abuse
var MyMemoryAbusingStruct Instance1;
var MyMemoryAbusingStruct Instance2;
var MyMemoryAbusingStruct Instance3;
var MyMemoryAbusingStruct Instance4;

var float ElapsedTime;

/** Let's do some needless copies */
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);
}

/** Let's call our slow code */
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);
      // Do something with MaxSlowStruct
   }
}

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

/** For showing the memory abuse */
function SetGroupOfStuffEx(const out MyMemoryAbusingStruct MemAbuse,const out MyMemoryAbusingStruct MoreAbuse)
{
}

/** Illustrates memory abuse */
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);
   }
}

/** Do the bad stuff once per frame */
event Tick(float DeltaTime)
{
   DoSlowness();
   
   ElapsedTime += DeltaTime;
   if (ElapsedTime > 0.5)
   {
      ElapsedTime = 0.0;
      DoAbuse(true);
   }
   else
   {
      DoAbuse(false);
   }
}