Ускорение вычислений преобразования

Я программирую 2D-движок OpenGL3. В настоящее время я пытаюсь решить узкое место. Пожалуйста, следуйте следующим выводам AMD Profiler: http://h7.abload.de/img/profilerausa.png

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

Однако при 50 000 спрайтов приложение testapp уже непригодно для использования при 5 кадрах в секунду.

Это показывает, что моим узким местом является функция преобразования, которую я использую. Это соответствующая функция: http://code.google.com/p/nightlight2d/source/browse/NightLightDLL/NLBoundingBox.cpp#130

void NLBoundingBox::applyTransform(NLVertexData* vertices) 
{
    if ( needsTransform() )
    {
            // Apply Matrix
            for ( int i=0; i<6; i++ )
            {
                glm::vec4 transformed = m_rotation * m_translation * glm::vec4(vertices[i].x, vertices[i].y, 0, 1.0f);
                vertices[i].x = transformed.x;
                vertices[i].y = transformed.y;
            }
            m_translation = glm::mat4(1);
            m_rotation    = glm::mat4(1);
            m_needsTransform = false;
    }
}

Я не могу сделать это в шейдере, потому что я объединяю все спрайты сразу. Это означает, что я должен использовать ЦП для вычисления преобразований.

Мой вопрос: как лучше всего решить это узкое место?

Я не использую потоки, поэтому, когда я использую vsync, я также получаю дополнительный удар по производительности, потому что он ожидает завершения экрана. Это говорит мне, что я должен использовать многопоточность.

Другой способ - использовать OpenCL, может быть? Я хочу избежать CUDA, потому что, насколько я знаю, он работает только на картах NVIDIA. Это правильно?

пост скриптум:

Вы можете скачать демо здесь, если хотите:

http://www63.zippyshare.com/v/45025690/file.html

Обратите внимание, что для этого требуется установленная версия VC++2008, так как это отладочная версия для запуска профилировщика.


person Community    schedule 02.08.2011    source источник
comment
Конечно можно использовать шейдер, батчинг этому не помешает.   -  person datenwolf    schedule 02.08.2011
comment
Пожалуйста, уточните это. Я имею в виду, что я не могу вычислить преобразование для каждого спрайта в шейдере, так как я рисую все спрайты за один вызов отрисовки.   -  person    schedule 03.08.2011
comment
Кроме того, проверка блока if needTransform() немного пахнет кодом. Должны ли вы преобразовывать или нет, это вопрос высокого уровня, отличный от вопроса реализации низкого уровня.   -  person Tom Kerr    schedule 03.08.2011
comment
В приведенном выше коде у вас ровно одно общее преобразование, которое применяется ко всем спрайтам. Если преобразование выполняется для спрайта, преобразование можно понимать как дополнительный атрибут вершины, при этом значение преобразования применяется ко всем вершинам спрайта. Или вы используете индекс преобразования (опять же атрибут вершины) в юниформ-буфер. Создание экземпляров делает вещи еще более краткими.   -  person datenwolf    schedule 03.08.2011
comment
Что произойдет, если вы вообще не будете вызывать эту функцию или просто сделаете ее прямую копию? Даже для 200 000 вершин я не вижу, чтобы это было вашим основным узким местом.   -  person Nicol Bolas    schedule 03.08.2011
comment
Если я отрендерю 50 000 спрайтов, не вызывая этого, я получу 50 кадров в секунду, то есть на 45 кадров в секунду больше. ВСИНХ выключен.   -  person    schedule 03.08.2011
comment
Я вижу, вы приняли ответ. Однако я заметил, что вы используете vec4 amd mat4. Поскольку вы работаете в 2D, вам нужен только vec2 (или vec3, если вам нужна базовая z-глубина), а mat3 необходим для 2D-преобразований. См. раздел Зачем двумерным преобразованиям нужны матрицы 3x3?, где объясняется, что я имею в виду. . Однако на самом деле 3x3 может быть не быстрее из-за SSE-оптимизации vec4 и mat4, но, возможно, стоит протестировать, если у вас все еще есть проблемы.   -  person Crog    schedule 04.09.2013


Ответы (4)


