Войти
UDKСтатьи

Применение структур в сценариях UnrealScript

Автор:

Обзор
Примеры
  MySlowness.uc
  MySlownessOptimized.uc

Обзор

Структуры UnrealScript являются мощным механизмом для группировки связанных элементов данных. Так же, как и в C++, объявление структур и доступ к членам экземпляров структур никак не сказывается на производительности. Однако следует учитывать, что как и в C++, передача структуры в качестве аргумента функции по значению влияет на производительность. Данный документ поможет вам разобраться в особенностях использования структур языка UnrealScript.

Подробнее о структурах и спецификаторах структур читайте на странице UnrealScript. Справочное руководство.

Примеры

Давайте рассмотрим простой класс, оперирующий несколькими структурами (смотрите сценарий MySlowness.uc, расположенный ниже). Он решает довольно распространенные задачи: объявляет несколько структур для объединения нескольких типов данных в единые составные типы, объявляет экземпляры этих структур и, наконец, определяет несколько функций для работы с этими структурами.

Класс MySlowness выполняет определенную работу каждый тик. Во-первых, в цикле он сравнивает значения членов экземпляров структуры MySlowStruct на предмет поиска максимальных. Во-вторых, изменения, сделанные в экземплярах структуры MyMemoryAbusingStructs, передаются в другим функциям. Отметим, что экземпляры структур возвращаются через функции доступа и все функции класса MySlowness передают структуры по значению. Подобный подход кажется естественным применительно ко многим более сложным типам данных, передаваемых точно также, например, в случае с экземплярами акторов, объектов и т.д. Однако, движок неявно передает экземпляры всех классов, производных от Object, по ссылке, а не по значению. Структуры неявно передаются по значению, что, естественно, сказывается на производительности.

Для начала давайте рассмотрим, как работа класса MySlowStruct сказывается на производительности. Класс включает в себя удобный метод для инициализации значений всех своих переменных. Другой метод класса сравнивает два экземпляра структуры и создает новый экземпляр, содержащий максимальные значения первых двух. Данный подход создает несколько нежелательных копий структуры, снижая производительность. Функция InitMySlowStruct(), возвращая результат, фактически создает копию данных экземпляра структуры в памяти. Это происходит при каждом ее вызове в методе 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
      (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
      

В случае с простыми типами данных, такими как MySlowStruct, нагрузка на центральный процессор и память относительно невелика. Для обработки же более сложных типов производительности и памяти требуется гораздо больше. На примере структуры MyMemoryAbusingStruct рассмотрим влияние на производительность более сложных типов данных. Обратите внимание, что каждый член структуры MyMemoryAbusingStruct является массивом. Каждый контейнер массива занимает 12 байт памяти, поэтому структура использует только 36 байт на каждый экземпляр. Однако, как только вы добавите элементы в эти массивы, для хранения данных каждого из массивов будет выделена дополнительная память. Поскольку элементы массивов являются сложными типами данных, копирование массивов не может быть осуществлено за один этап, каждый элемент этих массивов необходимо скопировать по отдельности.

Во-первых, для получения данных, содержащихся в Instance1, осуществляется вызов функции доступа. Возвращаемое значение не может быть передано по ссылке и должно быть скопировано. Это порождает выделение памяти для хранения структуры (36 байт), далее выделяется память для хранения элементов массивов в размере типа данных каждого элемента, помноженном на количество элементов. Наконец, каждый элемент каждого массива поэлементно копируется в экземпляр структуры, возвращаемый функцией. Затем эта структура передается в функцию SetGroupOfStuff() и все операции по выделению памяти и копированию элементов осуществляются повторно. Поскольку компилятор сценариев не является оптимизирующим компилятором, при следующем вызове GetInstance1() этот процесс повторяется. Эта картина повторяется и с другими вызовами функций, создавая все больше и больше копий данных. В результате мы получаем 6 отдельных копий Instance1 (или 9 для встроенного кода) и 2 копии Instance2 (или 4 для встроенного кода). Все эти операции создают нагрузку на менеджер памяти, засоряют кэш данных и используют массу процессорного времени без всякой реальной пользы.

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, которая работает примерно в 30 раз быстрее, чем наивная реализация, приведенная выше.

Вторая версия функции DoSlowness() включает в себя несколько важных оптимизаций. Во-первых, она больше не вызывает метод InitMySlowStruct() и, следовательно, не создает ненужных копий данных. Помимо этого она не обнуляет локальные переменные, которые по умолчанию уже обнулены. Обратите внимание, что в цикле новые значения присваиваются только необходимым членам структуры. Вторая оптимизация заключается в том, что метод MaxOfMySlowStruct() принимает все параметры по ссылке и, следовательно, копии параметров и возвращаемого значения функции не создаются. Эти простые модификации кода ускоряют его выполнение в 2,5 раза.

/** 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
   }
}

Применение этих же оптимизаций к другим функциям позволяет достичь еще более впечатляющих успехов. Функции доступа, создающие ненужные копии структур, были удалены. Методы SetGroupOfStuff() и SetGroupOfStuffEx() теперь принимают параметры по ссылке, а не по значению. Окончательный результат исключает многократное поэлементное копирование массивов и примерно в 364 раз быстрее старого метода. С учетом всех прочих оптимизаций окончательный вариант кода в 30 раз быстрее (с учетом вызова метода Tick()).

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

Примечание: Важно убедиться, что параметры ваших встроенных функций объявлены со спецификатором "const out". Это важно потому, что встроенные функции при взаимодействии с кодом сценариев создают копии параметров: одну при переносе данных из кода сценария в код на C++, и еще одну при переносе результатов из кода на C++ в код сценария.

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);
   }
}
Данный документ является переводом оригинального документа Unreal Script Struct Usage.

#UDK, #Unreal Development Kit

7 июня 2012