Соображения и рекомендации по производительности
Соображения по временным параметрам
В качестве разработчика приложения вы должны стремиться к тому, чтобы движок рендеринга обеспечивал постоянную частоту обновления кадров в 60 кадров в секунду. 60 кадров в секунду означают, что между каждым кадром имеется примерно 16 миллисекунд для обработки, включая обработку, необходимую для загрузки примитивов отрисовки в графическое оборудование.
На практике это означает, что разработчик приложения должен:
- использовать асинхронное программирование с обработкой событий, где это возможно
- использовать потоки-рабочие для выполнения значительной обработки
- никогда не запускать цикл событий вручную
- никогда не тратить более нескольких миллисекунд на кадр в блокирующих функциях
Невыполнение этих требований приведет к пропуску кадров, что окажет существенное влияние на пользовательский опыт.
Примечание: Иногда возникает желание использовать шаблон, но его никогда не следует применять, — создание собственного цикла событий QEventLoop или вызов QCoreApplication::processEvents() для предотвращения блокировки в блоке кода C++, вызванном из QML. Это опасно, потому что при входе в цикл событий в обработчике сигналов или привязке движок QML продолжает выполнять другие привязки, анимации, переходы и т. д. Эти привязки могут затем вызвать побочные эффекты, которые, например, уничтожают иерархию, содержащую цикл событий.
Профилирование
Самый важный совет: используйте встроенный в Qt Creator профайлер QML. Знание того, где тратится время в приложении, позволит сконцентрироваться на реальных проблемах, а не на проблемах, которые могут потенциально возникнуть. Для получения дополнительной информации о том, как использовать инструмент профилирования QML, обратитесь к руководству Qt Creator.
Определение наиболее часто выполняемых привязок или функций, в которых приложение тратит больше всего времени, позволит вам решить, нужно ли оптимизировать проблемные области или перепроектировать некоторые детали реализации вашего приложения для повышения производительности. Попытка оптимизировать код без профилирования, скорее всего, приведет к незначительному, а не существенному улучшению производительности.
Код JavaScript
Большинство приложений QML содержат большое количество кода JavaScript в виде динамических функций, обработчиков сигналов и выражений привязки свойств. Как правило, это не проблема. Благодаря некоторым оптимизациям в движке QML, таким как оптимизации компилятора привязок, это (в некоторых случаях) может быть быстрее, чем вызов функции C++.
Преобразование типов
Главная стоимость использования JavaScript заключается в том, что в большинстве случаев при доступе к свойству из типа QML создаётся объект JavaScript с внешним ресурсом, содержащим основанные данные C++ (или ссылку на них). В большинстве случаев это довольно недорого, но в других случаях это может быть довольно дорогостоящим. Одним из примеров, где это дорого, является присвоение QVariantMap C++ Q_PROPERTY свойству QML «variant». Списки также могут быть дорогими, хотя последовательности определенных типов (QList целых чисел, qreal, bool, QString и QUrl) должны быть недорогими; другие типы списков связаны с высокой стоимостью преобразования (создание нового массива JavaScript и добавление новых типов по одному, с преобразованием типа экземпляра C++ в значение JavaScript).
Преобразование между некоторыми основными типами свойств (такими как свойства «строка» и «url») также может быть дорогим. Использование наиболее подходящего типа свойства позволит избежать ненужных преобразований.
Если вам необходимо экспонировать QVariantMap в QML, используйте свойство «var» вместо свойства «variant». В общем случае «property var» следует считать предпочтительнее «property variant» для всех случаев применения начиная с QtQuick 2.0 и более поздних версий (обратите внимание, что «property variant» помечен как устаревший), поскольку он позволяет хранить истинную ссылку JavaScript (что может уменьшить количество преобразований, необходимых в определённых выражениях).
Разрешение свойств
Разрешение свойств занимает время. Хотя в некоторых случаях результат поиска можно кэшировать и повторно использовать, всегда лучше избегать ненужной работы, если это возможно.
В следующем примере у нас есть блок кода, который часто выполняется (в данном случае это содержимое явного цикла, но это может быть, например, часто оцениваемое выражение привязки), и в нём мы многократно разрешаем объект с идентификатором «rect» и его свойство «color»:
// bad.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which, value) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
printValue("red", rect.color.r);
printValue("green", rect.color.g);
printValue("blue", rect.color.b);
printValue("alpha", rect.color.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
} Вместо этого мы можем разрешить общий базовый элемент только один раз в блоке:
// good.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which, value) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
var rectColor = rect.color; // resolve the common base.
printValue("red", rectColor.r);
printValue("green", rectColor.g);
printValue("blue", rectColor.b);
printValue("alpha", rectColor.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
} Даже это простое изменение приводит к значительному улучшению производительности. Обратите внимание, что код выше можно улучшить ещё больше (поскольку свойство, которое ищется, никогда не изменяется во время обработки цикла), вынеся разрешение свойства за пределы цикла, как показано ниже:
// better.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which, value) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
var rectColor = rect.color; // resolve the common base outside the tight loop.
for (var i = 0; i < 1000; ++i) {
printValue("red", rectColor.r);
printValue("green", rectColor.g);
printValue("blue", rectColor.b);
printValue("alpha", rectColor.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
} Привязки свойств
Выражение привязки свойства будет переоцениваться, если какие-либо из ссылочных свойств будут изменены. Поэтому выражения привязок следует делать максимально простыми.
Если у вас есть цикл, где вы выполняете некоторую обработку, но важен только конечный результат обработки, часто лучше обновлять временный накопитель, который вы затем присваиваете нужному свойству для обновления, а не обновлять само свойство поэтапно, чтобы избежать повторной оценки выражений привязки на промежуточных этапах накопления.
Следующий условный пример поясняет эту мысль:
// bad.qml
import QtQuick
Item {
id: root
width: 200
height: 200
property int accumulatedValue: 0
Text {
anchors.fill: parent
text: root.accumulatedValue.toString()
onTextChanged: console.log("text binding re-evaluated")
}
Component.onCompleted: {
var someData = [ 1, 2, 3, 4, 5, 20 ];
for (var i = 0; i < someData.length; ++i) {
accumulatedValue = accumulatedValue + someData[i];
}
}
} Цикл в обработчике onCompleted приводит к переоценке привязки свойства «текст» шесть раз (что затем приводит к переоценке любых других привязок свойств, которые зависят от значения текста, а также обработчика сигнала onTextChanged каждый раз, и выводит текст на отображение каждый раз). В этом случае это явно не нужно, поскольку нас действительно интересует только конечное значение накопления.
Его можно переписать следующим образом:
// good.qml
import QtQuick
Item {
id: root
width: 200
height: 200
property int accumulatedValue: 0
Text {
anchors.fill: parent
text: root.accumulatedValue.toString()
onTextChanged: console.log("text binding re-evaluated")
}
Component.onCompleted: {
var someData = [ 1, 2, 3, 4, 5, 20 ];
var temp = accumulatedValue;
for (var i = 0; i < someData.length; ++i) {
temp = temp + someData[i];
}
accumulatedValue = temp;
}
} Рекомендации по последовательностям
Как упоминалось ранее, некоторые типы последовательностей быстры (например, QList<int>, QList<qreal>, QList<bool>, QList<QString>, QStringList и QList<QUrl>), в то время как другие значительно медленнее. Помимо использования этих типов везде, где это возможно, вместо медленных типов, есть некоторые другие семантики, связанные с производительностью, о которых вам нужно знать, чтобы достичь наилучшей производительности.
Во-первых, существуют две различные реализации для типов последовательностей: одна для случаев, когда последовательность является свойством Q_PROPERTY объекта QObject (назовём эту последовательность ссылкой), и другая для случаев, когда последовательность возвращается из функции Q_INVOKABLE объекта QObject (назовём эту последовательность копией).
Последовательность ссылок читается и записывается через QMetaObject::property() и, таким образом, читается и записывается как QVariant. Это означает, что изменение значения любого элемента последовательности из JavaScript приведёт к выполнению трёх шагов: полная последовательность будет считана из объекта QObject (как QVariant, а затем преобразована в последовательность правильного типа); элемент в указанном индексе будет изменён в этой последовательности; и полная последовательность будет записана обратно в объект QObject (как QVariant).
Последовательность копий намного проще, поскольку фактическая последовательность хранится в данных ресурса объекта JavaScript, поэтому цикл чтения/модификации/записи не происходит (вместо этого данные ресурса изменяются напрямую).
Поэтому запись в элементы последовательности ссылок будет намного медленнее, чем запись в элементы последовательности копий. На самом деле, запись в один элемент последовательности ссылок из N элементов эквивалентна по стоимости присвоению последовательности копий из N элементов этой последовательности ссылок, поэтому вы обычно можете лучше модифицировать временную последовательность копий, а затем присвоить результат последовательности ссылок во время вычислений.
Предположим существование (и предварительную регистрацию в пространстве имен «Qt.example») следующего типа C++:
class SequenceTypeExample : public QQuickItem
{
Q_OBJECT
Q_PROPERTY (QList<qreal> qrealListProperty READ qrealListProperty WRITE setQrealListProperty NOTIFY qrealListPropertyChanged)
public:
SequenceTypeExample() : QQuickItem() { m_list << 1.1 << 2.2 << 3.3; }
~SequenceTypeExample() {}
QList<qreal> qrealListProperty() const { return m_list; }
void setQrealListProperty(const QList<qreal> &list) { m_list = list; emit qrealListPropertyChanged(); }
signals:
void qrealListPropertyChanged();
private:
QList<qreal> m_list;
}; Следующий пример записывает элементы последовательности ссылок в плотный цикл, что приводит к плохой производительности:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
qrealListProperty.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
qrealListProperty[j] = j;
}
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
} Чтение и запись свойства объекта QObject во внутреннем цикле, вызванное выражением "qrealListProperty[j] = j", делает этот код очень неэффективным. Вместо этого функционально эквивалентный, но намного более быстрый код будет:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
var someData = [1.1, 2.2, 3.3]
someData.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
someData[j] = j;
}
qrealListProperty = someData;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
} Во-вторых, если изменяется любой элемент в свойстве последовательности, генерируется сигнал изменения свойства. Если у вас много привязок к конкретному элементу в свойстве последовательности, лучше создать динамическое свойство, связанное с этим элементом, и использовать это динамическое свойство в выражениях привязки вместо элемента последовательности, поскольку это приведёт к переоценке привязок только при изменении его значения.
Это необычный случай, с которым большинство клиентов не столкнутся, но стоит об этом знать, если вы обнаружите, что делаете что-то подобное:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
property int firstBinding: qrealListProperty[1] + 10;
property int secondBinding: qrealListProperty[1] + 20;
property int thirdBinding: qrealListProperty[1] + 30;
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
qrealListProperty[2] = i;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
} Обратите внимание, что даже если в цикле изменяется только элемент с индексом 2, все три привязки будут переоценены, так как гранулярность сигнала изменения заключается в том, что изменилось всё свойство. Таким образом, добавление промежуточной привязки иногда может быть полезным:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
property int intermediateBinding: qrealListProperty[1]
property int firstBinding: intermediateBinding + 10;
property int secondBinding: intermediateBinding + 20;
property int thirdBinding: intermediateBinding + 30;
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
qrealListProperty[2] = i;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
} В приведённом примере будет переоцениваться только промежуточная привязка, что приводит к значительному улучшению производительности.
Рекомендации по типам значений
Свойства типа значения (шрифт, цвет, вектор3d и т. д.) имеют аналогичные семантики свойств и уведомлений об изменениях объекта QObject с типами последовательностей. Поэтому рекомендации, приведенные выше для последовательностей, также применимы и к свойствам типа значения. Хотя обычно они менее проблемны при использовании типов значений (поскольку количество подсвойств типа значения обычно намного меньше, чем количество элементов в последовательности), любое увеличение числа привязок, которые переоцениваются излишне, будет негативно влиять на производительность.
Общие рекомендации по производительности
Общие соображения по производительности JavaScript, обусловленные языковым дизайном, также применимы к QML. Наиболее заметным является:
- Избегайте использования eval(), если это возможно
- Не удаляйте свойства объектов
Общие элементы интерфейса
Элементы текста
Вычисление макетов текста может быть медленной операцией. Вместо StyledText рассмотрите использование формата PlainText, где это возможно, так как это уменьшает объём работы, требуемой от движка макетов. Если вы не можете использовать PlainText (вам необходимо вставить изображения или использовать теги для указания диапазонов символов, которые должны иметь определённый формат (жирный, курсив и т. д.) вместо всего текста), то вы должны использовать StyledText.
Следует использовать только AutoText , если текст может быть (но, вероятно, не является) StyledText, так как этот режим повлечет за собой затраты на обработку. Режим RichText не следует использовать, поскольку StyledText предоставляет почти все его функции за долю стоимости.
Изображения
Изображения являются неотъемлемой частью любого пользовательского интерфейса. К сожалению, они также являются источником множества проблем из-за времени, необходимого для их загрузки, объема занимаемой памяти и способа их использования.
Асинхронная загрузка
Изображения часто бывают довольно большими, поэтому разумно убедиться, что загрузка изображения не блокирует поток пользовательского интерфейса. Установите свойство «асинхронный» элемента QML Image в true, чтобы включить асинхронную загрузку изображений из локальной файловой системы (удаленные изображения всегда загружаются асинхронно), если это не окажет негативного влияния на эстетику пользовательского интерфейса.
Элементы изображения со свойством «асинхронный», установленным в true, будут загружать изображения в потоке низкого приоритета.
Явное задание размера источника
Если ваше приложение загружает большое изображение, но отображает его в элементе малого размера, установите свойство «sourceSize» в размер отображаемого элемента, чтобы убедиться, что в памяти хранится уменьшенная версия изображения, а не большая.
Обратите внимание, что изменение sourceSize приведет к повторной загрузке изображения.
Избегайте композиции во время выполнения
Также помните, что вы можете избежать выполнения операций композиции во время выполнения, предоставив готовый составной ресурс изображения вместе с приложением (например, предоставив элементы с эффектами тени).
Избегайте сглаживания изображений
Включите image.smooth только в том случае, если это необходимо. Оно медленнее на некоторых аппаратных платформах, и оно не имеет визуального эффекта, если изображение отображается в своем естественном размере.
Отрисовка
Избегайте многократной отрисовки одной и той же области. Используйте Item в качестве корневого элемента вместо Rectangle, чтобы избежать многократной отрисовки фона.
Позиционирование элементов с помощью якорей
Для позиционирования элементов относительно друг друга эффективнее использовать якоря, а не привязки. Рассмотрим этот пример использования привязок для позиционирования rect2 относительно rect1:
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
x: rect1.x
y: rect1.y + rect1.height
width: rect1.width - 20
height: 200
} Это достигается более эффективно с помощью якорей:
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
height: 200
anchors.left: rect1.left
anchors.top: rect1.bottom
anchors.right: rect1.right
anchors.rightMargin: 20
} Позиционирование с помощью привязок (назначение выражений привязки к свойствам x, y, width и height визуальных объектов вместо использования якорей) относительно медленное, хотя оно обеспечивает максимальную гибкость.
Если макет не является динамическим, наиболее эффективным способом задания макета является статическая инициализация свойств x, y, width и height. Координаты элемента всегда относительны к его родительскому элементу, поэтому если вы хотите иметь фиксированный смещение от координат 0,0 родительского элемента, не следует использовать якоря. В приведенном ниже примере дочерние элементы Rectangle находятся в одном месте, но представленный код с якорями не так эффективен по ресурсам, как код, который использует фиксированное позиционирование посредством статической инициализации:
Rectangle {
width: 60
height: 60
Rectangle {
id: fixedPositioning
x: 20
y: 20
width: 20
height: 20
}
Rectangle {
id: anchorPositioning
anchors.fill: parent
anchors.margins: 20
}
} Модели и представления
Большинство приложений имеют как минимум одну модель, которая предоставляет данные представлению. Существуют некоторые семантики, о которых должны знать разработчики приложений, чтобы достичь максимальной производительности.
Пользовательские модели C++
Часто желательно создать собственную пользовательскую модель на C++ для использования с представлением в QML. Хотя оптимальная реализация любой такой модели в значительной степени зависит от конкретной задачи, которую она должна выполнять, существуют некоторые общие рекомендации:
- Делайте все возможное асинхронным
- Выполняйте все обработки в потоке (низкого приоритета) рабочего процесса
- Объединяйте операции backend, чтобы свести к минимуму (потенциально медленные) операции ввода-вывода и межпроцессного взаимодействия
Важно отметить, что использование потока низкого приоритета рекомендуется, чтобы свести к минимуму риск истощения потока GUI (что может привести к ухудшению воспринимаемой производительности). Также помните, что механизмы синхронизации и блокировки могут значительно снизить производительность, поэтому необходимо избегать ненужных блокировок.
Тип QML ListModel
QML предоставляет тип ListModel, который можно использовать для предоставления данных представлению ListView. Он должен быть достаточным для большинства случаев использования и обладать относительно высокой производительностью, если используется правильно.
Заполнение в потоке рабочего процесса
Элементы ListModel могут заполняться в потоке (низкого приоритета) рабочего процесса на JavaScript. Разработчик должен явно вызвать «sync()» на ListModel изнутри WorkerScript, чтобы изменения были синхронизированы с основным потоком. См. документацию WorkerScript для получения дополнительной информации.
Обратите внимание, что использование элемента WorkerScript приведет к созданию отдельного движка JavaScript (поскольку движок JavaScript является потокоспецифичным). Это приведет к увеличению использования памяти. Однако несколько элементов WorkerScript будут использовать один и тот же поток рабочего процесса, поэтому влияние на использование памяти второго или третьего элемента WorkerScript будет незначительным, если в приложении уже используется один.
Не используйте динамические роли
Элемент ListModel в QtQuick 2 намного производительнее, чем в QtQuick 1. Улучшения производительности в основном связаны с предположениями о типе ролей в каждом элементе данной модели — если тип не меняется, производительность кеширования значительно улучшается. Если тип может динамически изменяться от элемента к элементу, это оптимизация становится невозможной, и производительность модели ухудшится на порядок.
Поэтому динамическая типизация отключена по умолчанию; разработчик должен явно установить свойство boolean «dynamicRoles» модели для активации динамической типизации (и потерпеть соответствующее снижение производительности). Мы рекомендуем не использовать динамическую типизацию, если можно переработать приложение, чтобы избежать её.
Представления
Делегаты представления должны быть максимально простыми. В делегате должно быть достаточно QML для отображения необходимой информации. Любая дополнительная функциональность, которая не требуется немедленно (например, если она отображает больше информации при нажатии), не должна создаваться до тех пор, пока не понадобится (см. следующий раздел о ленивой инициализации).
Следующий список — хороший сводный обзор моментов, которые следует учитывать при проектировании делегата:
- Чем меньше элементов в делегате, тем быстрее они создаются, а значит, тем быстрее можно прокручивать представление.
- Минимизируйте количество привязок в делегате; в частности, используйте якоря вместо привязок для относительного позиционирования внутри делегата.
- Избегайте использования элементов ShaderEffect в делегатах.
- Никогда не включайте обрезку в делегате.
Вы можете установить свойство cacheBuffer представления, чтобы разрешить асинхронное создание и буферизацию делегатов за пределами видимой области. Использование cacheBuffer рекомендуется для делегатов представления, которые нетривиальны и вряд ли будут созданы за одну кадр.
Помните, что cacheBuffer хранит дополнительные делегаты в памяти. Поэтому выгоду от использования cacheBuffer необходимо уравновесить с дополнительным использованием памяти. Разработчики должны использовать бенчмаркинг, чтобы найти наилучшее значение для своего случая использования, так как увеличение нагрузки на память, вызванное использованием cacheBuffer , в некоторых редких случаях может привести к снижению частоты кадров при прокрутке.
Визуальные эффекты
Qt Quick 2 включает несколько функций, которые позволяют разработчикам и дизайнерам создавать исключительные визуальные пользовательские интерфейсы. Гладкость и динамические переходы, а также визуальные эффекты могут быть очень эффективны в приложении, но необходимо проявлять осторожность при использовании некоторых функций QML, так как они могут повлиять на производительность.
Анимации
В целом, анимация свойства приведет к переоценке любых привязок, ссылающихся на это свойство. Обычно это желательно, но в других случаях может быть лучше отключить привязку перед выполнением анимации и затем повторно назначить привязку после завершения анимации.
Избегайте выполнения JavaScript во время анимации. Например, следует избегать выполнения сложного выражения JavaScript для каждого кадра анимации свойства x.
Разработчики должны проявлять особую осторожность при использовании анимаций на основе скриптов, так как они выполняются в основном потоке (и, следовательно, могут привести к пропусканию кадров, если займут слишком много времени).
Частицы
Модуль Qt Quick Particles позволяет без проблем интегрировать красивые эффекты частиц в пользовательские интерфейсы. Однако у каждой платформы есть разные возможности графического оборудования, и модуль Particles не может ограничить параметры тем, что может поддерживать ваше оборудование. Чем больше частиц вы пытаетесь отобразить (и чем они больше), тем быстрее ваше графическое оборудование должно быть, чтобы отображать их с частотой 60 кадров в секунду. Влияние на большее количество частиц требует более быстрого процессора. Поэтому важно тщательно протестировать все эффекты частиц на вашей целевой платформе, чтобы откалибровать количество и размер частиц, которые вы можете отобразить с частотой 60 кадров в секунду.
Следует отметить, что система частиц может быть отключена, когда она не используется (например, в невидимом элементе), чтобы избежать ненужного моделирования.
См. Руководство по производительности системы частиц для получения более подробной информации.
Управление жизненным циклом элементов
Разделив приложение на простые, модульные компоненты, каждый из которых содержится в отдельном файле QML, вы можете добиться более быстрого времени запуска приложения и лучшего управления использованием памяти, а также уменьшить количество активных, но невидимых элементов в вашем приложении.
Ленивая инициализация
Движок QML выполняет некоторые сложные действия, чтобы убедиться, что загрузка и инициализация компонентов не приводят к пропусканию кадров. Однако нет лучшего способа сократить время запуска, чем избежать выполнения ненужной работы и отложить ее до необходимости. Это можно сделать, используя либо Loader, либо создавая компоненты динамически.
Использование Loader
Loader — это элемент, который позволяет динамически загружать и выгружать компоненты.
- Использование свойства «активный» Loader позволяет отложить инициализацию до момента необходимости.
- Использование перегруженной функции «setSource()» позволяет указать начальные значения свойств.
- Установка свойства Loader asynchronous в значение true также может улучшить плавность работы при создании компонента.
Использование динамического создания
Разработчики могут использовать функцию Qt.createComponent() для динамического создания компонента во время выполнения из JavaScript, а затем вызвать createObject() для его экземплирования. В зависимости от семантики владения, указанной в вызове, разработчик может потребоваться вручную удалить созданный объект. Подробнее см. Динамическое создание QML-объектов из JavaScript.
Удаление неиспользуемых элементов
Элементы, которые невидимы, потому что являются дочерними элементами невидимого элемента (например, второй вклад в виджете вкладок, в то время как отображена первая вкладка), в большинстве случаев следует инициализировать лениво и удалять, когда они больше не используются, чтобы избежать постоянных затрат на их активацию (например, рендеринг, анимации, оценка связывания свойств и т.д.).
Элемент, загруженный с помощью элемента Loader, может быть освобожден, если сбросить свойство "source" или "sourceComponent" элемента Loader, в то время как другие элементы могут быть явно освобождены путем вызова destroy() для них. В некоторых случаях может потребоваться оставить элемент активным, в этом случае его следует, как минимум, сделать невидимым.
Дополнительную информацию об активных, но невидимых элементах см. в предстоящем разделе «Рендеринг».
Рендеринг
Граф сцены, используемый для рендеринга в QtQuick 2, позволяет создавать высокодинамичные, анимированные пользовательские интерфейсы, которые рендерятся плавно с частотой 60 кадров в секунду. Однако существуют некоторые факторы, которые могут значительно снизить производительность рендеринга, и разработчики должны стараться избегать этих проблем по мере возможности.
Обрезка
Обрезка по умолчанию отключена и должна включаться только по необходимости.
Обрезка — это визуальный эффект, а НЕ оптимизация. Она увеличивает (а не уменьшает) сложность для рендерера. Если обрезка включена, элемент будет обрезать свое собственное рисование, а также рисование своих дочерних элементов, до своего ограничивающего прямоугольника. Это препятствует возможности рендерера свободно переупорядочивать порядок рисования элементов, что приводит к неэффективному прохождению по графу сцены в лучшем случае.
Обрезка внутри делегата особенно вредна и должна быть избегана любой ценой.
Перерисовывание и невидимые элементы
Если у вас есть элементы, которые полностью закрыты другими (непрозрачными) элементами, лучше всего установить их свойство "visible" в false , иначе они будут напрасно отрисовываться.
Аналогично, элементы, которые невидимы (например, вторая вкладка в виджете вкладок, когда отображена первая вкладка), но должны быть инициализированы во время запуска (например, если затраты на создание второй вкладки слишком велики, чтобы сделать это только при активации вкладки), должны иметь свойство "visible" установленное в false, чтобы избежать затрат на их отрисовку (хотя, как уже объяснялось ранее, они по-прежнему будут нести затраты на любые анимации или оценки связей, так как они все еще активны).
Прозрачный против Непрозрачный
Непрозрачный контент, как правило, намного быстрее отрисовывается, чем прозрачный. Причина в том, что для прозрачного контента требуется смешивание, и рендерер может потенциально лучше оптимизировать непрозрачный контент.
Изображение с одним прозрачным пикселем обрабатывается как полностью прозрачное, даже если оно в основном непрозрачно. То же самое относится к BorderImage с прозрачными краями.
Shader
Тип ShaderEffect позволяет размещать код GLSL непосредственно в приложении Qt Quick с очень небольшими накладными расходами. Однако важно понимать, что фрагментная программа должна выполняться для каждого пикселя в отрисовываемой фигуре. При развертывании на устройствах с низкой производительностью и если шейдер охватывает большое количество пикселей, следует ограничить фрагментный шейдер несколькими инструкциями, чтобы избежать низкой производительности.
Шейдеры, написанные на GLSL, позволяют писать сложные преобразования и визуальные эффекты, однако их следует использовать с осторожностью. Использование ShaderEffectSource приводит к предварительному рендерингу сцены в FBO перед её отрисовкой. Эти дополнительные расходы могут быть довольно высокими.
Распределение и сборка памяти
Объем памяти, который будет выделен приложением, и способ выделения этой памяти являются очень важными соображениями. Помимо очевидных опасений по поводу условий недостатка памяти на устройствах с ограниченным объёмом памяти, выделение памяти на куче — это достаточно дорогостоящая операция, и некоторые стратегии выделения могут привести к увеличению фрагментации данных по страницам. JavaScript использует управляемую кучу памяти, которая автоматически собирается, и это имеет некоторые преимущества, а также некоторые важные последствия.
Приложение, написанное на QML, использует память как из C++-кучи, так и из автоматически управляемой JavaScript-кучи. Разработчик приложения должен понимать тонкости каждого из них, чтобы максимально повысить производительность.
Советы для разработчиков приложений QML
Советы и рекомендации, содержащиеся в этом разделе, являются лишь руководящими принципами и могут не быть применимы во всех случаях. Убедитесь, что тщательно тестируете и анализируете свое приложение с использованием эмпирических показателей, чтобы принять наилучшие решения.
Инициализация компонентов лениво
Если ваше приложение состоит из нескольких представлений (например, нескольких вкладок), но в любой момент времени требуется только одно, вы можете использовать ленивую инициализацию, чтобы свести к минимуму объем памяти, который необходимо выделять в любой момент времени. Дополнительную информацию см. в предыдущем разделе «Ленивая инициализация».
Удаление неиспользуемых объектов
Если вы загружаете компоненты лениво или создаете объекты динамически во время выражения JavaScript, часто лучше destroy() их вручную, а не ждать автоматической сборки мусора. Дополнительную информацию см. в предыдущем разделе «Управление жизненным циклом элементов».
Не вызывайте сборщик мусора вручную
В большинстве случаев неразумно вызывать сборщик мусора вручную, так как это заблокирует поток GUI на значительное время. Это может привести к пропускам кадров и рывковым анимациям, чего следует избегать любой ценой.
Существуют некоторые случаи, когда ручное вызов сборщика мусора приемлемо (и это более подробно объясняется в предстоящем разделе), но в большинстве случаев вызов сборщика мусора не требуется и контрпродуктивен.
Избегайте определения нескольких идентичных неявных типов
Если QML-элемент имеет настроенное свойство в QML, он становится своим собственным неявным типом. Это более подробно объяснено в предстоящем разделе. Если несколько одинаковых неявных типов определены непосредственно в компоненте, будет тратиться память. В этом случае обычно лучше явно определить новый компонент, который затем можно использовать повторно.
Определение настраиваемого свойства часто может быть полезной оптимизацией производительности (например, для уменьшения количества необходимых или повторно оцениваемых связей) или улучшить модульность и поддерживаемость компонента. В этих случаях рекомендуется использовать настраиваемые свойства. Однако, если новый тип используется более одного раза, его следует разделить на свой собственный компонент (.qml-файл) для экономии памяти.
Использование существующих компонентов
Если вы рассматриваете создание нового компонента, стоит проверить, не существует ли уже такой компонент в наборе компонентов для вашей платформы. В противном случае вы вынудите движок QML генерировать и хранить метаданные типа для типа, который по сути является дубликатом другого существующего и, возможно, уже загруженного компонента.
Используйте типы-синглтоны вместо скриптов библиотек pragmas
Если вы используете скрипт библиотеки pragmas для хранения данных экземпляра на уровне приложения, рассмотрите использование типа-синглтона QObject вместо этого. Это должно привести к улучшению производительности и уменьшению использования памяти JavaScript-кучи.
Выделение памяти в приложении QML
Использование памяти приложением QML может быть разделено на две части: использование C++-кучи и использование JavaScript-кучи. Часть памяти, выделенной в каждой из них, будет неизбежной, так как её выделяет движок QML или движок JavaScript, в то время как остальная часть зависит от решений, принятых разработчиком приложения.
C++-куча будет содержать:
- фиксированные и неизбежные накладные расходы движка QML (внутренние структуры данных, информация о контексте и т. д.);
- скомпилированные данные и информация о типах на основе компонента, включая метаданные свойств на основе типа, которые генерируются движком QML в зависимости от того, какие модули и компоненты загружаются приложением;
- данные C++ на объект (включая значения свойств) плюс метаобъектная иерархия на элемент в зависимости от того, какие компоненты экземплирует приложение;
- любые данные, которые выделены специально импортами QML (библиотеками).
JavaScript-куча будет содержать:
- фиксированные и неизбежные накладные расходы самого движка JavaScript (включая встроенные типы JavaScript);
- фиксированные и неизбежные накладные расходы нашей интеграции JavaScript (функции конструкторов для загруженных типов, шаблоны функций и т. д.);
- информация о расположении на основе типа и другие внутренние данные типа, генерируемые движком JavaScript во время выполнения для каждого типа (см. примечание ниже, касающееся типов);
- данные JavaScript на объект («переменные var», функции JavaScript и обработчики сигналов, а также не оптимизированные выражения связывания);
- переменные, выделенные во время оценки выражения.
Кроме того, будет выделена одна JavaScript-куча для использования в главном потоке и, необязательно, ещё одна JavaScript-куча для использования в потоке WorkerScript. Если приложение не использует элемент WorkerScript, эти расходы не будут понесены. Размер JavaScript-кучи может составлять несколько мегабайт, поэтому приложения, написанные для устройств с ограниченным объёмом памяти, могут получить выгоду от избегания элемента WorkerScript несмотря на его полезность при асинхронном заполнении моделей списков.
Обратите внимание, что как движок QML, так и движок JavaScript будут автоматически генерировать свои собственные кэши данных типов о наблюдаемых типах. Каждый загруженный компонентом является отдельным (явным) типом, и каждый элемент (экземпляр компонента), который определяет свои собственные пользовательские свойства в QML, является неявным типом. Любой элемент (экземпляр компонента), не определяющий никаких пользовательских свойств, рассматривается движками JavaScript и QML как имеющий тип, явно определённый компонентом, а не его собственным неявным типом.
Рассмотрим следующий пример:
import QtQuick
Item {
id: root
Rectangle {
id: r0
color: "red"
}
Rectangle {
id: r1
color: "blue"
width: 50
}
Rectangle {
id: r2
property int customProperty: 5
}
Rectangle {
id: r3
property string customProperty: "hello"
}
Rectangle {
id: r4
property string customProperty: "hello"
}
}В предыдущем примере прямоугольники r0 и r1 не имеют пользовательских свойств, поэтому JavaScript- и QML-движки считают их одинакового типа. То есть, r0 и r1 считаются экземплярами явно определённого типа Rectangle. Прямоугольники r2, r3 и r4 имеют пользовательские свойства и считаются разных (явных) типов. Обратите внимание, что r3 и r4 считаются разными типами, даже если у них идентичная информация о свойствах, просто потому, что пользовательское свойство не было объявлено в компоненте, экземпляром которого они являются.
Если r3 и r4 были бы экземплярами компонента RectangleWithString, и в определении этого компонента было бы объявлено строковое свойство с именем customProperty, то r3 и r4 считались бы одного типа (то есть, они были бы экземплярами типа RectangleWithString, а не определяли свой собственный явный тип).
Подробные соображения по распределению памяти
При принятии решений, касающихся распределения памяти или компромиссов производительности, важно учитывать влияние производительности кэша процессора, страницирования операционной системы и сборки мусора JavaScript-движка. Потенциальные решения должны быть тщательно протестированы, чтобы выбрать наилучшее.
Никакой набор общих рекомендаций не может заменить глубокого понимания основ компьютерных наук в сочетании с практическим знанием деталей реализации платформы, для которой разрабатывается приложение. Кроме того, никакой объём теоретических расчётов не заменит хороших бенчмарков и инструментов анализа при принятии решений о компромиссах.
Дробление
Дробление — проблема разработки на C++. Если разработчик приложения не определяет никаких типов или плагинов C++, он может безопасно пропустить этот раздел.
Со временем приложение будет выделять большие объёмы памяти, записывать данные в эту память и затем освобождать некоторые части после завершения использования данных. Это может привести к тому, что «освобождённая» память будет расположена в несмежных кусках, которые не могут быть возвращены операционной системе для использования другими приложениями. Это также влияет на характеристики кэширования и доступа приложения, так как «живые» данные могут быть распределены по многим различным страницам физической памяти. В свою очередь, это может заставить операционную систему выполнять подкачку, что может вызвать ввод-вывод на файловой системе — что является, в сравнении, крайне медленной операцией.
Дробление можно избежать, используя пул-аллокаторы (и другие аллокаторы смежной памяти), уменьшая объём памяти, выделяемой за один раз, тщательно управляя жизненным циклом объектов, периодически очищая и перестраивая кэши или используя среду исполнения с управлением памятью и сборкой мусора (например, JavaScript).
Сборка мусора
JavaScript предоставляет сборку мусора. Память, выделенная в куче JavaScript (в отличие от кучи C++), принадлежит JavaScript-движку. Движок периодически собирает все неиспользуемые данные в куче JavaScript.
Последствия сборки мусора
Сборка мусора имеет преимущества и недостатки. Это означает, что ручное управление жизненным циклом объектов менее важно. Однако это также означает, что потенциально длительная операция может быть инициирована JavaScript-движком в момент, неподконтрольный разработчику приложения. Если разработчик приложения не обратит должного внимания на использование кучи JavaScript, частота и продолжительность сборки мусора могут отрицательно повлиять на пользовательский опыт.
Ручное вызов сборщика мусора
Приложению, написанному на QML, (скорее всего) потребуется выполнить сборку мусора на определенном этапе. Хотя сборка мусора будет автоматически запускаться JavaScript-движком, когда количество свободной памяти станет низким, иногда лучше, если разработчик приложения принимает решения о том, когда вручную вызывать сборщик мусора (хотя обычно это не так).
Разработчик приложения, скорее всего, лучше всего понимает, когда приложение будет простаивать в течение значительных периодов времени. Если QML-приложение использует много памяти в куче JavaScript, вызывая регулярные и разрушительные циклы сборки мусора во время задач, чувствительных к производительности (например, прокрутка списка, анимации и так далее), разработчику приложения может быть выгодно вручную вызывать сборщик мусора во время периодов бездействия. Периоды бездействия идеально подходят для выполнения сборки мусора, поскольку пользователь не заметит никакого ухудшения пользовательского опыта (пропущенные кадры, рывковые анимации и т. д.), что может произойти при вызове сборщика мусора во время активности.
Сборщик мусора можно вызвать вручную, вызвав gc() в JavaScript. Это запустит цикл полной сборки, который может занять от нескольких сотен до более тысячи миллисекунд, и поэтому его следует избегать, насколько это возможно.
Компромиссы между памятью и производительностью
В некоторых ситуациях можно пожертвовать увеличением использования памяти в обмен на уменьшение времени обработки. Например, кэширование результата поиска символа, используемого в плотном цикле, в временную переменную в выражении JavaScript приведёт к значительному улучшению производительности при оценке этого выражения, но это предполагает выделение временной переменной. В некоторых случаях такие компромиссы оправданы (например, в случае выше, что почти всегда оправдано), но в других случаях может быть лучше позволить обработке занять немного больше времени, чтобы избежать увеличения нагрузки на память системы.
В некоторых случаях влияние увеличенной нагрузки на память может быть экстремальным. В некоторых ситуациях торговля памятью за предполагаемый прирост производительности может привести к увеличению страниц или кэш-повреждениям, вызвав сильное снижение производительности. Всегда необходимо тщательно протестировать влияние компромиссов, чтобы определить, какое решение является лучшим в данной ситуации.
Для получения подробной информации о производительности кэша и компромиссах между временем и памятью, обратитесь к следующим статьям:
- Отличная статья Ульриха Дреппера: «Всё, что программист должен знать о памяти», по адресу: https://people.freebsd.org/~lstewart/articles/cpumemory.pdf.
- Отличные руководства Агнера Фога по оптимизации приложений C++ по адресу: http://www.agner.org/optimize/.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.2/qtquick-performance.html