Spec-Zone.ru › Qt 5.15

Учет производительности и рекомендации

Учет временных характеристик

Как разработчик приложения, вы должны стремиться к тому, чтобы движок отрисовки обеспечивал стабильную частоту обновления 60 кадров в секунду. 60 кадров в секунду означают, что между каждым кадром существует приблизительно 16 миллисекунд для обработки, включая обработку, необходимую для загрузки примитивов отрисовки в графический процессор.

На практике это означает, что разработчик приложения должен:

  • использовать асинхронное, управляемое событиями программирование, где это возможно
  • использовать потоки обработки для выполнения значительной обработки
  • никогда не запускать цикл событий вручную
  • никогда не тратить более нескольких миллисекунд на кадр внутри блокирующих функций

Несоблюдение этих рекомендаций приведет к пропуску кадров, что окажет серьезное влияние на пользовательский опыт.

Примечание: Использование шаблона, который может показаться привлекательным, но категорически недопустимо, — это создание собственного цикла событий QEventLoop или вызов QCoreApplication::processEvents() для предотвращения блокировки в блоке кода C++, вызываемом из QML. Это опасно, потому что при входе в цикл событий в обработчике сигнала или привязке движок QML продолжает выполнение других привязок, анимаций, переходов и т. д. Эти привязки могут вызвать побочные эффекты, например, уничтожение иерархии, содержащей цикл событий.

Профилирование

Самый важный совет: используйте встроенный в Qt Creator профайлер QML. Знание того, где тратится время в приложении, позволит сосредоточиться на реальных проблемах, а не на потенциальных. Обратитесь к руководству Qt Creator для получения дополнительной информации о том, как использовать инструмент профилирования QML.

Определение самых часто используемых привязок или функций, в которых ваше приложение тратит больше всего времени, позволит вам решить, нужно ли оптимизировать проблемные области или перепроектировать некоторые детали реализации вашего приложения для повышения производительности. Попытка оптимизировать код без профилирования, скорее всего, приведет к незначительным, а не к существенным улучшениям производительности.

Код JavaScript

Большинство приложений QML содержат значительный объем кода JavaScript в виде динамических функций, обработчиков сигналов и выражений привязки свойств. Это, как правило, не проблема. Благодаря некоторым оптимизациям в движке QML, таким как оптимизации компилятора привязок, это (в некоторых случаях) может быть быстрее, чем вызов функции C++.

Преобразование типов

Одной из основных затрат при использовании JavaScript является то, что в большинстве случаев при доступе к свойству из типа QML создается объект JavaScript с внешним ресурсом, содержащим данные (или ссылку на них) на основе C++. В большинстве случаев это достаточно недорого, но в других случаях это может быть весьма дорогостоящим. Одним из примеров дорогостоящих преобразований является присвоение C++ QVariantMap Q_PROPERTY свойству QML «variant». Списки также могут быть дорогими, хотя последовательности определенных типов (QList целых чисел, qreal, bool, QString и QUrl) должны быть недорогими; другие типы списков связаны с дорогостоящими затратами на преобразование (создание нового массива JavaScript и добавление новых типов по одному с преобразованием каждого типа из экземпляра C++ в значение JavaScript).

Преобразование между некоторыми основными типами свойств (такими как свойства «строка» и «url») также может быть дорогостоящим. Использование наиболее соответствующего типа свойства позволит избежать ненужных преобразований.

Если вам необходимо экспонировать QVariantMap в QML, используйте свойство «var», а не «variant». В общем случае «property var» следует считать предпочтительнее «property variant» для всех случаев использования начиная с QtQuick 2.0 и новее (обратите внимание, что «property variant» помечен как устаревший), поскольку это позволяет хранить истинную ссылку JavaScript (что может сократить количество необходимых преобразований в определенных выражениях).

Разрешение свойств

Разрешение свойств занимает время. Хотя в некоторых случаях результат поиска можно кэшировать и повторно использовать, всегда лучше всего избегать выполнения ненужной работы, если это возможно.

В следующем примере у нас есть блок кода, который часто выполняется (в этом случае это содержимое явного цикла; но это может быть, например, часто вычисляемое выражение привязки), и в нем мы многократно разрешаем объект с идентификатором «rect» и его свойством «color»:

// bad.qml
import QtQuick 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");
    }
}

В приведенном выше примере при повторной оценке будет участвовать только промежуточная привязка, что приведет к значительному улучшению производительности.

Рекомендации по свойствам типа значения

Свойства типа значения (шрифт, цвет, вектор3d и т. д.) имеют аналогичные семантику свойств объекта 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 приведет к повторной загрузке изображения.

Избегайте композиции во время выполнения

Также помните, что вы можете избежать выполнения работы по композиции во время выполнения, предоставив предварительно составленный ресурс изображения вместе с приложением (например, предоставляя элементы с эффектами тени).

Избегайте сглаживания изображений

Включите image.smooth только в том случае, если это необходимо. Оно медленнее на некоторых устройствах, и оно не оказывает визуального эффекта, если изображение отображается в своём естественном размере.

Отрисовка

Избегайте многократной отрисовки одного и того же участка. Используйте Item в качестве корневого элемента вместо Rectangle, чтобы избежать многократной отрисовки фона.

Размещение элементов с помощью якорей

Более эффективным способом размещения элементов относительно друг друга является использование якорей, а не привязок. Рассмотрим этот пример использования привязок для размещения rect2 относительно rect1:

Rectangle {
    id: rect1
    x: 20
    width: 200; height: 200
}
Rectangle {
    id: rect2
    x: rect1.x
    y: rect1.y + rect1.height
    width: rect1.width - 20
    height: 200
}

