Spec-Zone.ru › Qt 5.13

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

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

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

Его можно переписать следующим образом:

// good.qml
import QtQuick 2.3

Item {
    id: root
    width: 200
    height: 200
    property int accumulatedValue: 0

    Text {
        anchors.fill: parent
        text: root.accumulatedValue.toString()
        onTextChanged: console.log("text binding re-evaluated")
    }

    Component.onCompleted: {
        var someData = [ 1, 2, 3, 4, 5, 20 ];
        var temp = accumulatedValue;
        for (var i = 0; i < someData.length; ++i) {
            temp = temp + someData[i];
        }
        accumulatedValue = temp;
    }
}

Рекомендации по работе со списками

Как отмечалось ранее, некоторые типы списков быстры (например, QList<int>, QList<qreal>, QList<bool>, QList<QString>, QStringList и QList<QUrl>), а другие значительно медленнее. Помимо использования этих типов там, где это возможно, вместо более медленных типов, существует ряд других семантик, связанных с производительностью, которые следует учитывать для достижения наилучшей производительности.

Во-первых, существуют две различные реализации типов списков: одна для случаев, когда список является Q_PROPERTY объекта QObject (назовем этот список ссылкой), и другая, когда список возвращается из Q_INVOKABLE функции объекта QObject (назовем этот список копией).

Список-ссылка считывается и записывается через QMetaObject::property() и, таким образом, читается и записывается как QVariant. Это означает, что изменение значения любого элемента списка из JavaScript приведет к выполнению трех шагов: весь список будет считан из объекта QObject (как QVariant, а затем преобразован в список соответствующего типа); элемент в указанном индексе будет изменен в этом списке; и весь список будет записан обратно в объект QObject (как QVariant).

Список-копия намного проще, так как фактический список хранится в данных ресурса JavaScript-объекта, поэтому цикл чтения/модификации/записи не происходит (вместо этого данные ресурса изменяются напрямую).

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

Предположим существование (и предварительную регистрацию в пространстве имен «Qt.example 1.0») следующего типа C++:

class SequenceTypeExample : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY (QList<qreal> qrealListProperty READ qrealListProperty WRITE setQrealListProperty NOTIFY qrealListPropertyChanged)

public:
    SequenceTypeExample() : QQuickItem() { m_list << 1.1 << 2.2 << 3.3; }
    ~SequenceTypeExample() {}

    QList<qreal> qrealListProperty() const { return m_list; }
    void setQrealListProperty(const QList<qreal> &list) { m_list = list; emit qrealListPropertyChanged(); }

signals:
    void qrealListPropertyChanged();

private:
    QList<qreal> m_list;
};

Следующий пример записывает в элементы списка-ссылки в тесном цикле, что приводит к плохой производительности:

// bad.qml
import QtQuick 2.3
import Qt.example 1.0

SequenceTypeExample {
    id: root
    width: 200
    height: 200

    Component.onCompleted: {
        var t0 = new Date();
        qrealListProperty.length = 100;
        for (var i = 0; i < 500; ++i) {
            for (var j = 0; j < 100; ++j) {
                qrealListProperty[j] = j;
            }
        }
        var t1 = new Date();
        console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
    }
}

Чтение и запись свойства объекта QObject во внутреннем цикле, вызванное выражением "qrealListProperty[j] = j", делает этот код очень неэффективным. Вместо этого функционально эквивалентный, но намного более быстрый вариант будет:

// good.qml
import QtQuick 2.3
import Qt.example 1.0

SequenceTypeExample {
    id: root
    width: 200
    height: 200

    Component.onCompleted: {
        var t0 = new Date();
        var someData = [1.1, 2.2, 3.3]
        someData.length = 100;
        for (var i = 0; i < 500; ++i) {
            for (var j = 0; j < 100; ++j) {
                someData[j] = j;
            }
            qrealListProperty = someData;
        }
        var t1 = new Date();
        console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
    }
}

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

Это необычный случай использования, с которым большинство клиентов не столкнутся, но стоит знать о нём, если вы занимаетесь чем-то подобным:

// bad.qml
import QtQuick 2.3
import Qt.example 1.0

SequenceTypeExample {
    id: root

    property int firstBinding: qrealListProperty[1] + 10;
    property int secondBinding: qrealListProperty[1] + 20;
    property int thirdBinding: qrealListProperty[1] + 30;

    Component.onCompleted: {
        var t0 = new Date();
        for (var i = 0; i < 1000; ++i) {
            qrealListProperty[2] = i;
        }
        var t1 = new Date();
        console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
    }
}

Обратите внимание, что даже если в цикле модифицируется только элемент с индексом 2, все три привязки будут переоценены, так как гранулярность сигнала изменения заключается в том, что всё свойство изменилось. Таким образом, добавление промежуточной привязки иногда может быть полезным:

