Рекомендации по производительности
Учёт временных характеристик
Как разработчикам приложений, вам необходимо стремиться к тому, чтобы движок рендеринга поддерживал стабильную частоту обновления 60 кадров в секунду. 60 кадров в секунду означают, что между каждым кадром имеется примерно 16 миллисекунд, в течение которых можно выполнить обработку, включая обработку, необходимую для загрузки примитивов рисования в графическое оборудование.
На практике это означает, что разработчики приложений должны:
- использовать асинхронное, управляемое событиями программирование, где это возможно
- использовать потоки обработки для выполнения значительной части вычислений
- никогда не запускать цикл событий вручную
- никогда не тратить более пары миллисекунд на кадр в блокирующих функциях
Отклонение от этих правил приведёт к пропусканию кадров, что оказывает существенное влияние на пользовательский опыт.
Примечание: Искушение использовать такую модель, как создание собственного цикла событий QEventLoop или вызов QCoreApplication::processEvents() для избежания блокировки внутри блока кода C++, вызываемого из QML, должно быть полностью отвергнуто. Это опасно, поскольку при входе в цикл событий в обработчике сигналов или привязке движок QML продолжает выполнение других привязок, анимаций, переходов и т. д. Эти привязки могут вызвать побочные эффекты, например, разрушение иерархии, содержащей ваш цикл событий.
Профилирование
Самый важный совет: используйте встроенный в Qt Creator профайлер QML. Знание того, где тратится время в приложении, позволит сосредоточиться на реальных проблемах, а не на потенциальных. Для получения дополнительной информации о том, как использовать инструмент профилирования QML, см. руководство по Qt Creator.
Определение наиболее часто выполняемых привязок или функций, в которых приложение тратит больше всего времени, позволит вам решить, нужно ли оптимизировать проблемные области или перепроектировать некоторые детали реализации приложения для повышения производительности. Попытка оптимизировать код без профилирования, скорее всего, приведёт к незначительному, а не существенному улучшению производительности.
Код JavaScript
Большинство приложений QML содержат значительный объём кода JavaScript в виде динамических функций, обработчиков сигналов и выражений привязки свойств. Это обычно не проблема. Благодаря некоторым оптимизациям в движке QML, таким как оптимизации компилятора привязок, в некоторых случаях он может быть быстрее, чем вызов функции C++. Однако необходимо позаботиться о том, чтобы случайно не запускалась ненужная обработка.
Преобразование типов
Одной из основных затрат при использовании JavaScript является то, что в большинстве случаев при обращении к свойству типа QML создаётся объект JavaScript с внешним ресурсом, содержащим основополагающие данные C++ (или ссылку на них). В большинстве случаев это довольно недорого, но в других случаях это может быть весьма дорого. Одним из примеров дорогостоящего преобразования является присвоение C++ QVariantMap Q_PROPERTY свойству QML "variant". Спискам также может потребоваться много времени, хотя последовательности определённых типов (QList целых чисел, qreal, bool, QString и QUrl) должны быть недорогими; для других типов списков стоимость преобразования высока (создание нового JavaScript Array и добавление новых типов по одному с преобразованием каждого типа из экземпляра C++ в значение JavaScript).
Преобразование между некоторыми основными типами свойств (такими как свойства "string" и "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 вызывает повторную оценку привязки свойства "text" шесть раз (что затем приводит к повторной оценке всех других привязок свойств, которые зависят от значения text, а также обработчика сигнала 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");
}
} В приведённом выше примере повторно оцениваться будет только промежуточная привязка, что приведёт к значительному увеличению производительности.
Рекомендации по типу значений
Свойства типа значений (font, color, vector3d и т. д.) имеют аналогичные семантику свойства и уведомления об изменении объекта QObject типа свойств последовательности. Следовательно, вышеприведённые рекомендации по последовательностям также применимы к свойствам типа значений. Хотя они обычно менее проблемны в случае значений (так как количество подсвойств типа значения обычно значительно меньше, чем количество элементов в последовательности), любое увеличение количества привязок, которые бесполезно переоцениваются, окажет отрицательное влияние на производительность.
Общие советы по производительности
Общие соображения о производительности JavaScript, обусловленные конструкцией языка, применимы и к QML. Наиболее заметно:
- По возможности избегайте использования eval()
- Не удаляйте свойства объектов
Общие элементы пользовательского интерфейса
Элементы текста
Вычисление макетов текста может быть медленной операцией. По возможности используйте формат PlainText вместо StyledText, так как это уменьшает количество работы, выполняемой движком макета. Если вы не можете использовать PlainText (так как вам нужно встраивать изображения или использовать теги для указания диапазонов символов с определённым форматированием (жирный, курсив и т. д.) вместо всего текста), то вы должны использовать StyledText.
Следует использовать только AutoText, если текст может быть (но, вероятно, не является) StyledText, так как этот режим повлечёт за собой затраты на разбор. Режим RichText не следует использовать, поскольку StyledText предоставляет практически все его функции с меньшими затратами.
Изображения
Изображения являются важной частью любого пользовательского интерфейса. К сожалению, они также являются значительным источником проблем из-за времени загрузки, занимаемого объёма памяти и способа их использования.
Асинхронная загрузка
Изображения часто бывают достаточно большими, поэтому желательно обеспечить, чтобы загрузка изображения не блокировала поток пользовательского интерфейса. Установите свойство «asynchronous» элемента QML Image в true, чтобы включить асинхронную загрузку изображений из локальной файловой системы (удаленные изображения всегда загружаются асинхронно), если это не негативно скажется на эстетике пользовательского интерфейса.
Элементы изображения со свойством «asynchronous», установленным в 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. Хотя оптимальная реализация такой модели сильно зависит от конкретного использования, некоторые общие рекомендации таковы:
- Максимально используйте асинхронность
- Выполняйте все вычисления в потоке обработки низкого приоритета
- Группируйте операции back-end, чтобы свести к минимуму (возможно, медленные) операции ввода-вывода и межпроцессного взаимодействия
Важно отметить, что рекомендуется использовать поток обработки низкого приоритета, чтобы минимизировать риск голодания потока пользовательского интерфейса (что может привести к ухудшению восприятия производительности). Кроме того, помните, что механизмы синхронизации и блокировки могут значительно снизить производительность, поэтому следует избегать ненужных блокировок.
Тип QML ListModel
QML предоставляет тип ListModel, который можно использовать для подачи данных в ListView. Он должен быть достаточно эффективным для большинства случаев использования.
Заполнение в потоке обработки низкого приоритета
Элементы ListModel могут быть заполнены в потоке обработки низкого приоритета в JavaScript. Разработчик должен явно вызвать «sync()» на ListModel из WorkerScript, чтобы синхронизировать изменения с основным потоком. Дополнительную информацию см. в документации WorkerScript.
Обратите внимание, что использование элемента WorkerScript приведет к созданию отдельного движка JavaScript (так как движок JavaScript предназначен для каждого потока). Это увеличит использование памяти. Несколько элементов WorkerScript будут использовать один и тот же поток обработки, поэтому влияние на использование памяти при использовании второго или третьего элемента WorkerScript незначительно, если приложение уже использует один.
Не использовать динамические роли
Элемент ListModel в QtQuick 2 намного производительнее, чем в QtQuick 1. Улучшения производительности в основном связаны с предположениями о типе ролей в каждом элементе данной модели. Если тип не изменяется, производительность кэширования значительно повышается. Если тип может динамически изменяться от элемента к элементу, эта оптимизация становится невозможной, и производительность модели ухудшится на порядок. Поэтому динамическая типизация по умолчанию отключена. Разработчик должен явно установить свойство «dynamicRoles» модели в значение true, чтобы включить динамическую типизацию (и потерпеть соответствующее снижение производительности). Рекомендуется не использовать динамическую типизацию, если возможно переработать приложение, чтобы избежать этого.
Представления
Делегат представления должен быть максимально простым. В делегате должно быть достаточно QML для отображения необходимой информации. Любая дополнительная функциональность, которая не требуется немедленно (например, если она отображает больше информации при нажатии), не должна создаваться до тех пор, пока не понадобится (см. следующий раздел о ленивой инициализации).
Следующий список хорошо обобщает моменты, которые следует учитывать при проектировании делегата:
- Чем меньше элементов в делегате, тем быстрее они могут быть созданы, а значит, тем быстрее можно прокручивать представление.
- Минимизируйте количество привязок в делегате. В частности, для относительного позиционирования в делегате используйте якори, а не привязки.
- Избегайте использования элементов ShaderEffect в делегатах.
- Никогда не включайте обрезку делегата.
Вы можете установить свойство cacheBuffer представления, чтобы разрешить асинхронное создание и буферизацию делегатов за пределами видимой области. Рекомендуется использовать cacheBuffer для делегатов представления, которые не являются тривиальными и вряд ли будут созданы за один кадр.
Помните, что cacheBuffer хранит дополнительные делегаты в памяти. Поэтому преимущества использования cacheBuffer необходимо уравновесить с дополнительным использованием памяти. Разработчики должны использовать бенчмаркинг, чтобы найти наилучшее значение для своего конкретного случая использования, поскольку повышенное давление на память, вызванное использованием cacheBuffer, в некоторых редких случаях может привести к снижению частоты кадров при прокрутке.
Визуальные эффекты
Qt Quick 2 включает несколько функций, которые позволяют разработчикам и дизайнерам создавать исключительно привлекательные пользовательские интерфейсы. Плавность и динамические переходы, а также визуальные эффекты могут быть эффективно использованы в приложении, но при работе с некоторыми функциями QML необходимо проявить осторожность, поскольку они могут повлиять на производительность.
Анимации
В целом, анимация свойства приведёт к переоценке всех привязок, ссылающихся на это свойство. Обычно это то, что требуется, но в других случаях лучше отключить привязку перед выполнением анимации, а затем повторно назначить привязку после завершения анимации.
Избегайте выполнения JavaScript во время анимации. Например, следует избегать выполнения сложных выражений JavaScript для каждого кадра анимации свойства x.
Разработчики должны проявлять особую осторожность при использовании анимаций сценариев, так как они выполняются в главном потоке (и, следовательно, могут привести к пропуску кадров, если займут слишком много времени).
Частицы
Модуль Qt Quick Particles позволяет интегрировать красивые эффекты частиц в пользовательские интерфейсы. Однако каждая платформа обладает разными возможностями графического оборудования, и модуль Particles не может ограничить параметры тем, что может эффективно поддерживать ваше оборудование. Чем больше частиц вы пытаетесь отобразить (и чем они больше), тем быстрее ваше графическое оборудование должно работать, чтобы отображать 60 кадров в секунду. Воздействие на большее количество частиц требует более быстрого процессора. Поэтому важно тщательно протестировать все эффекты частиц на вашей целевой платформе, чтобы откалибровать количество и размер частиц, которые можно отобразить с частотой 60 кадров в секунду.
Следует отметить, что систему частиц можно отключить при её отсутствии (например, на невидимом элементе), чтобы избежать ненужного моделирования.
Дополнительную информацию см. в руководстве по производительности системы частиц Particle System Performance Guide.
Управление жизненным циклом элемента
Разделяя приложение на простые, модульные компоненты, каждый из которых находится в отдельном файле QML, можно добиться более быстрого времени запуска приложения, лучшего управления использованием памяти и сократить количество активных, но невидимых элементов в вашем приложении.
Ленивая инициализация
Движок QML выполняет некоторые сложные действия, чтобы обеспечить, что загрузка и инициализация компонентов не приведут к пропуску кадров. Однако нет лучшего способа сократить время запуска, чем избежать выполнения ненужной работы и отложить её до необходимости. Это можно сделать, используя либо Loader, либо создавая компоненты динамически.
Использование Loader
Loader — это элемент, который позволяет динамически загружать и выгружать компоненты.
- Используя свойство «active» Loader, инициализацию можно отложить до момента необходимости.
- Используя перегруженную функцию «setSource()», можно указать начальные значения свойств.
- Установление свойства Loader asynchronous в значение true также может улучшить плавность работы при создании компонента.
Использование динамического создания
Разработчики могут использовать функцию Qt.createComponent() для динамического создания компонента во время выполнения из JavaScript и затем вызвать createObject() для его экземпляризации. В зависимости от семантики владения, указанной в вызове, разработчику может потребоваться вручную удалить созданный объект. Подробнее см. Динамическое создание QML-объектов из JavaScript.
Уничтожение неиспользуемых элементов
Элементы, которые невидимы, потому что являются дочерними элементами невидимого элемента (например, вторая вкладка в наборе вкладок, в то время как отображается первая вкладка), в большинстве случаев должны инициализироваться лениво и удаляться, когда больше не используются, чтобы избежать постоянной стоимости их активации (например, отрисовки, анимации, вычисления связывания свойств и т.д.).
Элемент, загруженный с помощью элемента Loader, может быть освобожден путем сброса свойства «source» или «sourceComponent» Loader, в то время как другие элементы могут быть явно освобождены путем вызова destroy() на них. В некоторых случаях может потребоваться оставить элемент активным, в этом случае его следует сделать по крайней мере невидимым.
Дополнительную информацию об активных, но невидимых элементах см. в разделе «Отрисовка».
Отрисовка
Граф сцены, используемый для отрисовки в QtQuick 2, позволяет динамически отрисовывать высокодинамичные, анимированные пользовательские интерфейсы со скоростью 60 кадров в секунду. Однако существуют некоторые вещи, которые могут значительно снизить производительность отрисовки, и разработчики должны избегать этих проблем по возможности.
Обрезка
Обрезка отключена по умолчанию и должна быть включена только при необходимости.
Обрезка — это визуальный эффект, а НЕ оптимизация. Она увеличивает (а не уменьшает) сложность для рендерера. Если обрезка включена, элемент будет обрезать собственную отрисовку, а также отрисовку своих дочерних элементов до своего ограничивающего прямоугольника. Это препятствует возможности рендерера свободно переупорядочивать порядок рисования элементов, что приводит к неэффективному обходу графа сцены в лучшем случае.
Обрезка внутри делегата особенно вредна и должна быть избегаема во что бы то ни стало.
Перерисовывание и невидимые элементы
Если у вас есть элементы, которые полностью покрыты другими (непрозрачными) элементами, лучше всего установить их свойство «visible» в false , иначе они будут бесполезно отрисовываться.
Аналогично, элементы, которые невидимы (например, вторая вкладка в наборе вкладок, в то время как отображается первая вкладка), но должны быть инициализированы во время запуска (например, если стоимость инициализации второй вкладки занимает слишком много времени, чтобы сделать это только при активации вкладки), должны иметь свойство «visible» установленным в false, чтобы избежать затрат на их отрисовку (хотя, как уже объяснялось, они все равно будут нести затраты на любые анимации или вычисления связей, поскольку они все еще активны).
Прозрачный против непрозрачного
Отрисовка непрозрачного контента, как правило, намного быстрее, чем полупрозрачного. Причина в том, что полупрозрачный контент требует смешивания, а рендерер может потенциально лучше оптимизировать непрозрачный контент.
Изображение с одним полупрозрачным пикселем обрабатывается как полностью полупрозрачное, даже если оно в основном непрозрачное. То же самое верно для BorderImage с прозрачными краями.
Тени
Тип ShaderEffect позволяет размещать код GLSL непосредственно в приложении Qt Quick с очень небольшими накладными расходами. Однако важно понимать, что фрагментная программа должна выполняться для каждого пикселя в отрисовываемой фигуре. При развертывании на низкопроизводительных устройствах и если теней покрывает большое количество пикселей, необходимо ограничить фрагментную тень несколькими инструкциями, чтобы избежать плохой производительности.
Тени, написанные на GLSL, позволяют создавать сложные преобразования и визуальные эффекты, однако ими следует пользоваться осторожно. Использование ShaderEffectSource приводит к предварительной отрисовке сцены в FBO перед отрисовкой. Эти дополнительные накладные расходы могут быть достаточно дорогими.
Выделение и сборка памяти
Объем памяти, который будет выделен приложением, и способ выделения этой памяти являются очень важными соображениями. Помимо очевидных проблем с недостатком памяти на устройствах с ограниченным объемом памяти, выделение памяти в куче является довольно вычислительно затратным процессом, а определенные стратегии выделения могут привести к увеличению фрагментации данных по страницам. JavaScript использует управляемую кучу памяти, которая автоматически собирается мусором, и это имеет некоторые преимущества, но также и некоторые важные последствия.
Приложение, написанное на QML, использует память как из C++ кучи, так и из автоматически управляемой JavaScript кучи. Разработчик приложения должен быть осведомлен о тонкостях каждого из них, чтобы максимизировать производительность.
Рекомендации для разработчиков приложений QML
Рекомендации и предложения, содержащиеся в этом разделе, являются лишь руководством и могут не быть применимы во всех случаях. Убедитесь, что вы тщательно тестируете и анализируете свое приложение, используя эмпирические метрики, чтобы принять наилучшие возможные решения.
Инициализировать компоненты лениво
Если ваше приложение состоит из нескольких представлений (например, нескольких вкладок), но в любой момент времени требуется только одно, вы можете использовать ленивую инициализацию, чтобы минимизировать объем памяти, который вам нужно выделить в любой момент времени. Дополнительную информацию см. в предыдущем разделе о Ленивой инициализации.
Уничтожить неиспользуемые объекты
Если вы загружаете компоненты лениво или создаете объекты динамически во время выражения JavaScript, зачастую лучше вручную destroy() их, а не ждать автоматической сборки мусора. Дополнительную информацию см. в предыдущем разделе о Управлении жизненным циклом элементов.
Не вызывайте сборщик мусора вручную
В большинстве случаев не рекомендуется вызывать сборщик мусора вручную, так как это заблокирует поток графического интерфейса на значительный период времени. Это может привести к пропускам кадров и рывкам анимации, чего следует избегать во что бы то ни стало.
Есть некоторые случаи, когда ручное обращение к сборщику мусора приемлемо (и это подробно объясняется в предстоящем разделе), но в большинстве случаев вызов сборщика мусора не нужен и контрпродуктивен.
Избегайте определения нескольких одинаковых неявных типов
Если элемент QML имеет пользовательское свойство, определенное в QML, он становится своим собственным неявным типом. Это подробно описано в следующем разделе. Если несколько одинаковых неявных типов определены непосредственно в компоненте, это приведет к потере некоторой памяти. В такой ситуации обычно лучше определить новый компонент, который можно будет повторно использовать.
Определение пользовательского свойства часто может быть полезной оптимизацией производительности (например, для сокращения числа необходимых или повторно оцениваемых связей) или может повысить модульность и поддерживаемость компонента. В таких случаях использование пользовательских свойств приветствуется. Однако, если новый тип используется более одного раза, его следует разделить на отдельный компонент (.qml-файл), чтобы сохранить память.
Использовать существующие компоненты
Если вы рассматриваете определение нового компонента, стоит проверить, существует ли такой компонент в наборе компонентов для вашей платформы. В противном случае вы заставите движок QML генерировать и хранить данные типа для типа, который по сути является дубликатом другого существующего и потенциально уже загруженного компонента.
Используйте типы-синглтоны вместо скриптов библиотек pragma
Если вы используете скрипт библиотеки pragma для хранения данных экземпляра на уровне приложения, рассмотрите использование типа синглтона 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.1/qtquick-performance.html