Как я могу оптимизировать этот метод?

Я работал над созданием класса активов, который может генерировать динамические объекты TextureAtlas всякий раз, когда они мне нужны. Конкретный метод - Assets.generateTextureAtlas(), и я пытаюсь максимально оптимизировать его, поскольку мне довольно часто приходится регенерировать атлас текстур, и я надеялся получить лучшее время, чем мои средние 53 мс.

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

Весь код класса доступен здесь в кратком виде на github.

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

Для справки, вот метод:

public static function generateTextureAtlas(folder:String):void
{
    if (!_initialised) throw new Error("Assets class not initialised.");

    if (_renderTextureAtlases[folder] != null)
    {
        (_renderTextureAtlases[folder] as TextureAtlas).dispose();
    }

    var i:int;
    var image:Image = new Image(_blankTexture);
    var itemName:String;
    var itemNames:Vector.<String> = Assets.getNames(folder + "/");
    var itemsTexture:RenderTexture;
    var itemTexture:Texture;
    var itemTextures:Vector.<Texture> = Assets.getTextures(folder + "/");
    var noOfRectangles:int;
    var rect:Rectangle;
    var rectanglePacker:RectanglePacker = new RectanglePacker();
    var texture:Texture;

    noOfRectangles = itemTextures.length;

    if (noOfRectangles == 0)
    {
        return;
    }

    for (i = 0; i < noOfRectangles; i++)
    {
        rectanglePacker.insertRectangle(Math.round(itemTextures[i].width), Math.round(itemTextures[i].height), i);
    }

    rectanglePacker.packRectangles();

    if (rectanglePacker.rectangleCount != noOfRectangles)
    {
        throw new Error("Only " + rectanglePacker.rectangleCount + " out of " + noOfRectangles + " rectangles packed for folder: " + folder);
    }

    itemsTexture = new RenderTexture(rectanglePacker.width, rectanglePacker.height);

    itemsTexture.drawBundled(function():void
    {
        for (i = 0; i < noOfRectangles; i++)
        {
            itemTexture = itemTextures[rectanglePacker.getRectangleId(i)];
            rect = rectanglePacker.getRectangle(i, rect);

            image.texture = itemTexture;
            image.readjustSize();

            image.x = rect.x + itemTexture.frame.x;
            image.y = rect.y + itemTexture.frame.y;

            itemsTexture.draw(image);
        }
    });

    _renderTextureAtlases[folder] = new TextureAtlas(itemsTexture);

    for (i = 0; i < noOfRectangles; i++)
    {
        itemName = itemNames[rectanglePacker.getRectangleId(i)];
        itemTexture = itemTextures[rectanglePacker.getRectangleId(i)];
        rect = rectanglePacker.getRectangle(i);

        (_renderTextureAtlases[folder] as TextureAtlas).addRegion(itemName, rect, itemTexture.frame);
    }
}

person xLite    schedule 25.01.2013    source источник
comment
Итак, какие части при профилировании требуют времени?   -  person Daniel MesSer    schedule 25.01.2013
comment
Это весь метод, generateTextureAtlas(). Для завершения требуется в среднем 53 мс, и я ищу любые способы ускорить его. Он использует другие методы внутри этого метода, которые можно увидеть в сущности класса Assets на github. Я пытался оптимизировать столько, сколько мог, но я все еще застрял с нездоровым временем выполнения 53 мс.   -  person xLite    schedule 25.01.2013


Ответы (3)


Что ж, чтение проекта и поиск того, что можно оптимизировать, наверняка потребует времени.

Начните с удаления нескольких вызовов rectanglePacker.getRectangle(i) внутри циклов.

Например :

    itemName = itemNames[rectanglePacker.getRectangleId(i)];
    itemTexture = itemTextures[rectanglePacker.getRectangleId(i)];
    rect = rectanglePacker.getRectangle(i);

возможно, могло быть:

    rect = rectanglePacker.getRectangle(i);
    itemName = itemNames[rect];
    itemTexture = itemTextures[rect];