// good.qml
import QtQuick 2.3
import Qt.example 1.0

SequenceTypeExample {
    id: root

    property int intermediateBinding: qrealListProperty[1]
    property int firstBinding: intermediateBinding + 10;
    property int secondBinding: intermediateBinding + 20;
    property int thirdBinding: intermediateBinding + 30;

    Component.onCompleted: {
        var t0 = new Date();
        for (var i = 0; i < 1000; ++i) {
            qrealListProperty[2] = i;
        }
        var t1 = new Date();
        console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
    }
}

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

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

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

Другие объекты JavaScript

Разные движки JavaScript предоставляют разные оптимизации. Движок JavaScript, используемый Qt Quick 2, оптимизирован для создания объектов и поиска свойств, но предоставляемые им оптимизации полагаются на определенные критерии. Если ваше приложение не соответствует критериям, движок JavaScript переключается на режим "медленной дорожки" с гораздо худшей производительностью. Поэтому всегда старайтесь соблюдать следующие критерии:

  • По возможности избегайте использования eval()
  • Не удаляйте свойства объектов

Общие элементы интерфейса

Элементы текста

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

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

Изображения

Изображения являются неотъемлемой частью любого пользовательского интерфейса. К сожалению, они также являются источником проблем из-за времени, необходимого для их загрузки, объёма занимаемой памяти и способа их использования.

Асинхронная загрузка

Изображения часто бывают достаточно большими, поэтому желательно убедиться, что загрузка изображения не блокирует поток пользовательского интерфейса. Установите свойство «асинхронный» элемента QML Image в значение true, чтобы включить асинхронную загрузку изображений из локальной файловой системы (удаленные изображения всегда загружаются асинхронно), где это не повлияет на эстетику пользовательского интерфейса.

Элементы изображения со свойством «асинхронный», установленным в значение true, будут загружать изображения в потоке низкого приоритета.

Явное задание размера источника

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

Обратите внимание, что изменение sourceSize приведет к перезагрузке изображения.

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

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

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

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

Отрисовка

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Тип QML ListModel

QML предоставляет тип ListModel, который можно использовать для передачи данных в ListView. Он должен быть достаточно эффективным для большинства случаев использования.

Заполнение в потоке обработки

ListModel элементы могут быть заполнены в потоке (низкого приоритета) в JavaScript. Разработчик должен явно вызвать «sync()» на ListModel изнутри WorkerScript, чтобы изменения были синхронизированы с основным потоком. См. документацию по WorkerScript для получения дополнительной информации.

Обратите внимание, что использование элемента WorkerScript приведет к созданию отдельного движка JavaScript (так как движок JavaScript работает на потоковой основе). Это приведет к увеличению использования памяти. Однако несколько элементов WorkerScript будут использовать один и тот же поток обработки, поэтому влияние на память использования второго или третьего элемента WorkerScript будет незначительным, если приложение уже использует один.

Не используйте динамические роли

Элемент ListModel в QtQuick 2 намного производительнее, чем в QtQuick 1. Улучшения производительности в основном связаны с предположениями о типе ролей в каждом элементе данной модели — если тип не изменяется, производительность кеширования значительно улучшается. Если тип может динамически меняться от элемента к элементу, эта оптимизация становится невозможной, и производительность модели ухудшится на порядок. Поэтому динамическая типизация по умолчанию отключена; разработчик должен явно установить свойство boolean «dynamicRoles» модели, чтобы включить динамическую типизацию (и понести соответствующее снижение производительности). Мы рекомендуем не использовать динамическую типизацию, если возможно перепроектировать приложение, чтобы избежать ее.

Представления

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

Следующий список хорошо описывает моменты, которые следует учитывать при проектировании делегата:

  • Чем меньше элементов в делегате, тем быстрее они создаются, а значит, тем быстрее можно прокручивать представление.
  • Минимизируйте количество привязок в делегате; в частности, используйте якоря вместо привязок для относительного позиционирования в делегате.
  • Избегайте использования элементов ShaderEffect в делегатах.
  • Никогда не включайте обрезку в делегате.

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

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

Визуальные эффекты

Qt Quick 2 включает несколько функций, которые позволяют разработчикам и дизайнерам создавать исключительно привлекательные пользовательские интерфейсы. Плавность и динамические переходы, а также визуальные эффекты могут быть эффективно использованы в приложении, но необходимо соблюдать осторожность при использовании некоторых функций в QML, поскольку они могут влиять на производительность.

Анимации

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

Избегайте выполнения JavaScript во время анимации. Например, следует избегать выполнения сложного выражения JavaScript для каждого кадра анимации свойства x.

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

Частицы

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

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

Более подробную информацию см. в Руководстве по производительности системы частиц.

Управление жизненным циклом элементов

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

Ленивая инициализация

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

Использование Loader

