Учет производительности и рекомендации
Учет временных параметров
Как разработчику приложений, вам необходимо стремиться к тому, чтобы движок рендеринга обеспечивал постоянную частоту обновления 60 кадров в секунду. 60 кадров в секунду означает, что примерно через 16 миллисекунд происходит каждый кадр, в котором выполняется обработка, включая обработку, необходимую для загрузки примитивов отрисовки в графическое оборудование.
На практике это означает, что разработчик приложения должен:
- использовать асинхронное, управляемое событиями программирование, где это возможно
- использовать потоки обработки для выполнения значительной обработки
- никогда не запускать цикл событий вручную
- никогда не тратить более нескольких миллисекунд на кадр внутри блокирующих функций
Несоблюдение этих рекомендаций приведет к пропуску кадров, что окажет существенное влияние на пользовательский опыт.
Примечание: Шаблон, который может показаться привлекательным, но никогда не следует использовать, — это создание собственного QEventLoop или вызов QCoreApplication::processEvents() для избежания блокировок внутри блока кода C++, вызванного из QML. Это опасно, так как при входе в цикл событий в обработчике сигналов или привязке движок QML продолжает выполнять другие привязки, анимации, переходы и т. д. Эти привязки могут затем вызвать побочные эффекты, например, разрушение иерархии, содержащей ваш цикл событий.
Профилирование
Самый важный совет: используйте профилировщик QML, входящий в состав Qt Creator. Знание того, где тратится время в приложении, позволит сфокусироваться на реальных проблемах, а не на потенциальных. См. руководство по Qt Creator, чтобы узнать больше о том, как использовать инструмент профилирования QML.
Определение того, какие привязки запускаются чаще всего или в каких функциях ваше приложение тратит больше всего времени, позволит решить, нужно ли оптимизировать проблемные области или перепроектировать некоторые реализации деталей вашего приложения для повышения производительности. Попытка оптимизировать код без профилирования, скорее всего, приведет к незначительному, а не существенному улучшению производительности.
Код JavaScript
Большинство приложений QML содержат большое количество кода JavaScript в виде динамических функций, обработчиков сигналов и выражений привязки свойств. Это обычно не проблема. Благодаря некоторым оптимизациям в движке QML, таким как те, которые были выполнены для компилятора привязок, в некоторых случаях это может быть быстрее, чем вызов функции C++.
Привязки
В QML есть два типа привязок: оптимизированные и неоптимизированные. Рекомендуется сохранять выражения привязки как можно проще, так как движок QML использует оптимизированный оценщик выражений привязки, который может оценивать простые выражения привязки, не переключаясь в полную среду выполнения JavaScript. Эти оптимизированные привязки оцениваются гораздо эффективнее, чем более сложные (неоптимизированные) привязки. Основное требование для оптимизации привязок заключается в том, что информация о типе каждого обращенного к символу должна быть известна на этапе компиляции.
Следует избегать в выражениях привязки, чтобы максимально увеличить возможность оптимизации:
- объявление промежуточных переменных JavaScript
- доступ к свойствам «var»
- вызов функций JavaScript
- создание замыканий или определение функций в выражении привязки
- доступ к свойствам за пределами непосредственной области оценки
- запись в другие свойства как побочный эффект
Привязки работают быстрее, когда они знают тип объектов и свойств, с которыми работают. Это означает, что поиск свойств в выражении привязки, не являющийся окончательным, может быть медленнее в некоторых случаях, когда существует вероятность изменения типа искомого свойства (например, типом производного типа).
Непосредственная область оценки можно обобщить, сказав, что она содержит:
- свойства объекта области выражения (для выражений привязки это объект, к которому принадлежит привязка свойства)
- идентификаторы любых объектов в компоненте
- свойства корневого элемента в компоненте
Идентификаторы объектов из других компонентов и свойства любых таких объектов, а также символы, определенные или включенные из импорта JavaScript, не входят в непосредственную область оценки, и поэтому привязки, обращающиеся к этим элементам, не будут оптимизированы.
Обратите внимание, что если движок QML не может оптимизировать привязку с помощью оптимизированного оценщика выражений привязки, и, следовательно, она должна быть оценена полной средой выполнения JavaScript, некоторые из перечисленных выше советов больше не применимы. Например, иногда бывает полезно кэшировать результат разрешения свойства в промежуточной переменной JavaScript в очень сложном выражении привязки. В следующих разделах содержится более подробная информация об этих видах оптимизаций.
Преобразование типов
Одна из основных затрат при использовании JavaScript заключается в том, что в большинстве случаев при доступе к свойству из типа QML создается объект JavaScript с внешним ресурсом, содержащим данные основного C++ (или ссылку на него). В большинстве случаев это относительно недорого, но в других случаях это может быть довольно дорого. Один пример дорогостоящего преобразования — это присвоение QVariantMap C++ QVariantMap свойству «variant» QML. Списки также могут быть дорогими, хотя последовательности определенных типов (QList целых чисел, qreal, bool, QString и QUrl) должны быть недорогими; другие типы списков связаны с дорогостоящими затратами на преобразование (создание нового массива JavaScript и добавление новых типов по одному с преобразованием каждого типа из экземпляра C++ в значение JavaScript).
Преобразование между некоторыми базовыми типами свойств (например, свойствами «string» и «url») также может быть дорогостоящим. Использование наиболее соответствующего типа свойства позволит избежать ненужных преобразований.
Если вам необходимо экспонировать QVariantMap в QML, используйте свойство «var», а не свойство «variant». В целом, «property var» следует считать предпочтительным по сравнению с «property variant» для всех случаев использования с QtQuick 2.0 и новее (обратите внимание, что «property variant» помечен как устаревший), так как это позволяет хранить истинную ссылку JavaScript (что может уменьшить количество необходимых преобразований в некоторых выражениях).
Разрешение свойств
Разрешение свойств занимает время. Хотя в некоторых случаях результат поиска можно кэшировать и повторно использовать, всегда лучше избегать ненужной работы, если это возможно.
В приведенном ниже примере мы имеем блок кода, который часто выполняется (в данном случае это содержимое явного цикла; но это может быть, например, выражение привязки, которое часто оценивается) и в нем мы многократно ищем объект с идентификатором «rect» и его свойство «color»:
// bad.qml
import QtQuick 2.3
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 2.3
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 2.3
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 2.3
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 2.3
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 1.0») следующего типа 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 2.3
import Qt.example 1.0
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 2.3
import Qt.example 1.0
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 2.3
import Qt.example 1.0
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 2.3
import Qt.example 1.0
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");
}
} В приведенном выше примере при каждом изменении будет переоцениваться только промежуточная привязка, что значительно повышает производительность.
Советы по свойствам типа значение
Свойства типа значение (шрифт, цвет, vector3d и т. д.) имеют аналогичные семантику свойств и уведомлений об изменениях объекта QObject свойств типа последовательности. Таким образом, советы, приведенные выше для последовательностей, также применимы к свойствам типа значение. Хотя с типами значений они обычно менее проблематичны (поскольку количество подсвойств типа значение обычно намного меньше, чем количество элементов в последовательности), любое ненужное увеличение числа повторно оцениваемых привязок окажет негативное влияние на производительность.
Другие объекты JavaScript
Разные движки JavaScript предоставляют разные оптимизации. Движок JavaScript, используемый Qt Quick 2, оптимизирован для создания объектов и поиска свойств, но оптимизации, которые он предоставляет, зависят от определённых критериев. Если ваше приложение не соответствует критериям, движок JavaScript переключается на режим «медленной» обработки со значительно худшей производительностью. Поэтому всегда старайтесь удовлетворять следующим критериям:
- Избегайте использования eval(), если это возможно
- Не удаляйте свойства объектов
Общие элементы интерфейса
Элементы текста
Вычисление макетов текста может быть медленной операцией. Рассмотрите возможность использования формата PlainText вместо StyledText всякий раз, когда это возможно, так как это сокращает количество работы, выполняемой движком макета. Если вы не можете использовать PlainText (поскольку вам нужно встраивать изображения или использовать теги для указания диапазонов символов, которые должны иметь определённый форматирование (жирный, курсив и т. д.) в отличие от всего текста), то вы должны использовать StyledText.
Вы должны использовать AutoText только в том случае, если текст может быть (но, вероятно, не является) StyledText, так как этот режим потребует затрат на обработку. Режим RichText не следует использовать, поскольку StyledText предоставляет почти все его функции за долю его стоимости.
Изображения
Изображения являются неотъемлемой частью любого пользовательского интерфейса. К сожалению, они также являются источником проблем из-за времени, необходимого для их загрузки, занимаемого ими объема памяти и способов их использования.
Асинхронная загрузка
Изображения часто довольно большие, поэтому важно убедиться, что загрузка изображения не блокирует поток пользовательского интерфейса. Установите свойство "asynchronous" элемента QML Image в true, чтобы включить асинхронную загрузку изображений из локальной файловой системы (удаленные изображения всегда загружаются асинхронно), где это не повлияет на эстетику пользовательского интерфейса.
Элементы изображения со свойством "asynchronous", установленным в true, будут загружать изображения в потоке низкого приоритета.
Явный размер источника
Если ваше приложение загружает большое изображение, но отображает его в элементе малого размера, установите свойство "sourceSize" в размер отображаемого элемента, чтобы гарантировать, что в памяти хранится уменьшенная версия изображения, а не большая.
Обратите внимание, что изменение sourceSize приведет к повторной загрузке изображения.
Избегайте композиции во время выполнения
Также помните, что вы можете избежать работы по композиции во время выполнения, предоставив предварительно составленный ресурс изображения с вашим приложением (например, предоставив элементы с эффектами тени).
Размещение элементов с якорями
Более эффективным способом размещения элементов относительно друг друга является использование якорей, а не привязок. Рассмотрим такое использование привязок для размещения 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. Хотя оптимальная реализация любой такой модели будет сильно зависеть от того, для какой задачи она предназначена, некоторые общие рекомендации следующие:
- Максимально используйте асинхронность
- Выполняйте все обработку в потоке (низкого приоритета) рабочего потока
- Группируйте операции бэкэнда, чтобы минимизировать (потенциально медленную) ввод-вывод и межпроцессное взаимодействие
- Используйте скользящий срез окна для кэширования результатов, параметры которого определяются с помощью профилирования
Важно отметить, что использование потока рабочего потока низкого приоритета рекомендуется для минимизации риска истощения потока 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 — это элемент, который позволяет динамически загружать и выгружать компоненты.
- Используя свойство «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 с прозрачными краями.
Shader
Тип ShaderEffect позволяет размещать код GLSL непосредственно в приложении Qt Quick с минимальными накладными расходами. Однако важно понимать, что фрагментная программа должна выполняться для каждого пикселя в отрисовываемой фигуре. При развертывании на устройствах с низкой производительностью и при большом количестве пикселей, охватываемых шейдером, фрагментный шейдер должен содержать несколько инструкций, чтобы избежать снижения производительности.
Шейдеры, написанные на GLSL, позволяют реализовывать сложные преобразования и визуальные эффекты, но их следует использовать с осторожностью. Использование ShaderEffectSource приводит к предварительной отрисовке сцены в FBO перед ее отображением. Эти дополнительные накладные расходы могут быть довольно дорогими.
Выделение и сборка памяти
Объем памяти, который будет выделен приложением, и способ выделения этой памяти являются очень важными соображениями. Помимо очевидных опасений по поводу исчерпания памяти на устройствах с ограниченным объемом памяти, выделение памяти в куче является довольно вычислительно затратной операцией, и определенные стратегии выделения могут привести к увеличению фрагментации данных по страницам. JavaScript использует управляемую кучу памяти, которая автоматически собирает мусор, и это обеспечивает некоторые преимущества, но также имеет некоторые важные последствия.
Приложение, написанное на QML, использует память как из C++ кучи, так и из автоматически управляемой кучи JavaScript. Разработчику приложения необходимо понимать нюансы каждого из них, чтобы максимизировать производительность.
Советы для разработчиков QML-приложений
Советы и предложения, содержащиеся в этом разделе, являются только рекомендациями и могут не быть применимы во всех случаях. Убедитесь, что вы тщательно тестируете и анализируете свое приложение с помощью эмпирических показателей, чтобы принять наилучшие решения.
Инициализация компонентов ленивым способом
Если ваше приложение состоит из нескольких представлений (например, нескольких вкладок), но требуется только одно одновременно, вы можете использовать ленивую инициализацию, чтобы свести к минимуму объем памяти, который необходимо выделить в любой момент. Дополнительную информацию см. в предыдущем разделе о ленивой инициализации.
Уничтожение неиспользуемых объектов
Если вы лениво инициализируете компоненты или динамически создаете объекты во время выполнения выражения JavaScript, часто лучше вручную destroy() их, чем ждать автоматической сборки мусора. Дополнительную информацию см. в предыдущем разделе о управлении жизненным циклом элементов.
Не вызывать вручную сборщик мусора
В большинстве случаев не рекомендуется вручную вызывать сборщик мусора, поскольку он заблокирует поток GUI на значительное время. Это может привести к пропуску кадров и рывкам в анимациях, чего следует избегать любой ценой.
Существуют некоторые случаи, когда ручное вызов сборщика мусора приемлем (и это более подробно описано в последующем разделе), но в большинстве случаев вызов сборщика мусора излишний и неэффективен.
Избегайте сложных связей
Помимо снижения производительности сложных связей (например, из-за необходимости входа в контекст выполнения JavaScript для выполнения оценки), они также занимают больше памяти как в C++ куче, так и в JavaScript куче, чем связи, которые могут быть оценены оптимизированным анализатором выражений связей QML.
Избегайте определения нескольких одинаковых неявных типов
Если QML-элемент имеет настроенное свойство, определенное в QML, оно становится собственным неявным типом. Это описано более подробно в последующем разделе. Если несколько одинаковых неявных типов определены встраиваемо в компоненте, будет потрачена память. В таком случае обычно лучше явно определить новый компонент, который можно повторно использовать.
Определение настраиваемого свойства часто может быть полезной оптимизацией производительности (например, для сокращения количества необходимых или повторно оцениваемых связей) или улучшить модульность и поддерживаемость компонента. В таких случаях использование пользовательских свойств рекомендуется. Однако, если новый тип используется более одного раза, его следует разделить на отдельный компонент (.qml-файл), чтобы сохранить память.
Использование существующих компонентов
Если вы рассматриваете возможность определения нового компонента, стоит убедиться, что такой компонент не существует в наборе компонентов для вашей платформы. В противном случае вы заставите движок QML генерировать и хранить данные типа для типа, который по существу является дубликатом другого уже существующего и, возможно, уже загруженного компонента.
Использование одиночных типов вместо скриптов библиотек с псевдонимами
Если вы используете скрипт библиотек с псевдонимами для хранения данных экземпляра во всем приложении, рассмотрите возможность использования типа одиночного объекта 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 2.3
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 приведет к значительному улучшению производительности при оценке этого выражения, но это связано с выделением временной переменной. В некоторых случаях эти компромиссы целесообразны (например, в случае выше, что почти всегда целесообразно), но в других случаях может быть лучше позволить обработке занять немного больше времени, чтобы избежать увеличения нагрузки на память системы.
В некоторых случаях влияние увеличения нагрузки на память может быть значительным. В некоторых ситуациях обмен использованием памяти на предполагаемое улучшение производительности может привести к увеличению страничного или кэш-замещения, вызывая существенное снижение производительности. Всегда необходимо тщательно проверять влияние компромиссов, чтобы определить, какое решение является лучшим в данной ситуации.
Для получения подробной информации о производительности кэша и компромиссах между памятью и временем, пожалуйста, обратитесь к отличной статье Ульриха Дреппера «Что каждый программист должен знать о памяти» (доступна по адресу http://ftp.linux.org.ua/pub/docs/developer/general/cpumemory.pdf по состоянию на 18 апреля 2012 г.), а для информации о специфических оптимизациях C++, пожалуйста, обратитесь к отличным руководствам Агнера Фога по оптимизации приложений C++ (доступны по адресу http://www.agner.org/optimize/ по состоянию на 18 апреля 2012 г.).
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.6/qtquick-performance.html