left_top.gif (4373 bytes)

Mahys.narod.ru

Главная
Flash Игры
Исходники
Статьи
Гостевая

Оптимизация кода

Оптимизация кода очень важная задача для разработки приложений связанных с обработкой данных и 3D-моделирования. Для написания производительного кода надо знать не только тонкости языка в данной технологии, но и рационально писать алгоритмы обработки данных, то есть избавляться от лишней работы. Решениями для проблемных мест могут быть различные подходы, применение определенных методов сортировки и вычислений, что существенно может улучшить код, особенно вместе с хорошей его оптимизацией.

Самый надежный способ проверить код на производительность - вооружиться тестовым кодом (если приложение довольно большое, то желательно написать юнит-тесты для всех критически важных мест) в котором проверять интересующие части. Простым инструментом может послужить тест-код простого сравнения времени выполнения:


function GetTimeInterval(f:Function, count:Number):Number
{
	var start:Number = getTimer();
	for (var i:Number = 0; <count; i++) f();
	return getTimer() - start;
}

где, f - функция содержащая тестируемый код, count - количество повторений для более точной оценки. Все последующие рекомендации тестировались при помощи такой функции.

В дополнение к этому можно проводить анализ и оптимизацию при помощи байт кода. Для этих целей понадобиться специальная бесплатная программа flasm (http://flasm.sourceforge.net). Данный метод более гибкий и эффективный, так как позволяет видеть последовательность команд для виртуальной машины флеш. Но надо быть очень осторожным, так как не всегда более короткий байт код должен обязательно быстрее работать, требуется учитывать каким образом он выполняется, не делается ли более громоздкая работа, ведь сама виртуальная машина может быть с оптимизирована на выполнение определенных команд.

Короткие имена

У флеша есть особенность - чем короче имя переменной или названия функции, тем она быстрее обрабатывается. Тестирование показало, что даже при очень длинных именах переменных и функций, какие навряд ли кто пишет, расхождение по производительности очень мало. Но в важных местах уменьшение длины имен тоже может повысить производительность.

Компактные выражения и константы

Короткие описания выполняемых действий тоже дают преимущества. Так инициализация нескольких переменных одним значением: x = y = z = 0, работает несколько быстрее, чем построчная, где каждой переменной отдельно это значение присваивается.

Данная особенность относиться и к выражениям, как числовым так и логическим. Например, все числовые вычисления, занесенные в одну формулу, будут выполняться быстрее, чем эти же вычисления разбитые на несколько последовательных под вычислений. Особо следует отметить способность компилятора вычислять и сохранять константные значения выражений в байт-код, как для математических операций, так и для функций с константным параметром:


x = Math.cos(Math.PI/4+1/2) + 5*3/12;

- значение в переменной x будет вычислено и сохранено в байт-код во время компиляции, а при выполнении кода будет использовано это значение. Чтоб помочь компилятору распознать константные выражения в сложной формуле надо выделить их скобками, иначе эти вычисления будут выполняться во время работы. В коде:


y = ((10-5)/(123 - 85))*x + (23*6/5);

арифметические операции в скобках будут вычислены компилятором и сохранены в байт-код, а при выполнении данной строки в программе будут вставлены на свои места, что значительно ускорит вычисление.

При использовании оператора условия, необходимо помещать по-возможности все логические значения или выражения сразу:


if (a && b && c){...}

так как это работает значительно быстрее, чем описание всех веток:

if (a)
{
	if (b)
	{
		if (c){ ... }
	}
}
      

Сделать код более компактным могут помочь и специальные операторы, например ?:, который заменяет две ветки оператора if c присваиванием.

Преобразования типов

Как и во многих языках программирования в Action Script существует неявное преобразование типа, заданного значения к требуемому типу. Этим можно пользоваться для уменьшения лишних операций в коде. Самый частый пример такого использования - логические флаги с булевыми и числовыми значениями. Написание:


n_hide = 1;
b_hide = true;
b_move = false;
if (n_hide && b_hide && !b_move) {...}

предпочтительнее чем


n_hide = 1;
b_hide = true;
b_move = false;
if ((n_hide == 1) && (b_hide == true) && (b_move == false)) {...}

так как в последнем случае мы добавляем лишнюю работу по сравнению. Такой подход дает немалое улучшение производительности в циклах уменьшающих итератор до нуля, для этого достаточно условием цикла написать саму переменную, при нулевом значении условие возвратит false и цикл завершится:


for (var i = 10; i; --i){...}
while (—i){...}

Функции

Никакой серьезный код не может обойтись без функций, они дают выигрыш по размеру кода, когда требуется выполнить одни и те же действия в разных местах, делают код более осмысленным и логичным. Но вызов функции занимает дополнительное время. Например, вот такой безобидный код:


function Sum(x:Number, y:Number):Number
{
	return x + y;
}
x = 1;
y = 2;
z = Sum(x, y);

работает в 1.5-2 раза медленнее, чем аналогичный:


x = 1;
y = 2;
z = x + y;

И это достаточно логично, так как происходит много лишних действий: вызов функции, передача параметров, возврат значения - все в конечном итоге сказывается на производительности. Поэтому желательно ограничивать использование вызовов функций в местах с повышенным требованием производительности или же совсем отказываться от них путем прямого встраивания ее кода (в языке С++ есть объявление функции с директивой inline, что указывает на прямое встраивание содержимого функции в место ее вызова, в AS к сожалению такого нет).

Требуется строить код преимущественно на стандартных функциях обработки данных, так как они выполняются значительно быстрее определенных в программе. Например, функция Math.max, будет работать вдвое быстрее, чем любая ее аналогичная реализация в коде или в виде отдельной функции.

Глобальные переменные

При использовании глобальных переменных как объявленных на глобальном объекте _global, так и на любом другом (например, объекте муви-клипа), следует помнить что доступ к таким переменным замедлен и постоянное размещение в памяти во время проигрывания приводит к потерям производительности кода. Поэтому целесообразно в случаях длительного не использования удалять их из памяти командой delete или же к крайнем случае устанавливать значение равное null (забирает меньше времени на хранение и обработку).

Локальные переменные

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


function a()
{
	x:Number = 10;
	y:Array = new Array();
	for (i=0; i<x; i++) y[i] = i;
}

приводит к созданию лишних переменных x, y, i на объекте где функция вызывается (например, _root), которые будут находиться постоянно в памяти, что может замедлять работу данного кода во много раз. Поэтому в данном случае функция должна иметь вид:


function a()
{
	var x: Number = 10;
	var y:Array = new Array();
	for (var i:Number=0; i<x; i++) y[i] = i;
}

Массивы

Массив - очень гибкий тип AS при помощи которого можно создавать любой сложности динамические структуры данных. Он также имеет ряд особенностей.

Первая из них объявление и инициализация массива. Неявное создание массива немного быстрее явного создания класса массива. То есть arr = []; будет выполняться быстрее, чем arr = new Array();. Еще больший выигрыш получается при инициализации массива: arr = [1,2,3]; быстрее, чем arr = new Array(1,2,3);. И чем больше у нас элементов или же они более сложные (например, инициализация строковыми элементами), тем будет быстрее.

Следующая особенность с массивами заключается в прямой работе с его элементами, избегая использования метода push, который можно заменить следующим образом:


arr[arr.length] = 11; // push

Это может значительно ускорить работу кода, особенно если это выполняются очень интенсивно в цикле.

Объекты

Объекты, пришедшие из Flash Player 5, тоже, как и массивы неявно создаются быстрее: obj = {}; быстрее чем obj = new Object();. Но самое большое преимущество дает инициализация, то есть obj = {name1: value1, name2: value2,...nameN: valueN} может в разы превосходить по скорости аналогичную инициализацию c new Object(), так как в этом случае надо последовательно присваивать все значения полей: obj.name1 = value1; ... obj.nameN = valueN;, что забирает много времени.

Доступ

Для программного управления анимацией создаются объекты муви-клипы. Управление ими осуществляется через доступ к их свойствам, вызова их функций. В последних версиях macromedia рекомендует использовать “dot” нотацию, для доступа к объектам через глобальный путь. Это является самым быстрым способом работы по сравнению со всеми другими. Глобальный путь избавляет плеер проводить поиск указанного объекта во всех областях видимости, то есть, если внутри данной области он ничего не находит по заданному имени он ищет выше. Поэтому для быстродействия желательно не опускать глобальный путь, даже если объекты лежат в одной области видимости и можно писать просто их имена. Простой код:


my_mc._x = 10;

на 25% уступает по времени выполнения более оптимальному аналогу:


_root.my_mc._x = 10;

Это также касается и вызовов функций, как определенных, так и стандартных:


y = Math.cos(x);
z = getValue(x);

на 10% времени медленнее, чем:


y = _global.Math.cos(x);
z = _root.getValue(x);

К другим способам доступа можно отнести старую команду “tellTarget”, которая сейчас поддерживается для совместимости. Но она уступает при правильном написании кода, как по доступу к одиночному элементу, так и к группе свойств, так что от нее желательно отказаться. Это хорошо видно из следующего кода:


tellTarget(“/test_mc”)
{
	_x = 11;
	_y = 111;
	_alpha = 10;
}

который забирает на выполнение около 25% больше, чем:


var t = _root.test_mc;
t._x = 11;
t._y = 111;
t._alpha = 10;

Можно предположить, что последний код можно дополнительно оптимизировать установив групповую команду “with”. Но, к сожалению, это не так. Данная операция добавляет упрощение кода ценой отбора “на ровном месте” более 30% производительности! Другими словами не рекомендуется использовать “with”, для работы с объектом в критически важных местах, лучше пользоваться прямым оптимизированным доступом, как показано в коде выше.

Доступ через массив - _root[“test_mc”], равнозначен “dot“-нотации по скорости выполнения.

Самый громоздкий доступ выполняется через команду “eval()”. Эта функция позволяет на лету по строковому имени создавать доступ к любым переменным (объектам, клипам и т.д.), что требует достаточно много времени. Поэтому даже самый оптимально написанный код с eval() всегда будет около двух раз уступать по производительности доступу через массив или точку, поэтому последний хорошая ему альтернатива. Это относится и для применения команд set, get, setProperty, getProperty - выполнение которых занимает довольно много времени. В связи с этим рекомендуется отказаться или максимально ограничить использование таких функций в коде и пользоваться исключительно доступом через точку.

Циклы

Ключевой момент оптимизации - циклы, так как чаще всего именно в них происходит интенсивная обработка больших объемов данных. Главная рекомендация по их оптимизации - убрать всю лишнюю работу и сделать код внутри цикла как можно компактней. Для этого подходят выше описанные рекомендации, но есть также ряд особенностей.

1. Переменные

Все объявления локальных переменных используемых в цикле лучше размещать перед циклом, это избавит плеер от необходимости каждый раз создавать и удалять переменную из памяти при выполнении цикла, что значительно ускоряет работу.

2. Кеширование пути

При частом использовании какого либо глобального объекта или переменной, весьма не эффективно каждый раз обращаться ко всем его свойствам через абсолютный путь в цикле. При этом мы фактически задаем каждый раз операцию “точка” для доступа к его свойству, что заставляет плеер проходить один и тот же путь. Более оптимальное решение указать прямой путь к объекту в локальной переменной (кешируем во время выполнения) и вынести ее за пределы цикла. Таким образом мы значительно ускоряем доступ к объекту. Следующий код:


for (var i=0; i<10; i++)
{
	_root.obj.a = 1;
	_root.obj.b = 2;
	_root.obj.c = 3;
}

использует глобальный объект obj и его поля через полный путь. Но это почти в два раза работает медленнее (чем больше операций с объектом тем медленнее) чем аналогичный код, где полный путь мы храним локально:


var v_obj:Object = _root.obj;
for (var i=0; i<10; i++)
{
	v_obj.a = 1;
	v_obj.b = 2;
	v_obj.c = 3;
}

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


for (var i=0; i<10; i++)
	y = _global.Math.cos(i) + _root.getValue(i);

где, getValue - любая определенная функция в программе, будет выполняться на 50% времени дольше, чем его оптимизированный аналог:


var v_сos:Function = _global.Math.cos;
var v_getvalue:Function = _root.getValue;
for (var i=0; i<10; i++)
	y = v_cos(i) + v_getvalue(i);

3. Обход массива.

Практически любая обработка большого количества данных сводиться к обработке массива. Это реализуется обыкновенным циклом. Самый простой цикл для обхода массива “arr” выглядит следующим образом:


for (var i=0; i<arr.length; i++)
	arr[i] = i

Выше приведенный код желательно никогда не писать! Во-первых, если массив “arr” не локальный, то на него должна быть описана локальная переменная, как описывалось выше. Во-вторых, выражение “i<arr.length” выполняется при каждой итерации, следовательно каждый раз происходит считывание свойства “arr.length”. Поэтому, поместив длину массива в локальную переменную до цикла, мы получаем еще дополнительный выигрыш по времени. Получим, код, работающий в два раза быстрее:


var varr = _root.arr;
var len = varr.length;
for (var i=0; i<len; i++)
	varr[i] = i;

Еще немного ускорить работу может следующая конструкция оператора цикла:


var varr = _root.arr;
var len = varr.length;
var i = -1;
while (++i < len)
	varr[i] = i;

Пример цикла с while не на много быстрей чем с for, тесты показывают, что они равнозначны на небольших количествах итераций и непродолжительных вычислениях, только на больших можем получить немного преимущества.

Но, этот код самый оптимальный только в том случае, когда все ячейки массива имеют значения на всей длине “arr.length”. Но как только у нас массив используется как контейнер: используются функции pop, push, удаляются ячейки внутри массива или добавляются значения на любой индекс, эффективность такого кода резко падает, так как при обходе придется еще и проверять существует ли значение в ячейке. Потому, что свойство “length” возвращает длину массива как максимальный индекс массива плюс один, даже если массив почти пуст. В таких случаях лучше всего использовать цикл:


var varr = arr;
for (var i in varr)
	varr[i] = i;

так как он осуществляет обход только по ячейкам со значениями.

Невидимый клип

Если графический интерфейс состоит из множества скрытых клипов на сцене, которые могут быть показаны при определенных событиях или действиях пользователя, то важно правильно их прятать. Использование свойства _visible, и тем более _alpha (например, may_mc._visible = false; или may_mc._alpha = 0;) на объекте муви-клипа, совсем ошибочно. В действительности, плеер только не отрисовывает невидимый клип, но он его выполняет. Это легко проверить если поместить муви-клип с анимацией на сцену и в его фреймах написать вывод в Output при помощи команды “trase()”. Получим бесконечную обработку невидимого клипа. Это ощутимо может забрать ресурсы при достаточно большом количестве скрытых роликов. Выходом из этой ситуации может послужить дополнительный пустой фрейм на муви-клипе, на который мы переходим командой “gotoAndStop()”, что запрещает его дальнейшее выполнение. Если клип больше не нужен или не будет использоваться длительное время, желательно вообще удалять его “my_mc.unloadMovie();”.

Повторяемый код

Вся программная анимация на флеш основана на базовых понятиях фрейма и событий на нем. Поэтому и вся оптимизация главным образом должна быть сосредоточена на оптимизации кода выполняющегося по самым часто используемым событиям - это главным образом setInterval, onEnterFrame, onMouseMove. При использовании setInterval важно обязательно вызывать clearInterval, при окончании использования периодической функции. Но более важно разумно использовать onEnterFrame, onMouseMove, так как функции на них вызывается с большой частотой и рекомендуется не задавать их обработку одновременно большому количеству объектов клипов. Поэтому, если данная функция на объекте уже не нужна, то требуется удалять ее из памяти, в противном случае она будет оттягивать на себя время выполнение кода - сама при этом ничего не делая. Хороший пример этого - растворение-всплывание муви-клипа на экране. Так как после исчезновения клипу уже не требуется выполнять код своего растворения на onEnterFrame, то желательно удалить созданную функцию:


my_mc.onEnterFrame = function()
{
	_alpha -= 5;
	if (_alpha <= 0) delete this.onEnterFrame;
};

При выполнении выгрузки муви-клипа (unloadMovie), все его функции на событиях прекращают выполняться автоматически, но желательно удалить их из памяти перед тем как будет удален сам клип (возможно, unloadMovie не очищает все свои event handles).

Классы

ОПП безусловно добавляет широкие возможности для программирования. Ценой за это мы теряем в размере файла и в производительности. Поэтому, желательно очень четко сопоставлять преимущества и те недостатки, которые возникают при использовании классов. Например, если объекты довольно простые, используются редко и не требуют создания каких-либо иерархий, то желательно строить их на простых типах или объектах не создавая классы. Это хорошо можно сравнить на следующих примерах.
Путь нам надо иметь простую переменную строкового типа для хранения строки, которую мы будем не часто использовать, то тогда следующее:


var h_str:String = “Hello”;
h_str.substr(1, 2);

будет работать быстрее чем инициализация объектом класса String:


var str:String = new String(“Hello”);
h_str.substr(1, 2);

Но если же нам надо выполнять много операций над этой строкой при помощи методов класса String, то код:


var str:String = new String(“Hello”);
for (var i=0; i<20; i++)
	h_str.substr(1, 2);

будет работать в два раза быстрее, чем если бы мы задали переменную h_str простой строкой а не классом.
Это связано с тем, что происходит автоматическое преобразование типов при операции точка, к тому метод которого вызывается. Плееру каждый раз приходилось бы преобразовывать тип String к объекту класса String, что забирает дополнительное время.

Пусть нам надо построить простой объект, имеющий два свойства x и y и функцию для установки координат. Реализуем сначала это при помощи объекта:


var myobj:Object =
{
	x: 0,
	y: 0,
	setCoord: function(x,y)
	{
		this.x = x;
		this.y = y;
	}
};

Пустой муви-клип с этим объявлением на первом фрейме будет занимать всего 123 байта. Теперь реализуем то же самое на основе класса:


class myClass
{
	var x = 0;
	var y = 0;
	function setCoord(x, y)
	{
		this.x = x;
		this.y = y;
	}
}

После того как мы создадим объект строчкой:


var myclass:myClass = new myClass();

в другом чистом файле и скомпилируем - получим размер в 249 байт! Иными словами при одинаковой функциональности мы проигрываем в размере файла в два раза, так как в клип зашиваются все достоинства ОПП, которые нам может и совсем не нужны. Вызов функции setCoord(x, y) то же более медленный для объекта класса, что является еще одним аргументом против. При использовании ООП в AS2 следует не забывать о этих недостатках, а в остальном методы по оптимизации работы кода в классах аналогичны всем описанным выше.

Простые решения

Для написания хорошего оптимального кода, достаточно писать его красиво, логично и просто, избавляя от лишней работы и лишних данных. Вот несколько примеров на основе часто встречающихся проблем и их решений.

Функция

Частая ошибка - многократный вызовов функции, которая в действительности вызывается только один раз. Следующий код:


var value = 0;
...
for (var i=0; i<100; i++)
{
	if (IsValid(value)) {...};
	...
}

является не некорректным, так как постоянно вызывается функция, которая возвращает одно и то же значение (переменная value в цикле не меняется). Корректно написанный код должен выглядеть так:


var value = 0;
...
var is_valid = IsValid(value);
for (var i=0; i<100; i++)
{
	if (is_valid) {...};
	...

в этом случае мы значительно выигрываем по производительности, избавляясь от лишней работы.

Битовое поле

Другой пример основан на логичном использовании переменных для хранения данных. Путь у нас есть набор флагов состояния для объекта, принимающие только два значения 0 и 1. Эти флаги можно оформить в виде отдельных свойств и проверять их значения при при выполнении кода. Но этот подход весьма плох, так как делает код громоздким и влечет к хранению большого количества переменных, что в свою очередь замедляет доступ к ним. Например:


obj.hide = true;
obj.alpha = false;
obj.run = true;

Намного лучше оформить данные флаги в виде битового поля mode, которое и будет содержать все эти значения. Равнозначный код будет выглядеть следующим образом:


obj.mode = 5;

где, hide, alpha, run занимают 1, 2, 3 старшие биты соответственно. При присваивании 5 (101 в двоичном представлении) первый бит(hide) будет равен 1, второй(alpha) - 0, третий(run) - 1. Проверить такой флаг весьма просто выполнив битовую операцию над требуемым битом. То есть, для проверки состояния hide, требуется проверить первый бит:


is_alpha = obj.mode & 4;

При битовом логическом сложении, это будет выглядеть следующим образом:


	101 // 5 в десятичном представлении
	&&&
	100 // 4 в десятичном представлении
	--- 
	100 // 4 в десятичном представлении > 0 

Результат будет отличен от нуля, следовательно первый бит hide равен 1. Для проверки остальных битов alpha и run, нужно нужно брать числа 2 (010 двоичное) и 1 (001 - двоичное) соответственно.

Линейная матрица

Выполнении сложных геометрических расчетов или других действий по преобразованиям чаще всего основано на многомерных матрицах. Но для флеш-плеера работа с такими матрицами весьма громоздка и медлительна, так как плееру приходиться выполнять множественные сдвиги (операция []) для доступа к значению. Операция arr[3], будет всегда выполняться быстрее, чем arr[3][3] и т.д. Для обработки больших массивов лишние замедление может стать большой проблемой, поэтому следующий метод может помочь обойти это. Он заключается в представлении многомерного массива в виде вектора-строки (одномерного массива), фактически мы просто укладываем все строки массива последовательно в одну длинную. Например, для двумерной матрицы, которая имеет n столбцов и m строк, доступ к элементу в i столбце и j строке arr[i][j] будет выглядеть как:


         arr[i + n*j]; // (где i < n и j < m)

Без математических выводов это формула позволяет резервировать для ячеек столбцов n мест на позиции m строки в одномерном массиве. При этом важно, чтобы каждый индекс не был равен и не превышал максимальной значения (i < n и т.д), так как это может нарушить целостность данных массива для многих ячеек. Аналогичным образом для трехмерного массива размерности n*m*z - arr[i][j][k], формула примет вид:


         arr[i + n*j + (m*n)*k]; // (где i < n, j < m и k < z)
       

Такое преобразование не очень сложное, но дает довольно большое повышение производительности при работе с многомерными массивами.

Автор: Джордж Форест

 

 

Hosted by uCoz