Если getRectangle действительно просто "получает прямоугольник" и ничего не устанавливает.

person loxxy    schedule 26.01.2013
comment
На самом деле векторные индексы имеют Id в конце функции, я думаю, вы не заметили. Также строки 102-109 имеют длину 0-2 мс, поэтому они не требуют особого внимания. Большое спасибо за ответ! - person xLite; 26.01.2013

Я думаю, что более серьезная проблема заключается в том, почему, почему вы ДОЛЖНЫ делать это во время выполнения, в ситуации, когда это не может занять больше времени? Это экспансивная операция, независимо от того, насколько сильно вы ее оптимизируете, вы, вероятно, в конечном итоге получите около 40 мс или около того при выполнении в AS3.

Вот почему такие операции следует выполнять во время компиляции или во время «загрузочных экранов» или других «переходов», когда частота кадров не критична и когда вы можете себе это позволить.

В качестве альтернативы создайте другую систему на С++ или каком-либо другом языке, который действительно может обрабатывать числовые вычисления, которые дают вам конечный результат.

Кроме того, когда дело доходит до проверки производительности, да, вся функция занимает 53 мс, НО, где используются эти миллисекунды? 53 мс ничего не говорит и является всего лишь «профилированием служебных данных», где вы нашли виновника, вам нужно разбить его на более мелкие фрагменты, чтобы собрать достоверную информацию о том, что НА САМОМ ДЕЛЕ требует времени внутри этой функции.

Я имею в виду, что внутри этой функции у вас есть 3 цикла for, несколько вызовов других классов, приведения, удаления, создания. Это не похоже на то, что вы делаете что-то одно, эта функция, вероятно, приводит к ~ 500 строкам кода и миллиарду операций процессора. И вы понятия не имеете, где это используется. Я бы предположил, что именно rectanglePacker.packRectangles(); занимает 60% этого времени, но без профилирования мы с вами не знаем, что оптимизировать, у нас просто недостаточно данных.

Если вам НЕОБХОДИМО сделать это во время выполнения в AS3, я бы порекомендовал сделать это в течение нескольких кадров и равномерно распределить рабочую нагрузку в течение 10 кадров или около того. Вы также можете сделать это с помощью другого потока и рабочих. Но больше всего это похоже на ошибку дизайна, поскольку это, вероятно, можно было бы сделать в другое время. А если нет, то на другом языке, который лучше справляется с такими операциями.

Самый простой способ профилировать это — добавить пару меток времени, похожих на:

var timestamps:Array = [];

Затем нажмите getTimer() в разных местах кода, а затем распечатайте их, когда функция будет выполнена.