Первое, что я бы сделал, это объединить ваше вращение и преобразовать матрицы в одну матрицу, прежде чем вы войдете в цикл for... таким образом вы не вычисляете два матричных умножения и вектор в каждом цикле for; вместо этого вы будете умножать только один вектор и матрицу. Во-вторых, вы можете захотеть развернуть свой цикл, а затем скомпилировать его с более высоким уровнем оптимизации (на g++ я бы использовал как минимум -O2, но я не знаком с MSVC, поэтому вам придется перевести этот уровень оптимизации самостоятельно) . Это позволит избежать любых накладных расходов, которые могут возникнуть в ответвлениях в коде, особенно при очистке кеша. Наконец, если вы еще не изучили это, попробуйте выполнить некоторые оптимизации SSE, поскольку вы имеете дело с векторами.

ОБНОВЛЕНИЕ: я собираюсь добавить последнюю идею, которая будет включать в себя многопоточность... в основном конвейер ваших вершин, когда вы выполняете свою многопоточность. Так, например, предположим, что у вас есть машина с восемью доступными потоками ЦП (т. е. четырехъядерная с гиперпоточностью). Настройте шесть потоков для обработки вершинного конвейера и используйте неблокирующие очереди с одним потребителем/производителем для передачи сообщений между этапами конвейера. Каждый этап преобразует один член вашего массива вершин из шести элементов. Я предполагаю, что существует множество этих массивов вершин с шестью элементами, поэтому установка в потоке, который проходит через конвейер, позволяет очень эффективно обрабатывать поток и избегать использования мьютексов и других блокирующих семафоров и т. д. Для больше информации о быстрой неблокирующей очереди одного производителя/потребителя, см. мой ответ здесь.

ОБНОВЛЕНИЕ 2: у вас есть только двухъядерный процессор... поэтому откажитесь от идеи конвейера, так как он столкнется с узкими местами, поскольку каждый поток будет бороться за ресурсы ЦП.

person Jason    schedule 02.08.2011
comment
Это профиль из оптимизированного бинарника: h3.abload.de/img/opt_profile1d84.png Это действительно не сносит. Кроме того, я переместил мульт двух матриц из цикла (как я мог это проконтролировать -.-), но все равно это не очень помогает. с 50k спрайтов, все движущиеся и вращающиеся в каждом кадре, я выиграл 1 кадр и пару мс. Ничего большого. Я также отключил RTTI. - person ; 02.08.2011
comment
Жаль слышать, что вы получили только один кадр в секунду ... на какой машине вы это используете? Кроме того, достаточно ли статична ваша матрица вращения и преобразования, чтобы вы могли фактически кэшировать значение умножения двух матриц (т. Е. Вы умножаете их только один раз в своем фактическом экземпляре класса NboundingBox)? Можете ли вы кэшировать любые другие значения, такие как сами преобразования? - person Jason; 03.08.2011
comment
AMD5200 DualCore с оперативной памятью ATI890HD 1 ГБ. Нет, я не могу кэшировать его дальше. m_rotation и m_translation — это матрицы для вращения и движения скважины, и в этом тестовом примере каждый спрайт перемещается и вращается для его стресс-тестирования. И у каждого спрайта есть своя трансформация и позиция. Когда они вообще не двигаются и не вращаются, у меня около 50 кадров в секунду без вертикальной синхронизации. tiny.cc/dt5f9 - person ; 03.08.2011
comment
Какая видеокарта ATI890HD? ... Кстати, только с двухъядерным процессором вы могли бы попробовать конвейер, но были бы некоторые разногласия, т. Е. У вас было бы много киосков. - person Jason; 03.08.2011
comment
HD4890, моя 4 уже плохо работает ›_›. Нужна новая клавиатура. - person ; 03.08.2011
comment
Так что да, поскольку кажется, что маршрут оптимизации ЦП работает не слишком хорошо, я предполагаю, что единственным вариантом для некоторого ускорения порядка величины будет использование OpenCL. Это ядро ​​довольно маленькое и не должно быть проблемой для переноса. К сожалению, мое понимание OpenCL немного ограничено, поэтому я не могу сказать вам, какой тип конкуренции вы можете увидеть на уровне драйвера между OpenGL и OpenCL. Простое преобразование 50 000 объектов, хотя я думаю, что это должно быть довольно просто для графического процессора. - person Jason; 03.08.2011
comment
Кстати, я читал комментарий datenwolf ... OpenCL может быть слишком тяжелым молотом для этого ... Я не эксперт по шейдерам, но его комментарии кажутся привлекательным решением. - person Jason; 03.08.2011

Я не могу сделать это в шейдере, потому что я объединяю все спрайты сразу. Это означает, что я должен использовать ЦП для вычисления преобразований.

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

