Spec-Zone.ru › Qt 5.14

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

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

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

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

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

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

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

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

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

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

Код JavaScript

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

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

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

Преобразование между некоторыми базовыми типами свойств (такими как свойства "string" и "url") также может быть дорогостоящим. Использование свойства с наилучшим соответствием спасёт от ненужного преобразования.

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

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

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

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

// bad.qml
import QtQuick 2.3

Item {
    width: 400
    height: 200
    Rectangle {
        id: rect
        anchors.fill: parent
        color: "blue"
    }

    function printValue(which, value) {
        console.log(which + " = " + value);
    }

    Component.onCompleted: {
        var t0 = new Date();
        for (var i = 0; i < 1000; ++i) {
            printValue("red", rect.color.r);
            printValue("green", rect.color.g);
            printValue("blue", rect.color.b);
            printValue("alpha", rect.color.a);
        }
        var t1 = new Date();
        console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
    }
}

Вместо этого мы можем разрешить общий базовый элемент только один раз в блоке:

// good.qml
import QtQuick 2.3

Item {
    width: 400
    height: 200
    Rectangle {
        id: rect
        anchors.fill: parent
        color: "blue"
    }

    function printValue(which, value) {
        console.log(which + " = " + value);
    }

    Component.onCompleted: {
        var t0 = new Date();
        for (var i = 0; i < 1000; ++i) {
            var rectColor = rect.color; // resolve the common base.
            printValue("red", rectColor.r);
            printValue("green", rectColor.g);
            printValue("blue", rectColor.b);
            printValue("alpha", rectColor.a);
        }
        var t1 = new Date();
        console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
    }
}

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

// better.qml
import QtQuick 2.3

Item {
    width: 400
    height: 200
    Rectangle {
        id: rect
        anchors.fill: parent
        color: "blue"
    }

    function printValue(which, value) {
        console.log(which + " = " + value);
    }

    Component.onCompleted: {
        var t0 = new Date();
        var rectColor = rect.color; // resolve the common base outside the tight loop.
        for (var i = 0; i < 1000; ++i) {
            printValue("red", rectColor.r);
            printValue("green", rectColor.g);
            printValue("blue", rectColor.b);
            printValue("alpha", rectColor.a);
        }
        var t1 = new Date();
        console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
    }
}

Привязки свойств

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

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

Следующий искусственный пример поясняет этот момент:

// bad.qml
import QtQuick 2.3

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

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

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

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

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

// good.qml
import QtQuick 2.3

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

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

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

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

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

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

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

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

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

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

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

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

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

signals:
    void qrealListPropertyChanged();

private:
    QList<qreal> m_list;
};

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

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

SequenceTypeExample {
    id: root
    width: 200
    height: 200

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

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

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

SequenceTypeExample {
    id: root
    width: 200
    height: 200

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

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

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

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

SequenceTypeExample {
    id: root

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

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

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

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

SequenceTypeExample {
    id: root

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

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

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

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

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

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

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

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

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

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

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

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

Изображения

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

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

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

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

Явное размер исходного изображения

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

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

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

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

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

Включите 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. Хотя оптимальная реализация любой такой модели в значительной степени зависит от задачи, которую она должна выполнить, следующие общие рекомендации являются полезными:

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

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

Тип QML ListModel

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

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

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

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

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

Элемент ListModel в QtQuick 2 значительно производительнее, чем в QtQuick 1. Улучшения производительности в основном связаны с предположениями о типе ролей в каждом элементе данной модели — если тип не изменяется, производительность кэширования значительно улучшается. Если тип может динамически изменяться от элемента к элементу, это оптимизация становится невозможной, и производительность модели будет на порядок хуже.

Поэтому динамическая типизация по умолчанию отключена; разработчик должен явно установить свойство «dynamicRoles» модели в значение true, чтобы включить динамическую типизацию (и получить соответствующее снижение производительности). Мы рекомендуем не использовать динамическую типизацию, если есть возможность перепроектировать приложение, чтобы этого избежать.

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

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

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

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

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

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

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

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

Анимации

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

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

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

Частицы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Дополнительную информацию об активных, но невидимых элементах см. в предстоящем разделе «Отрисовка».

Отрисовка

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

Обрезка

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

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

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

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

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

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

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

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

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

Шейдеры

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

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

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

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

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

Рекомендации для разработчиков приложений QML

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

Инициализация компонентов лениво

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

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

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

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

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

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

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

Помимо снижения производительности сложных привязок (например, из-за необходимости вхождения в контекст выполнения 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.14/qtquick-performance.html

Spec-Zone.ru

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