Рекомендации по производительности
Учет временных параметров
Разработчикам приложений необходимо стремиться к обеспечению стабильной частоты обновления 60 кадров в секунду. 60 кадров в секунду означает, что между каждым кадром имеется приблизительно 16 миллисекунд для обработки, включая обработку, необходимую для загрузки примитивов отрисовки в графическое оборудование.
На практике это означает, что разработчик приложения должен:
- использовать асинхронное, управляемое событиями программирование, где это возможно
- использовать потоки-рабочие для значительной обработки
- никогда не запускать цикл событий вручную
- никогда не тратить более пары миллисекунд в каждом кадре внутри блокирующих функций
Несоблюдение этих правил приведет к пропуску кадров, что окажет существенное влияние на пользовательский опыт.
Примечание: Шаблон, который может показаться привлекательным, но использовать никогда не следует, заключается в создании собственного цикла событий QEventLoop или вызове QCoreApplication::processEvents() для избежания блокирования в блоке кода C++, вызываемого из QML. Это опасно, поскольку при входе в цикл событий в обработчике сигналов или привязки движок QML продолжает выполнение других привязок, анимаций, переходов и т. д. Эти привязки могут вызвать побочные эффекты, например, уничтожение иерархии, содержащей ваш цикл событий.
Профилирование
Самый важный совет: используйте профилировщик QML, включенный в Qt Creator. Зная, где тратится время в приложении, вы сможете сосредоточиться на фактических проблемах, а не на потенциальных. Для получения дополнительной информации о том, как использовать инструмент профилирования QML, обратитесь к руководству Qt Creator.
Определение наиболее часто выполняемых привязок или функций, в которых ваше приложение тратит больше всего времени, позволит вам решить, нужно ли оптимизировать проблемные области или перепроектировать некоторые реализации вашего приложения для повышения производительности. Попытка оптимизировать код без профилирования скорее всего приведет к незначительному, а не существенному улучшению производительности.
Код JavaScript
Большинство приложений QML содержат значительный объем кода JavaScript в виде динамических функций, обработчиков сигналов и выражений привязки свойств. В целом, это не проблема. Благодаря некоторым оптимизациям в движке QML, таким как оптимизации компилятора привязок, в некоторых случаях это может быть быстрее, чем вызов функции C++. Однако необходимо следить за тем, чтобы случайно не вызывалась ненужная обработка.
Привязки
В QML существуют два типа привязок: оптимизированные и неоптимизированные. Рекомендуется сохранять выражения привязки максимально простыми, поскольку движок QML использует оптимизированный оценщик выражений привязки, который может вычислять простые выражения привязки, не переключаясь в полную среду выполнения JavaScript. Эти оптимизированные привязки оцениваются намного эффективнее, чем более сложные (неоптимизированные) привязки. Основным требованием для оптимизации привязок является то, что информация о типе каждого доступного символа должна быть известна во время компиляции.
Что следует избегать в выражениях привязки для максимальной оптимизации:
- объявление промежуточных переменных JavaScript
- доступ к свойствам "var"
- вызов функций JavaScript
- создание замыканий или определение функций внутри выражения привязки
- доступ к свойствам за пределами непосредственной области оценки
- запись в другие свойства в качестве побочных эффектов
Привязки работают быстрее, когда они знают тип объектов и свойств, с которыми работают. Это означает, что поиск свойств, не являющихся окончательными, в выражении привязки может быть медленнее в некоторых случаях, когда тип искомого свойства может быть изменен (например, производным типом).
Непосредственная область оценки можно обобщить, сказав, что она содержит:
- свойства объекта области выражения (для выражений привязки это объект, к которому относится привязка свойства)
- идентификаторы любых объектов в компоненте
- свойства корневого элемента в компоненте
Идентификаторы объектов из других компонентов и свойства таких объектов, а также символы, определенные или включенные из импорта JavaScript, не находятся в непосредственной области оценки, и поэтому привязки, которые обращаются к этим элементам, не будут оптимизированы.
Обратите внимание, что если движок QML не может оптимизировать привязку с помощью оптимизированного оценщика выражений привязки и, следовательно, должен вычислять ее в полной среде JavaScript, некоторые из перечисленных выше советов больше не будут применяться. Например, в очень сложном выражении привязки иногда может быть полезно кэшировать результат разрешения свойства во временной переменной JavaScript.
Преобразование типов
Одна из основных затрат использования JavaScript заключается в том, что в большинстве случаев при доступе к свойству из типа QML создается объект JavaScript с внешним ресурсом, содержащим данные на основе C++ (или ссылку на них). В большинстве случаев это довольно недорого, но в других случаях это может быть довольно дорого. Пример дорогостоящего преобразования — присвоение QVariantMap C++ Q_PROPERTY QML-свойству "variant". Списки также могут быть дорогими, хотя последовательности определенных типов (QList int, 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 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(), если это вообще возможно
- Не удаляйте свойства объектов
Общие элементы интерфейса
Элементы текста
Вычисление макетов текста может быть медленной операцией. Вместо 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 {
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. Хотя оптимальная реализация любой такой модели будет сильно зависеть от конкретного случая, который она должна выполнить, некоторые общие рекомендации следующие:
- Максимально используйте асинхронность
- Выполняйте все обработку в потоке работ (низкого приоритета)
- Группируйте операции на стороне сервера, чтобы минимизировать (потенциально медленные) операции ввода-вывода и IPC
- Используйте скользящий срез окна для кеширования результатов, параметры которого определяются с помощью профилирования
Важно отметить, что рекомендуется использовать поток работы низкого приоритета, чтобы минимизировать риск голодания потока графического интерфейса (что может привести к ухудшению воспринимаемой производительности). Кроме того, помните, что механизмы синхронизации и блокировки могут значительно снизить производительность, поэтому следует избегать ненужных блокировок.
Тип 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 кадров в секунду.
Следует отметить, что система частиц может быть отключена при неиспользовании (например, на невидимом элементе), чтобы избежать ненужного моделирования.
Дополнительную информацию см. в Руководстве по производительности системы частиц.
Управление жизненным циклом элемента
Разделив приложение на простые модульные компоненты, каждый из которых содержится в отдельном файле 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() их, чем ждать автоматической сборки мусора. Дополнительную информацию см. в предыдущем разделе о управлении жизненным циклом элемента.
Не вызывать сборщик мусора вручную
В большинстве случаев не следует вызывать сборщик мусора вручную, так как это блокирует поток GUI на значительный период времени. Это может привести к пропуску кадров и рывковым анимациям, чего следует избегать любой ценой.
Есть некоторые случаи, когда ручное вызов сборщика мусора приемлем (и это подробно объяснено в последующем разделе), но в большинстве случаев вызов сборщика мусора не требуется и контрпродуктивен.
Избегайте сложных привязок
Помимо снижения производительности сложных привязок (например, из-за необходимости вхождения в контекст выполнения JavaScript для выполнения оценки), они также занимают больше памяти как в C++ куче, так и в куче JavaScript, чем привязки, которые могут быть оценены оптимизированным оценщиком выражений привязки QML.
Избегайте определения нескольких идентичных неявных типов
Если элемент QML имеет пользовательское свойство, определенное в QML, он становится своим собственным неявным типом. Это подробно описано в последующем разделе. Если несколько одинаковых неявных типов определены в строке в компоненте, будет потрачена некоторой память. В этой ситуации обычно лучше явно определить новый компонент, который можно затем повторно использовать.
Определение пользовательского свойства часто может быть полезной оптимизацией производительности (например, для уменьшения количества необходимых или повторно оцениваемых привязок) или улучшить модульность и поддерживаемость компонента. В этих случаях рекомендуется использовать пользовательские свойства. Однако новый тип, если он используется более одного раза, следует разделить на собственный компонент (.qml файл), чтобы сохранить память.
Использовать существующие компоненты
Если вы планируете определять новый компонент, стоит дважды проверить, не существует ли такой компонент в наборе компонентов для вашей платформы. В противном случае вы заставите движок QML генерировать и хранить данные типа для типа, который по сути является дубликатом другого существующего и потенциально уже загруженного компонента.
Использовать типы singleton вместо скриптов библиотек pragma
Если вы используете скрипт библиотеки pragma для хранения данных экземпляра на уровне всего приложения, рассмотрите использование типа QObject singleton вместо этого. Это должно привести к лучшей производительности и к меньшему использованию памяти кучи 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/qt-5.12/qtquick-performance.html