Что вам нужно сделать, так это не иметь меньше партий. Вы должны иметь правильное количество партий. Вы понимаете, что зашли слишком далеко с пакетной обработкой, когда отказываетесь от преобразования вершин графического процессора в пользу преобразования ЦП.

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

Кроме того, GLM — прекрасная математическая библиотека, но она не рассчитана на максимальную производительность. Как правило, это не то, что я бы использовал, если бы мне нужно было преобразовывать 300 000 вершин на процессоре каждый кадр.

person Nicol Bolas    schedule 02.08.2011

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

Редактировать:

Это больше похоже на то, о чем я думал.

// Apply Matrix
glm::vec4 transformed;
glm::mat4 translation = m_rotation * m_translation;
for ( int i=0; i<6; i++ )
{
    transformed.x = vertices[i].x;
    transformed.y = vertices[i].y;
    transformed.z = vertices[i].z;
    transformed.w = 1.f; // ?
    /* I can't find docs, but assume they have an in-place multiply
    transformed.mult(translation);
    // */
    vertices[i].x = transformed.x;
    vertices[i].y = transformed.y;
}

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

Вы можете попытаться продублировать какой-нибудь стек и сделать больше меньших циклов.

glm::vec4 transformed[6];
for (size_t i = 0; i < 6; i++) {
    transformed[i].x = vertices[i].x;
    transformed[i].y = vertices[i].y;
    transformed[i].z = vertices[i].z;
    transformed.w = 1.f; // ?
}
glm::mat4 translation = m_rotation * m_translation;
for (size_t i = 0; i < 6; i++) {
    /* I can't find docs, but assume they have an in-place multiply
    transformed.mult(translation);
    // */
}
for (size_t i = 0; i < 6; i++) {
    vertices[i].x = transformed[i].x;
    vertices[i].y = transformed[i].y;
}

Как упомянул Джейсон, раскручивание этих циклов вручную может быть интересным.

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

Я подозреваю, что вызов этой функции меньше важнее, чем ее ускорение. Тот факт, что у вас есть эта проверка needTransform внутри этой функции, заставляет меня думать, что это, вероятно, актуально.

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

Реальность такова, что вы должны просто вызвать этот метод один раз. Вы должны применить Transform, когда хотите применить Transform. Вы не должны вызывать applyTransform, когда вам может понадобиться applyTransform. Интерфейсы должны быть контрактом, относитесь к ним как к таковым.

person Tom Kerr    schedule 02.08.2011
comment
Я переместил вычисления 2 матриц из цикла, m_rotation * m_translation, но все равно выиграл только 1 кадр. - person ; 02.08.2011
comment
Я не могу найти документы, но предполагаю, что у них есть умножение на месте. Умножение вектора/матрицы на месте должно было бы создать временное хранилище для сохранения результата, а затем скопировать его обратно в оригинал. Или он скопировал бы оригинал и умножил бы на оригинал. В любом случае вы ничего не выиграете по сравнению с использованием оператора *. Copy-elision должен уберечь вас от любого лишнего копирования возвращаемого временного файла. Оба метода будут иметь временное значение и будут выполнять копирование, поэтому оба метода фактически эквивалентны. - person Nicol Bolas; 03.08.2011
comment
@Nicol Честно говоря, я ожидал, что это так. С этим сложно что-то сделать, но попробуйте угадать, что можно попробовать, поскольку у нас есть только фрагмент. :) - person Tom Kerr; 03.08.2011

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

Прямо сейчас вы используете матрицы 4x4 в 2D-среде, где одной матрицы 2x2 для вращения и простого вектора для перемещения должно быть достаточно. Это 4 умножения и 4 сложения для поворота, а также два сложения для перевода.

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

По сравнению с операциями, выполняемыми этими матрицами 4x4 прямо сейчас, это намного меньше.

person TravisG    schedule 03.08.2011
comment
+1 ... это хорошая идея, хотя вместо матриц 2x2 и вектора преобразования, вероятно, было бы лучше использовать матрицу 3x3 с однородными координатами для сохранения линейности поворотов и переводов. Другими словами, если вы используете матрицу поворота 2x2 и вектор переноса, вам придется использовать очень специфический порядок для обратных преобразований, и вы потеряете возможность конкатенации поворотов и переводов. Однородные матрицы 3x3 будут поддерживать линейность как перемещения, так и вращения, и вы можете объединить любую серию преобразований в одну матрицу. - person Jason; 04.08.2011