Loader — это элемент, который позволяет динамически загружать и выгружать компоненты.

  • Используя свойство «active» Loader, инициализацию можно отложить до тех пор, пока она не потребуется.
  • Используя перегруженную версию функции «setSource()», можно задать начальные значения свойств.
  • Установив свойство Loader asynchronous в значение true, также можно повысить плавность работы при создании компонента.

Использование динамического создания

Разработчики могут использовать функцию Qt.createComponent() для динамического создания компонента во время выполнения из JavaScript и затем вызывать createObject() для его создания. В зависимости от семантики владения, указанной в вызове, разработчик может потребоваться вручную удалить созданный объект. Дополнительную информацию см. в разделе Динамическое создание QML-объектов из JavaScript.

Уничтожение неиспользуемых элементов

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

Элемент, загруженный с помощью элемента Loader, можно освободить, сбросив свойство «source» или «sourceComponent» Loader, в то время как другие элементы можно явно освободить, вызвав destroy() для них. В некоторых случаях может быть необходимо оставить элемент активным, в этом случае его следует, по крайней мере, сделать невидимым.

Дополнительную информацию об активных, но невидимых элементах см. в следующей секции по рендерингу.

Отображение

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

Обрезка

Обрезка по умолчанию отключена и должна быть включена только при необходимости.

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

Обрезка внутри делегата особенно вредна и должна быть полностью исключена.

Перерисовка и невидимые элементы

Если у вас есть элементы, которые полностью закрыты другими (непрозрачными) элементами, лучше установить для их свойства «visible» значение false, иначе они будут бесполезно отображаться.

Аналогично, элементы, которые невидимы (например, вторая вкладка в виджете вкладок, пока отображается первая вкладка), но должны быть инициализированы во время запуска (например, если стоимость создания второй вкладки занимает слишком много времени, чтобы сделать это только при активации вкладки), должны иметь свойство «visible» установленное в false, чтобы избежать затрат на их отображение (хотя, как уже объяснялось, они по-прежнему будут нести затраты на любые анимации или вычисления связей, поскольку они все еще активны).

Прозрачный против непрозрачного

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

Изображение с одним полупрозрачным пикселем обрабатывается как полностью полупрозрачное, даже если оно в основном непрозрачное. То же самое относится к BorderImage с прозрачными краями.

Shader

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

Шейдеры, написанные на GLSL, позволяют создавать сложные преобразования и визуальные эффекты, но их следует использовать с осторожностью. Использование ShaderEffectSource приводит к предварительному рендерингу сцены в FBO перед отображением. Эти дополнительные накладные расходы могут быть довольно дорогими.

Выделение и сборка памяти

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

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

Советы разработчикам приложений QML

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

Инициализировать компоненты лениво

Если ваше приложение состоит из нескольких представлений (например, нескольких вкладок), но в любой момент времени требуется только одно, вы можете использовать ленивую инициализацию, чтобы свести к минимуму объем памяти, который вам нужно выделить в любой момент времени. Дополнительную информацию см. в предыдущем разделе о Ленивой инициализации.

Удалить неиспользуемые объекты

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

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

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

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

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

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

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

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

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

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

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

Используйте одиночные типы вместо сценариев библиотек pragma

Если вы используете скрипт библиотеки pragma для хранения данных экземпляра на уровне приложения, рассмотрите использование вместо этого синглтона типа QObject. Это должно улучшить производительность и уменьшить использование памяти JavaScript-кучи.

Выделение памяти в приложении QML

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

Куча C++ будет содержать:

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

JavaScript-куча будет содержать:

  • фиксированные и неизбежные накладные расходы самого движка JavaScript (включая встроенные типы JavaScript);
  • фиксированные и неизбежные накладные расходы нашей интеграции JavaScript (конструкторские функции загруженных типов, шаблоны функций и т. д.);
  • информацию о расположении по типу и другие внутренние данные типа, генерируемые движком JavaScript во время выполнения для каждого типа (см. примечание ниже, касающееся типов);
  • данные JavaScript на объект ("var" свойства, функции JavaScript и обработчики сигналов, а также неоптимизированные выражения связывания);
  • переменные, выделенные во время вычисления выражений.

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

Обратите внимание, что как движок QML, так и движок JavaScript автоматически генерируют собственные кэши данных типа об наблюдаемых типах. Каждый загруженный компонент приложения является отдельным (явным) типом, а каждый элемент (экземпляр компонента), определяющий собственные пользовательские свойства в QML, является неявным типом. Любой элемент (экземпляр компонента), не определяющий пользовательских свойств, движки JavaScript и QML рассматривают как принадлежащего к явным типу, определенному компонентом, а не к собственному неявным типу.

Рассмотрим следующий пример:

import QtQuick 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.13/qtquick-performance.html

Spec-Zone.ru

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