Ускорение преобразований - PullRequest
7 голосов
/ 02 августа 2011

Я программирую OpenGL3 2D Engine. В настоящее время я пытаюсь устранить узкое место. Пожалуйста, отсюда следующий вывод 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. Это правильно?

post scriptum:

Вы можете скачать демо здесь, если вам нравится:

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

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

Ответы [ 4 ]

4 голосов
/ 02 августа 2011

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

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

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

2 голосов
/ 03 августа 2011

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

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

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

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

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

1 голос
/ 03 августа 2011

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

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

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

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

1 голос
/ 02 августа 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;
}

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

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

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

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

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

...