Spec-Zone.ru › Qt 5.9

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

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

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

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

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

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

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

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

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

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

Код JavaScript

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

Привязки

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

Что следует избегать в выражениях привязки для максимальной оптимизируемости:

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

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

Немедленный контекст оценки можно суммировать следующим образом:

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

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

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

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

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

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

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

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

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

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

// bad.qml
import QtQuick 2.3

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

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

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

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

// good.qml
import QtQuick 2.3

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

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

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

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

// better.qml
import QtQuick 2.3

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

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

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

Свойства привязок

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

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

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

// bad.qml
import QtQuick 2.3

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

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

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

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

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

// good.qml
import QtQuick 2.3

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

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

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

Советы по последовательностям

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

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

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

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

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

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

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

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

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

signals:
    void qrealListPropertyChanged();

private:
    QList<qreal> m_list;
};

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

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

SequenceTypeExample {
    id: root
    width: 200
    height: 200

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

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

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

SequenceTypeExample {
    id: root
    width: 200
    height: 200

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

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

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

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

SequenceTypeExample {
    id: root

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

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

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

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

SequenceTypeExample {
    id: root

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

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

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

Советы по свойствам типа значение

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

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

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

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

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

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

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

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

Изображения

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

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

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

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

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

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

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

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

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

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

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

Рисование

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Будьте максимально асинхронными
  • Выполняйте все вычисления в потоке обработки низкого приоритета
  • Группируйте операции обработки данных, чтобы свести к минимуму ввод-вывод (ввод/вывод и 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 позволяет плавно интегрировать красивые эффекты частиц в пользовательские интерфейсы. Однако каждая платформа имеет разные возможности графического оборудования, и модуль Частицы не может ограничить параметры тем, что ваше оборудование может поддерживать без проблем. Чем больше частиц вы пытаетесь отобразить (и чем они больше), тем быстрее должно быть ваше графическое оборудование, чтобы отображать их со скоростью 60 кадров в секунду. Воздействие на большее количество частиц требует более быстрого процессора. Поэтому важно тщательно протестировать все эффекты частиц на вашей целевой платформе, чтобы откалибровать количество и размер частиц, которые вы можете отобразить со скоростью 60 кадров в секунду.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Отрисовка

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

Обрезка

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

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

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

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

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

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

Прозрачность против непрозрачности

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

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

Тени

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Если вы используете скрипт библиотек с пragma для хранения данных экземпляра на уровне всего приложения, рассмотрите возможность использования типа-синглтона 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 2.3

Item {
    id: root

    Rectangle {
        id: r0
        color: "red"
    }

    Rectangle {
        id: r1
        color: "blue"
        width: 50
    }

    Rectangle {
        id: r2
        property int customProperty: 5
    }

    Rectangle {
        id: r3
        property string customProperty: "hello"
    }

    Rectangle {
        id: r4
        property string customProperty: "hello"
    }
}

В предыдущем примере прямоугольники r0 и r1 не имеют пользовательских свойств, и поэтому движки JavaScript и QML считают их обоими одного типа. То есть, r0 и r1 оба считаются типа Rectangle. Прямоугольники r2, r3 и r4 каждый имеют пользовательские свойства и каждый считается разными (неявными) типами. Обратите внимание, что r3 и r4 каждый считается разными типами, даже если у них одинаковая информация о свойствах, просто потому, что пользовательское свойство не было объявлено в компоненте, которым они являются экземплярами.

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

Подробные соображения по выделению памяти

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

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

Дробление

Дробление — это проблема разработки на C++. Если разработчик приложения не определяет никаких типов C++ или плагинов, он может безопасно пропустить этот раздел.

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

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

Сборка мусора

JavaScript предоставляет сборку мусора. Память, выделенная в куче JavaScript (в отличие от кучи C++), принадлежит движку JavaScript. Движок периодически собирает все неиспользуемые данные в куче JavaScript.

Последствия сборки мусора

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

Вручную вызов сборщика мусора

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

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

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

Компромиссы между памятью и производительностью

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

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

Дополнительную информацию о производительности кэша и компромиссах между памятью и временем, пожалуйста, см. в отличной статье Ульриха Дреппера «Что каждый программист должен знать о памяти» (доступной по адресу http://ftp.linux.org.ua/pub/docs/developer/general/cpumemory.pdf по состоянию на 18 апреля 2012 г.), а информацию по оптимизациям, специфичным для C++, см. в отличных руководствах Агнера Фога по оптимизации приложений C++ (доступных по адресу http://www.agner.org/optimize/ по состоянию на 18 апреля 2012 г.).

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

Spec-Zone.ru

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