person Daniel MesSer    schedule 26.01.2013
comment
Мне нужно отображать элементы в изометрической сетке за 1 вызов отрисовки с использованием QuadBatch. Они позволяют использовать только одну текстуру, поэтому мне нужно убедиться, что все, что мне нужно, находится в одной текстуре. Хотя иногда мне нужно добавить больше предметов, например, новый предмет мебели. Это происходит не в каждом кадре, но любое сокращение времени выполнения было бы здорово. Что касается метода, то в большинстве он играет свою роль в диапазоне от 10 до 30 мс. - person xLite; 26.01.2013
comment
Я надеялся больше на советы по оптимизации с помощью простой визуализации. Например, я поменял Math.round(i) на int(i + .5), что является той оптимизацией, которую я ищу. Вместо того, чтобы требовать фактических результатов профилирования, просто укажите очевидные оптимизации, которые, как вы знаете, ускорят выполнение в подобной ситуации. Полный код класса доступен, если вам нужно увидеть код в других методах. Большое спасибо за ответ! - person xLite; 26.01.2013
comment
Предоставленный код хорош с точки зрения производительности, с ним вряд ли что-то можно сделать. Однако: 1) вы объявляете i:int, но используете его как число int(i + 0.5), это вызывает преобразования. 2) RectanglePacker.getRectangleId(i) вызывается дважды, это можно кэшировать в локальную переменную. Эмпирическое правило: всегда используйте локальные переменные, если вы используете их более 1 раза. 3) У вас есть странная встроенная функция itemsTexture.drawBundled(function():void в зависимости от того, когда и как эта функция вызывается, это может быть очень дорого, поскольку все в этой области действия функции доступно для анонимной функции. - person Daniel MesSer; 26.01.2013
comment
Но, в конце концов, это действительно не имеет значения. Это скорее вопрос другого масштаба, этот код уже оптимизирован и его производительность не сильно улучшится. Вы должны найти другое решение. Поскольку код хорош с точки зрения производительности, в нем есть только очень незначительные проблемы. Вы можете получить пару MS, но вам нужно сократить общее время выполнения до 15 мс, чтобы избежать медлительности на вашей машине. И вы не можете получить это от микрооптимизации этого кода. - person Daniel MesSer; 26.01.2013

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

  1. Зачем вам каждый раз заново генерировать весь атлас (вызывая Assets.getTextures() и создавая новую текстуру рендеринга)? Почему бы вам просто не добавить новые элементы в существующий атлас? Создание нового RenderTexture (и, соответственно, новой текстуры в памяти GPU) — очень затратная операция, так как требует синхронизации между CPU и GPU. С другой стороны, отрисовка в RenderTexture выполняется полностью внутри GPU, поэтому занимает гораздо меньше времени.

  2. Если вы размещаете каждый элемент в сетке, вы можете избежать использования RectanglePacker, поскольку все ваши прямоугольники могут иметь одинаковые размеры, соответствующие размерам сетки.

Изменить:

Уточню, некоторое время назад у меня была аналогичная проблема: мне приходилось регулярно добавлять новые элементы в существующий атлас. И производительность этой операции была вполне приемлемой (около 8 мс на iPad3 при использовании динамической текстуры 1024x1024). Но я использовал тот же объект RenderTexture и тот же объект Sprite, которые содержали элементы моего динамического атласа. Когда мне нужно добавить новый элемент, я просто создаю новый Image с нужной текстурой (отдельно или из другого статического атласа), затем помещаю его в контейнер Sprite, а затем перерисовываю этот контейнер в RenderTexture. Аналогично с удалением/модификацией элемента.

person skozin    schedule 26.01.2013
comment
Возможно, вы что-то придумали с повторным использованием RenderTexture, не знаю, почему это вылетело у меня из головы. Хотя причина, по которой я перерисовываю каждый элемент, заключается в том, что мне нужно убедиться, что я могу вместить как можно больше. Каждый предмет может сильно различаться по своим размерам, поэтому мне нужно убедиться, что он упакован как можно плотнее, чтобы не тратить место. - person xLite; 27.01.2013
comment
На самом деле причина, по которой я создаю новый объект RenderTexture (только после проверки), заключается в том, что в зависимости от того, насколько плотно упакованы элементы, ширина и высота текстуры не обязательно будут одинаковыми после каждого packRectangles() вызов. Это сделано для того, чтобы я не использовал полное 2048x2048 RenderTexture, хотя на самом деле мне может понадобиться только 128x2048 и т. д. и т. д. - person xLite; 27.01.2013
comment
Я понимаю, но повторное использование одной и той же текстуры большего размера может быть более эффективным, чем создание каждый раз новой текстуры меньшего размера. Опять же, если объекты в вашем проекте расположены на сетке (и, следовательно, имеют одинаковые размеры), может быть не так сложно реализовать алгоритм упаковки, гарантирующий результирующие размеры. Например, взгляните на эту статью: эффективный рекурсивный метод разделения для упаковки одинаковых прямоугольников в прямоугольник. - person skozin; 27.01.2013
comment
Каждый элемент может сильно различаться по своим размерам — извините, я пропустил это. Тогда проблема более сложная. В любом случае, я думаю, стоит попробовать повторно использовать текстуру. - person skozin; 27.01.2013