Через тернии к звездам. Оптимизация кода.

Эта статья посвящена оптимизации ActionScript3 на примере создания эффекта «полета сквозь звезды»(на подобии старого скринсейвера Windows). Мне этот эффект необходим был для игры. Эффект должен был служить фоном и поэтому требовалось чтобы он был как можно менее ресурсоемким.
Я не изобретаю никакие новые приемы оптимизации, я лишь приведу пример их применения на практике. Для начала я поискал в интернете готовые решения и наткнулся на решение, которое визуально меня устраивало: http://www.lemlinh.com/flash-source-starfield-generator/. Открыв код мы обнаружим, что эти Звездочки — это MovieClip'ы (крик ужаса за кадром). Каждый флеш-разработчик понимает, что кроме отображения «точки» на каждую «Звездочку-MovieClip» вешается куча свойств и методов класса MovieClip. И чем больше звезд — тем хуже. Я ничего не хочу сказать об авторе — просто нам требуется другое.

Ясно, что в такой ситуации прийдется сделать все самому и с нуля. Ничего гениального в нашей реализации нет:

  1. создать массив точек stars:Array
  2. в каждом кадре:

  • … просчитать новое положение каждой точке по суперсложной математической формуле (умножение).
  • … нарисовать каждую точку на экране.

Сразу оговорюсь, я использую собственный метод получения случайного значения в классе 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

Комментарии (38)

+1
Да, лучше сразу учится писать оптимальный код, потому что дополнительная оптимизация потом потребует не только силы на переработку алгоритмов, но и время на их тестирование :(
+1
Вначале пишешь хоть какой-то рабочий код, потом переписываешь критичные его части.
0
Надо стараться писать оптимальный, а хоть какой то рабочий и так получится (:
+1
Чем «оптимальнее» у тебя код, тем сильнее ты теряешь в скорости разработки. Нужно понимать, что оптимизация требуется только в узких местах.
+1
+1. «Не оптимизируй то, что не тормозит» ©
+3
Не согласен. Если подобные правила как в этом посте брать себе за стандарт (вызубрить и не забывать), то и скорость разработки будет нормальная и код будет «быстрым».
0
Если математическая операция выполняется 2 раза, стоит ли кешировать ее результат? А 10 раз? А 5000 раз? С какого количества выполнения нужно оптимизировать?
0
Два раза каждый кадр или два раза вообще за всю работу приложения? :) Я думаю, что у хорошего программиста таких вопросов не должно возникать, он должен делать все на автомате и не копаться потом в коде выискивая медленные операции ;)
0
Я более чем согласен. На самом деле, когда я писал этот класс, сначала был написал «правильный» оптимизированный класс. Потом я решил написать статью и сделал класс «как быть не должно». Написание оптимизированного кода заняло столько же времени, как и написание обычного.
+1
+1
Не так сложно и напряжно следовать хотя бы основным правилам, чтобы потом не выискивать и переписывать все:
— не использовать в циклах stars.length (каждый раз высчитывается)
— если к элементу массива обращаются много раз, сохранить его в локальную переменную
— Vector быстрее Array

А потом если нужно дальше, то уже можно изгаляться — умножение быстрее деления, битовые операции быстрее умножения и деления.
Вынести часто вычисляемые значения в переменные (константы)
+1
+1, подписываюсь как программист с многолетним стажем и опытом работы вплоть до лид-программера в игровых фирмах.

Часто бывает, что оптимизация под конец хорошо проходит, поскольку профайлер сразу показывает ботлнек и ты его оптимизируешь. Но некоторые массовые вещи типа «если к элементу массива обращаются много раз, сохранить его в локальную переменную» не делать — боттленеков может и не быть. И оптимизировать уже не получится малой кровью.
0
За всю жизнь столкнулся с необходимостью оптимизации кода только 2 раза, для iPhone игры и для j2me игры, и то по причине ограничений платформы. Преждевременная оптимизация — зло, т.к. порождает нечитабельный, плохо поддающийся изменению код. Ума не приложу, как можно написать тормозящую по причине кода на флеш игрушку. Т.е. сложная анимация или слишком сложная физическая сцена — да, но чтобы не оптимизированные операторы на что-то влияли — очень сомнительно.
0
Ну вот а ФлешБокс2Д хорошо бы пройтись и оптимизировать по полной программе! :)
0
Я ж не спорю, box2d в плане чистоты кода и оптимизированности никакой.
0
Я к тому что там как раз хорошо бы и оптимизировать операторы тоже, ибо нагрузка вычислительная большая — от всего будет плюс.
0
Ну, я про преждевременную оптимизацию говорил. В случае с боксом требуется оптимизация уже написанного и отлаженного кода. Ну и все-таки бокс — это библиотечка, а не игра :).
0
А как ты высчитывал время инициализации и время выполнения?
0
Время инициализации проверялось путем создания 100 раз объекта и замера времени выполнения функций в конструкторе c помощью getTimer();
Время выполнения считалось тоже через getTimer() за 2000 циклов и находилось среднее значение. Т.е. я дополнительно вписывал код в программу. На самом деле код при тестировании выглядел так:
private function enterFrame(e:Event):void 
{
        Profiler.instance.beginProfiling();
        if (countTime == iteratorsCount) {
                trace('Avarage execution time : ', Math.round( sumTime / (countTime-100) * 10000 ) / 10000);
        }else {
        
                if (countTime == 100) {
                        sumTime = 0;
                }
                
                Profiler.instance.showVar("Execution : ", int( countTime / iteratorsCount * 100 ) + "%" );
                Profiler.instance.showVar("Avarage execution time : ", Math.round( sumTime / (countTime-100) * 10000 ) / 10000 + "ms" );
                                
                var timerStart:int = getTimer();
                                
                updateStars();
                drawStars();
                                
                sumTime += getTimer() - timerStart;
                countTime++;
        }
        Profiler.instance.endProfiling();
}
0
А понял, спасибо. Я тоже этим воспользуюсь.
0
В дебажном плеере(делаю вывод из того, что используется trace) измерять скорость выполнения — не корректно, в релизном плеере скорости выполнения различных команд может отличаться, как в положительную так и отрицательную сторону.
0
В смысле относительная скорость выполнения))
0
Обкакал мои методы, но не предложил ничего взамен! Лучше просто расскажи как правильно.
0
Просто, тестить в релизном плеере, выводить результаты в текстовое поле
0
Нужно будет попробовать как-нибудь.
0
кстати, если в процессе перебора не нужно изменять массив то цикл for each удобней
сравните ваш вариант с while с этим:
for each (var star:Star in stars) {
    star.doSomething();
    ...
}
  • qdrj
  • qdrj
