Spec-Zone.ru › Qt 6.0

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

Учет временных параметров

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

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

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

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

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

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

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

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

Код JavaScript

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

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

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

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

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

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

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

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

// bad.qml
import QtQuick

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

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

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

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

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») следующего типа 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
import Qt.example

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
import Qt.example

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
import Qt.example

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
import Qt.example

SequenceTypeExample {
    id: root

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

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

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

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

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

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

Общие соображения по производительности JavaScript, вытекающие из структуры языка, также применимы к QML. Прежде всего:

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

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

Схемы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Использовать типы singleton вместо скриптов прагмы библиотеки

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

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

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

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

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

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

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

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

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

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

import QtQuick

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-6.0/qtquick-performance.html

Spec-Zone.ru

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