Багато покращень було внесено до Qt 3D з моменту випуску Qt 5.6, нашої попередньої версії довгострокової підтримки (LTS). Інженери з KDAB і The Qt Company завзято працювали, щоб привнести нові функції в Qt 5.9 LTS, багато з яких перераховані в Що нового Qt 3D з Qt 5,9 у посту блогу Шон Хармера з KDAB. Незважаючи на те, що безліч можливостей ще в розробці (наприклад, Vulkan backend), основна увага в останніх випусках змістилася у бік продуктивності та стабільності. Ефективність значно покращилася порівняно з Qt 5.6, особливо для складних сцен та сцен з великою кількістю графів.
Сцени з багатьма вікнами перегляду зазвичай призводять до великої кількості кадрових графів, оскільки кожне вікно перегляду відповідає листовому вузлу. Якщо ви не знайомі з концепцією кадрового графа в Qt 3D і про те, наскільки це потужно, вам слід прочитати повідомлення з блогу Пола Лемарі на kdab.com . Нижче розташовано знімок екрана одного з наших внутрішніх тестів; досить проста (і барвиста) сцена з 28 вікнами перегляду:
Використання ЦП у цьому тесті значно скоротилося в Qt 5.9.2 у порівнянні з Qt 5.6.2, і компанія Qt працює разом з інженерами KDAB над рядом змін, які, як ми очікуємо, зменшать навантаження на ЦП ще більше в Qt 5.11:
Багато покращень продуктивності було перенесено на порт Qt 3D Studio, заснований на Qt 3D. Незважаючи на те, що середовище виконання заплановане на випуск наступного року, ми вже зараз додаємо покращення продуктивності до поточної серії Qt 5.9.x LTS. Ось деякі результати тестів наших внутрішніх прикладів Qt3D Studio:
Поліпшення продуктивності додані у багатьох частинах Qt 3D. Наприклад, ми додали підтримку ефективних форматів файлів, таких як glTF2. У цьому пості ми докладно розглянемо деякі зміни, які ми робимо зменшення використання ЦП, а пізнішому повідомленні ми обговоримо скорочення споживання пам'яті.
Поліпшення вирішувача залежностей завдань
Одне з покращень продуктивності, яке ми зробили – це вирішувач залежностей завдань Qt 3D. Qt 3D ділить роботу, яка має виконуватися кожен кадр на окремі, дрібніші завдання, які можуть виконуватися паралельно. Завдання є частиною гнучкої архітектури backend/frontend Qt 3D, яка відокремлює інтерфейс в основному потоці від бекенда, який складається з аспектів, що виконують обробку рендерингу, введення та анімацію (докладніше про це у документації Qt 3D Overview ).
Бекенд запускає завдання з різних аспектів пулу потоків, і кожне завдання може визначати залежність від інших завдань, які мають виконуватися перед ним. Ці залежності необхідно вирішувати ефективно, тому що завдання часто змінюються від одного кадру до іншого. Хоча це просто, коли кількість завдань невелика, це стає більш трудомістким для складних сцен з великими кадрами.
Профілюючи наші приклади за допомогою Callgrind , ми виявили вузькі місця продуктивності у певних частинах вирішувача залежностей завдань. Зокрема, великий QVector всіх залежностей буде змінюватися щоразу, коли завдання буде завершено, і відповідні залежності можна видалити зі списку. Це різко понизило продуктивність.
Ми розпочали роботу над рішенням, в якому ми повністю позбавимося QVector і зберігатимемо два списки пов'язаних із завданням: один список складається з того, від чого завдання залежить, і інший з того, що від цього завдання залежить.
class AspectTaskRunnable { // ... other definitions QVector m_dependencies; QVector m_dependers; };
За допомогою цього рішення, коли завдання завершиться, воно може пройти через список m_dependers і видалити себе зі списку m_dependencies в кожному з m_dependers. Якщо список m_dependers порожній, це завдання можна запустити. Однак тепер у нас стало багато маленьких QVectors, які змінюються весь час. Хоча це краще, ніж зміна розміру одного великого QVector, це ще не оптимально.
Нарешті, ми зрозуміли, що оскільки залежності не можуть змінюватися під час виконання завдання, немає необхідності відстежувати, що залежить від завдання і від чого це завдання. Кожному завданню достатньо знати, які завдання залежать від нього і від якої кількості завдань залежить воно саме.
class AspectTaskRunnable { // ... other definitions int m_dependencyCount = 0; QVector<AspectTaskRunnable*> m_dependers; };
Щоразу, коли завдання завершується, ми переглядаємо список завдань залежно від нього та віднімаємо в них кількість залежностей на одиницю. Останній код виглядає приблизно так (безсоромно спрощений для зручності читання):
void QThreadPooler::taskFinished(AspectTaskRunnable *job) { const auto &dependers = job->m_dependers; for (auto &depender : dependers) { depender->m_dependencyCount--; if (depender->m_dependencyCount == 0) { m_threadPool.start(depender); } } }
Впроваджуючи цю зміну, вирішувач залежностей завдань став незначним внеском у використанні ЦП, і ми змогли зосередитись на інших вузьких місцях.
Покращення продуктивності QThreadPool
Інші частини Qt також мають можливості оптимізації, які можна знайти в наших тестах. Наприклад, Qt 3D використовує QThreadPool від Qt Core для автоматичного керування завданнями та розподілу їх для різних потоків. Однак, як і в попередньому випадку, QThreadPool використовувався для зберігання завдань у QVector, який змінював свій розмір при кожному завершенні завдання. Це не велика проблема, коли йдеться про невелику кількість завдань, але це раптово стало вузьким місцем для складних 3D сцен Qt з великою кількістю завдань.
Ми вирішили змінити реалізацію QThreadPool, щоб використовувати більші «сторінки черги» та помістити вказівники на ці сторінки в QVector. На кожній сторінці ми відстежуємо індекс першого завдання у черзі та індекс останнього завдання у черзі:
class QueuePage { enum { MaxPageSize = 256; }; // ... helper functions, etc. int m_firstIndex = 0; int m_lastIndex = -1; QRunnable *m_entries[MaxPageSize]; };
Тепер все, що нам потрібно зробити, - це збільшити перший індекс щоразу, коли завдання завершується, і збільшити останній індекс при додаванні завдання. Якщо немає місця на сторінці, ми виділяємо нову. Це проста та низькорівнева реалізація, але це ефективно.
Кешування результатів конкретних завдань
Потім ми виявили, що певні завдання виділяються дуже вимогливі до процесора. Деякі з цих завдань, такі як QMaterialParameterGathererJob, виконували багато роботи в кожному кадрі, навіть якщо результати попередніх кадрів були однаковими. Це була ясна можливість для кешування результатів підвищення продуктивності. По-перше, давайте подивимося, що робить QMaterialParameterGathererJob.
У Qt 3D ви можете перевизначити значення кожного параметра, визначеного QRenderPass, встановивши його на QTechnique, QEffect або QMaterial, який використовує цей прохід рендерингу. Кожен параметр, своєю чергою, використовується визначення однорідного значення у фінальній програмі шейдерів. Цей код показує приклад QML, де параметр "колір" встановлений на всіх рівнях:
Material { parameters: [ Parameter { name: "color"; value: "red"} ] effect: Effect { parameters: [ Parameter { name: "color"; value: "blue"} ] techniques: Technique { // ... graphics API filter, filter keys, etc. parameters: [ Parameter { name: "color"; value: "green"} ] renderPasses: RenderPass { parameters: [ Parameter { name: "color"; value: "purple"} ] shaderProgram: ShaderProgram { // vertex shader code, etc. fragmentShaderCode: " #version 130 uniform vec4 color; out vec4 fragColor; void main() { fragColor = color; } " } } } } }
Щоб з'ясувати кінцеве значення параметра, що використовується в програмі шейдерів, QMaterialParameterGathererJob переглядає всі матеріали в сцені та знаходить відповідні ефекти, методи та проходи рендерингу. Потім, визначаючи пріоритети параметрів, заданих QMaterial, QEffect, QTechnique і QRenderPass, ми визначаємо остаточне значення параметра.В цьому випадку значення «червоне», оскільки параметри QMaterial мають найвищий пріоритет.
Збір всіх параметрів досить трудомісткий у великих сценах з багатьма матеріалами і виявився вузьким місцем для деяких прикладів Qt 3D Studio. Тому ми вирішили кешувати параметри, знайдені QMaterialParameterGathererJob, але швидко зрозуміли, що кеш завжди буде недійсним, якщо значення змінюються кожен кадр. Це звичайний випадок, якщо параметри анімовані. Натомість ми вирішили кешувати покажчики на об'єкти QParameter, а не їх значення. Значення потім зберігаються поза кешем і виймаються лише за необхідності. Кешування результатів призвело до величезного збільшення продуктивності в сценах з багатьма параметрами, оскільки нам потрібно було виконувати цю роботу лише за великих змін сцени, наприклад, додавання матеріалів.
Ми працювали з багатьма подібними випадками, де ми брали кілька наших великих прикладів, профілювали їх, виявляли вузькі місця у конкретних завданнях, і працювали, щоб знайти способи покращення продуктивності або кешування результатів. На щастя, система на основі завдань Qt 3D спрощує оптимізацію або кешування певних завдань незалежно, тому ви можете очікувати, що в майбутні випуски Qt 3D з'являться додаткові покращення.
Стаття написана: Svenn-Arne Dragly | Четвер, Листопад 16, 2017р.