0
Тут же речь идет об оптимизации, for each в as3 медленный. Да что там говорить, он во всех языках медленный :).
+1
Хорошая статья, спасибо.
0
Просто — как мысль. Есть еще один очень неплохой способ оптимизации кода, который, как мне кажется подходит к этой ситуации.

Идея в том — что бы по другому организовать цикл. Как понял — в примере выше сделано так — что на каждом цикле каждый объект перемещается. (очень внимательно в код не вчитывался — так что мог и ошибиться)

То есть у него

— апейтится его позиция (а насколько мне известно — апдейт позиции даже с целочисленными координатами приводит к перерендеру)
— так же на каждом цикле происходит перерасчет значений.

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

Грубо — только в виде примера — оно будет выглядеть так

— Создаем массив точек. У каждой точки есть специальная величина. Время, через которую точка сместиться на 1 пиксель (то есть время, через которое нужно эту точеу апдейтить). назовем ее — ulDeltaT. Чем быстрее точка двигается — тем эта величина будет меньше. (то есть — если точка двигается по одной оси — ulDeltaT = 1 / fMotionSpeed)

Так же у точки есть время, в которое ее нужно будет двигать — ulNextT.

Логика работы такая вот.

— Весь массив сортируем по ulNextT.

Хотя, вообще говоря — точки лучше хранить не в виде массив, а в виде связанного списка — тогда с ними проще будет работать (быстрее будут операции добавления\убирания точки)

— Таким оразом — получаем массив, в котором очень легко определить — какие точки нужно двигать и когда. Простым просмотром массива точек от его «головы».

В цикле игры — инкрементим базовый счетчик. И на каждом игровом цикле — берем те точки, для которых подошло время. И двигаем их.

Точку, после того, как ее заапдейтили — меняем ее ulNextTime — и вставляем на новое место в массиве.

Таких точек будет не много, и потому количество расчетов и рендера — так же будет немного. Приросты по скорости получаются — по моему опыту — очень и очень большие. На порядки больше, чем от любых других способов.

В примере выше — логика очень простая. Но — вообще говоря — такой «событийный» подход к организации цикла — можно использовать и в намного более сложном игровом цикле.
  • uofk
  • uofk
0
Ещё более оптимизировано будет заменить Math.floor() на Int(), который работает в разы быстрее. :)
0
Если уж совсем бросаться в пучину порока оптимизаторства, по слухам num>>0 быстрее int(num) ;)
+1
Не знал про него вообще, спасибо.

Провёл тест:

Взял число с 7ю знаками после запятой и округлял.
При числе итераций в 10'000'000 вот средние (тест повторялся 20 раз) значения:

Math.floor — 1066.76 ms
int — 78.80 ms
>>0 — 80.52 ms

Видно, что Math.floor явно проиграл. На счёт остальных двух: результаты варьируются (то один больше другого, то наоборот), но разница на столько не велика, что можно её не учитывать. :)
0
Спасибо! Развеял миф!
0
Ну раз вы пошли маньячиться, вот вам (но везде нужен разумный баланс):

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
0
Ого, есть что почитать, спасибо. :)
0
Ого, есть что почитать, спасибо. :)

PS:
Нашёл там в комментариях:
Ceil(x) == int(x + 0.9999) //Вместо int(x) + 1

И после этого я наконец понял как оптимизировать округление:
round(x) == int(x + 0.50001)
0
Есть такое дело
round(x) = int(x + 0.5)
+2
Автор блога по последним ссылкам, похоже, реально маньяк)) Днем ковыряет байткод, а ночью с резиновым фаллосом бегает за старушками в парке, не иначе как)))

Еще малоизвестная фишка, по слухам: (для целых, а может и не только) x = a + b + c; работает в 3 раза медленнее, чем x = a + b; x += c;
0
Точно, тоже для маньяков ;)
jpauclair.net/2010/03/15/flash-asm/
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.