Это достигается более эффективно с помощью якорей:

Rectangle {
    id: rect1
    x: 20
    width: 200; height: 200
}
Rectangle {
    id: rect2
    height: 200
    anchors.left: rect1.left
    anchors.top: rect1.bottom
    anchors.right: rect1.right
    anchors.rightMargin: 20
}

Размещение с помощью привязок (назначение выражений привязки к свойствам x, y, width и height визуальных объектов вместо использования якорей) относительно медленное, хотя и позволяет максимальную гибкость.

Если макет не динамический, наиболее производительным способом задания макета является статическая инициализация свойств x, y, width и height. Координаты элементов всегда относительны к их родителю, поэтому, если вы хотите иметь фиксированное смещение от координаты 0,0 родителя, не следует использовать якоря. В приведенном ниже примере дочерние объекты Rectangle находятся в одном и том же месте, но код с якорями не так эффективен по ресурсам, как код, использующий фиксированное позиционирование через статическую инициализацию:

Rectangle {
    width: 60
    height: 60
    Rectangle {
        id: fixedPositioning
        x: 20
        y: 20
        width: 20
        height: 20
    }
    Rectangle {
        id: anchorPositioning
        anchors.fill: parent
        anchors.margins: 20
    }
}

Модели и представления

Большинство приложений будут иметь по меньшей мере одну модель, предоставляющую данные представлению. Разработчики приложений должны быть осведомлены о некоторых аспектах семантики, чтобы добиться максимальной производительности.

Пользовательские модели C++

Часто бывает желательно написать собственную пользовательскую модель на C++ для использования с представлением в QML. Хотя оптимальная реализация любой такой модели будет сильно зависеть от решаемой задачи, некоторые общие рекомендации таковы:

  • Максимально использовать асинхронность
  • Выполнять все обработки в потоке низкого приоритета
  • Группировать операции обращений к данным для минимизации (возможно, медленных) операций ввода-вывода и межпроцессного взаимодействия
  • Использовать скользящее окно для кэширования результатов, параметры которого определяются с помощью профилирования

Важно отметить, что рекомендуется использовать поток низкого приоритета, чтобы свести к минимуму риск перегрузки потока графического интерфейса (что может привести к ухудшению восприятия производительности). Также помните, что механизмы синхронизации и блокировки могут значительно снижать производительность, поэтому следует избегать ненужных блокировок.

Тип 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() их, чем ждать автоматического сбора мусора. Дополнительную информацию см. в предыдущем разделе о управлении жизненным циклом элементов.

Не вызывайте сборщик мусора вручную

В большинстве случаев не рекомендуется вызывать сборщик мусора вручную, так как это заблокирует поток графического интерфейса на значительное время. Это может привести к пропускам кадров и рывкам в анимациях, чего следует избегать любой ценой.

Есть некоторые случаи, когда ручное вызов сборщика мусора приемлем (и это подробно объясняется в последующем разделе), но в большинстве случаев вызов сборщика мусора не нужен и контрпродуктивен.

Избегайте сложных связей

Помимо снижения производительности сложных связей (например, из-за необходимости входа в контекст выполнения JavaScript для выполнения вычислений), они также потребляют больше памяти как в C++ куче, так и в JavaScript куче, чем связи, которые могут быть вычислены оптимизированным анализатором выражений связей QML.

Избегайте определения нескольких одинаковых неявных типов

Если QML элемент имеет настраиваемое свойство, определённое в QML, он становится собственным неявным типом. Это подробно объясняется в последующем разделе. Если несколько одинаковых неявных типов определены встраиваемо в компоненте, будет потрачена память. В такой ситуации обычно лучше явно определить новый компонент, который затем можно использовать повторно.

Определение пользовательского свойства часто может быть полезной оптимизацией производительности (например, для сокращения количества необходимых или переоцениваемых связей) или улучшить модульность и поддерживаемость компонента. В этих случаях использование пользовательских свойств приветствуется. Однако новый тип, если он используется более одного раза, следует выделить в отдельный компонент (.qml файл), чтобы сохранить память.

Используйте существующие компоненты

Если вы рассматриваете определение нового компонента, стоит дважды проверить, существует ли такой компонент в наборе компонентов вашей платформы. В противном случае вы заставите движок QML генерировать и хранить данные типа для типа, который по сути является дубликатом другого существующего и, возможно, уже загруженного компонента.

Используйте типы-синглтоны вместо скриптов библиотек с пragma

Если вы используете скрипт библиотек с пragma для хранения данных экземпляра приложения, рассмотрите использование типа синглтона 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 приведет к значительному улучшению производительности при оценке этого выражения, но это включает выделение временной переменной. В некоторых случаях такие компромиссы целесообразны (например, как в случае выше, что почти всегда целесообразно), но в других случаях может быть лучше позволить обработке занять немного больше времени, чтобы избежать увеличения нагрузки на память системы.

В некоторых случаях влияние повышенной нагрузки на память может быть экстремальным. В некоторых ситуациях обмен использованием памяти на предполагаемое улучшение производительности может привести к увеличению страницы или кэша, что приведет к сильному снижению производительности. Всегда необходимо тщательно протестировать влияние компромиссов, чтобы определить, какое решение является лучшим в данной ситуации.

Для получения подробной информации о производительности кэша и компромиссах времени-памяти ознакомьтесь со следующими статьями:

  • Отличная статья Ульриха Дреппера «Что каждый программист должен знать о памяти» по адресу: https://people.freebsd.org/~lstewart/articles/cpumemory.pdf.
  • Отличные руководства Агнера Фога по оптимизации приложений C++ по адресу: http://www.agner.org/optimize/.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.15/qtquick-performance.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API