
Через тернии к звездам. Оптимизация кода.
Эта статья посвящена оптимизации ActionScript3 на примере создания эффекта «полета сквозь звезды»(на подобии старого скринсейвера Windows). Мне этот эффект необходим был для игры. Эффект должен был служить фоном и поэтому требовалось чтобы он был как можно менее ресурсоемким.
Я не изобретаю никакие новые приемы оптимизации, я лишь приведу пример их применения на практике. Для начала я поискал в интернете готовые решения и наткнулся на решение, которое визуально меня устраивало: http://www.lemlinh.com/flash-source-starfield-generator/. Открыв код мы обнаружим, что эти Звездочки — это MovieClip'ы (крик ужаса за кадром). Каждый флеш-разработчик понимает, что кроме отображения «точки» на каждую «Звездочку-MovieClip» вешается куча свойств и методов класса MovieClip. И чем больше звезд — тем хуже. Я ничего не хочу сказать об авторе — просто нам требуется другое.
Ясно, что в такой ситуации прийдется сделать все самому и с нуля. Ничего гениального в нашей реализации нет:
Сразу оговорюсь, я использую собственный метод получения случайного значения в классе Tools.Utils.as:
Реализуем наш нехитрый алгоритм и получаем класс StarFieldBad.as. Код нам не важен. Но именно такой код я написал бы до того, как начал закомиться с методами оптимизации когда.
Звезд у нас будет меньше чем на небе, создадим их всего 2000 (реально их хватит 100-200, но нам для теста нужно побольше):
Выполнение инициализации не является критическим параметром, т.к. создание объекта делается не часто. Но все же среднее время создания:7,42ms.
Критическим показателем в нашем случае является время выполнения кадра: 9,1095ms.
Вроде бы и подход не самый страшный, и звезды не создаются каждый раз, но на самом деле это все не верх совершенства. Итак, пойдем по порядку.
В итоге время выполнения стало: 8,9753ms. (Печально, маленький прирост)
Цикл при каждом проходе обращается к объекту и его свойству. Заодно заменяем цикл for на цикл while:
Время выполнения: 7,5379ms. (Удивительно большой прирост)
Например у нас внутри циклов происходят подобные операции:
Заменяем оператор квадратные скобки на ссылку в локальной переменной (имеет смысл, если мы обращаемся к переменной много раз):
Время выполнения: 4.4279ms. (Невероятно большой прирост. Видимо, ещё сказывается большой объем массива)
Заменяем Array на Vector.<Point>. «В проигрыватель Flash Player 10 добавлен класс Vector, который обеспечивает более быстрый доступ для чтения и записи, чем класс Array.» (источник)
Время выполнения: 3.0963ms.
Можно заранее просчтитать значения таких выражений как «fieldWidth / 2» на «halfWidth» или «fieldHeight+offset» на «fieldWidthWithOffset» и сохранить их в переменные, тем самым избавив флеш от ~20000 лишних операций деления и сложения(это в нашем примере) за кадр:
Время выполнения: 2.9642ms. (ну, большого прироста никто и не обещал)
Далее я опущу описание других методов, которые мало сказались на производительности (но это не значит, что нужно ими пренебрегать!):
В итоге время выполнения скрипта было уменьшено с 9,7753ms до 2.9405ms. А это более чем в 3 раза! И время инициализации сократилось в 2 раза — с 7,42ms до 3,26ms.
Файл Tools.Utils.as.
Исходный файл StarFieldBad.as.
Итоговый файл StarField.as
Эти и подобные методы оптимизации следует научится использовать сразу, чтобы в последующем не пришлось проводить оптимизацию кода. Всем творческих успехов!
Пару ссылок по теме:
Оптимизация производительности для платформы Flash Platform
http://www.rozengain.com/blog/2007/05/01/some-actionscript-30-optimizations/
И конечно, Google
Я не изобретаю никакие новые приемы оптимизации, я лишь приведу пример их применения на практике. Для начала я поискал в интернете готовые решения и наткнулся на решение, которое визуально меня устраивало: http://www.lemlinh.com/flash-source-starfield-generator/. Открыв код мы обнаружим, что эти Звездочки — это MovieClip'ы (крик ужаса за кадром). Каждый флеш-разработчик понимает, что кроме отображения «точки» на каждую «Звездочку-MovieClip» вешается куча свойств и методов класса MovieClip. И чем больше звезд — тем хуже. Я ничего не хочу сказать об авторе — просто нам требуется другое.
Ясно, что в такой ситуации прийдется сделать все самому и с нуля. Ничего гениального в нашей реализации нет:
- создать массив точек stars:Array
- в каждом кадре:
- … просчитать новое положение каждой точке по суперсложной математической формуле (умножение).
- … нарисовать каждую точку на экране.
Сразу оговорюсь, я использую собственный метод получения случайного значения в классе Tools.Utils.as:
public static function rand(lowValue:Number, hightValue:Number, round:Number = 1):Number{
return lowValue + Math.floor(Math.random() * (hightValue-lowValue)*round)/round;
}
Реализуем наш нехитрый алгоритм и получаем класс StarFieldBad.as. Код нам не важен. Но именно такой код я написал бы до того, как начал закомиться с методами оптимизации когда.
Звезд у нас будет меньше чем на небе, создадим их всего 2000 (реально их хватит 100-200, но нам для теста нужно побольше):
background = new StarFieldBad(_game.STAGE.stageWidth, _game.STAGE.stageHeight, 2000);
addChild(background);
Выполнение инициализации не является критическим параметром, т.к. создание объекта делается не часто. Но все же среднее время создания:7,42ms.
Критическим показателем в нашем случае является время выполнения кадра: 9,1095ms.
Вроде бы и подход не самый страшный, и звезды не создаются каждый раз, но на самом деле это все не верх совершенства. Итак, пойдем по порядку.
bitmapData.lock()
В коде не хватает использования метода lock() и unlock(). Добавляем их. (Подробнее «Работа с пикселами» на сайте Adobe.)В итоге время выполнения стало: 8,9753ms. (Печально, маленький прирост)
Цикл FOR
...for (var i:int = 0; i < stars.length; i++) // Проходимся по всем звездам...
Цикл при каждом проходе обращается к объекту и его свойству. Заодно заменяем цикл for на цикл while:
var i:uint = starsCount;// Итератор звезд
while ( --i > -1)// Проходимся по всем звездам
...
Время выполнения: 7,5379ms. (Удивительно большой прирост)
По возможности избегайте использования оператора квадратной скобки
Например у нас внутри циклов происходят подобные операции:
stars[i].x += (stars[i].x - fieldWidth/2) * 0.01;
...
Заменяем оператор квадратные скобки на ссылку в локальной переменной (имеет смысл, если мы обращаемся к переменной много раз):
var star:Point;// Ссылка на мат. модель звезды
while ( --i > -1)// Проходимся по всем звездам
{
star = stars[i];
star.x += (star.x - halfWidth) * 0.01;
...
Время выполнения: 4.4279ms. (Невероятно большой прирост. Видимо, ещё сказывается большой объем массива)
По возможности используйте класс Vector вместо класса Array
Заменяем Array на Vector.<Point>. «В проигрыватель Flash Player 10 добавлен класс Vector, который обеспечивает более быстрый доступ для чтения и записи, чем класс Array.» (источник)
stars = new Vector.<Point>(starsCount, true);
Время выполнения: 3.0963ms.
Избавляем Flash от ненужных повторяющихся вычислений
Можно заранее просчтитать значения таких выражений как «fieldWidth / 2» на «halfWidth» или «fieldHeight+offset» на «fieldWidthWithOffset» и сохранить их в переменные, тем самым избавив флеш от ~20000 лишних операций деления и сложения(это в нашем примере) за кадр:
public function StarField(width:int, height:int, count:uint)
{
fieldWidthWithOffset = fieldWidth = width;
fieldHeightWithOffset = fieldHeight = height;
fieldWidthWithOffset += offset;
fieldHeightWithOffset += offset;
halfWidth = fieldWidth / 2;
halfHeight = fieldHeight / 2;
...
Время выполнения: 2.9642ms. (ну, большого прироста никто и не обещал)
Другие приемы оптимизации
Далее я опущу описание других методов, которые мало сказались на производительности (но это не значит, что нужно ими пренебрегать!):
- Определение переменных вне цикла.
- Встраивание кода для уменьшения числа вызовов функций в коде.
В итоге время выполнения скрипта было уменьшено с 9,7753ms до 2.9405ms. А это более чем в 3 раза! И время инициализации сократилось в 2 раза — с 7,42ms до 3,26ms.
Файл Tools.Utils.as.
Исходный файл StarFieldBad.as.
Итоговый файл StarField.as
Эти и подобные методы оптимизации следует научится использовать сразу, чтобы в последующем не пришлось проводить оптимизацию кода. Всем творческих успехов!
Пару ссылок по теме:
Оптимизация производительности для платформы Flash Platform
http://www.rozengain.com/blog/2007/05/01/some-actionscript-30-optimizations/
И конечно, Google
- +11
- Regul
Комментарии (38)
Не так сложно и напряжно следовать хотя бы основным правилам, чтобы потом не выискивать и переписывать все:
— не использовать в циклах stars.length (каждый раз высчитывается)
— если к элементу массива обращаются много раз, сохранить его в локальную переменную
— Vector быстрее Array
А потом если нужно дальше, то уже можно изгаляться — умножение быстрее деления, битовые операции быстрее умножения и деления.
Вынести часто вычисляемые значения в переменные (константы)
Часто бывает, что оптимизация под конец хорошо проходит, поскольку профайлер сразу показывает ботлнек и ты его оптимизируешь. Но некоторые массовые вещи типа «если к элементу массива обращаются много раз, сохранить его в локальную переменную» не делать — боттленеков может и не быть. И оптимизировать уже не получится малой кровью.
Время выполнения считалось тоже через getTimer() за 2000 циклов и находилось среднее значение. Т.е. я дополнительно вписывал код в программу. На самом деле код при тестировании выглядел так:
сравните ваш вариант с while с этим:
Идея в том — что бы по другому организовать цикл. Как понял — в примере выше сделано так — что на каждом цикле каждый объект перемещается. (очень внимательно в код не вчитывался — так что мог и ошибиться)
То есть у него
— апейтится его позиция (а насколько мне известно — апдейт позиции даже с целочисленными координатами приводит к перерендеру)
— так же на каждом цикле происходит перерасчет значений.
То есть — на каждом цикле идет куча операций. Но — если переписать код таким образом — что бы перерисовывать точки и, как следствие — рендерить их только тогда, когда реально точка поменялась. Тогда — все будет намного лучше.
Грубо — только в виде примера — оно будет выглядеть так
— Создаем массив точек. У каждой точки есть специальная величина. Время, через которую точка сместиться на 1 пиксель (то есть время, через которое нужно эту точеу апдейтить). назовем ее — ulDeltaT. Чем быстрее точка двигается — тем эта величина будет меньше. (то есть — если точка двигается по одной оси — ulDeltaT = 1 / fMotionSpeed)
Так же у точки есть время, в которое ее нужно будет двигать — ulNextT.
Логика работы такая вот.
— Весь массив сортируем по ulNextT.
Хотя, вообще говоря — точки лучше хранить не в виде массив, а в виде связанного списка — тогда с ними проще будет работать (быстрее будут операции добавления\убирания точки)
— Таким оразом — получаем массив, в котором очень легко определить — какие точки нужно двигать и когда. Простым просмотром массива точек от его «головы».
В цикле игры — инкрементим базовый счетчик. И на каждом игровом цикле — берем те точки, для которых подошло время. И двигаем их.
Точку, после того, как ее заапдейтили — меняем ее ulNextTime — и вставляем на новое место в массиве.
Таких точек будет не много, и потому количество расчетов и рендера — так же будет немного. Приросты по скорости получаются — по моему опыту — очень и очень большие. На порядки больше, чем от любых других способов.
В примере выше — логика очень простая. Но — вообще говоря — такой «событийный» подход к организации цикла — можно использовать и в намного более сложном игровом цикле.
порокаоптимизаторства, по слухам num>>0 быстрее int(num) ;)Провёл тест:
Взял число с 7ю знаками после запятой и округлял.
При числе итераций в 10'000'000 вот средние (тест повторялся 20 раз) значения:
Math.floor — 1066.76 ms
int — 78.80 ms
>>0 — 80.52 ms
Видно, что Math.floor явно проиграл. На счёт остальных двух: результаты варьируются (то один больше другого, то наоборот), но разница на столько не велика, что можно её не учитывать. :)
gorbatov.blogspot.com/2008/03/as3.html
Тут можно поглядеть наглядно все (слева снизу меняются страницы)
gskinner.com/talks/quick/
lab.polygonal.de/2007/05/10/bitwise-gems-fast-integer-math/
www.stephencalenderblog.com/?p=109
www.stephencalenderblog.com/?p=12
www.stephencalenderblog.com/?p=19
www.stephencalenderblog.com/?p=25
PS:
Нашёл там в комментариях:
Ceil(x) == int(x + 0.9999) //Вместо int(x) + 1
И после этого я наконец понял как оптимизировать округление:
round(x) == int(x + 0.50001)
round(x) = int(x + 0.5)
Еще малоизвестная фишка, по слухам: (для целых, а может и не только) x = a + b + c; работает в 3 раза медленнее, чем x = a + b; x += c;
jpauclair.net/2010/03/15/flash-asm/