QMLパフォーマンスに関する考察と提案

タイミングに関する考察

アプリケーション開発者としては、レンダリングエンジンが一貫して60フレーム/秒のリフレッシュレートを達成できるように努力しなければなりません。60 FPSとは、各フレーム間に約16ミリ秒の処理時間があることを意味し、これには描画プリミティブをグラフィックスハードウェアにアップロードするための処理も含まれます。

これは、描画プリミティブをグラフィックス・ハードウェアにアップロードするのに必要な処理を含む:

  • 可能な限り非同期、イベント駆動プログラミングを使用する。
  • 重要な処理にはワーカースレッドを使用する。
  • 手動でイベントループを回さない。
  • ブロッキング関数の中で1フレームあたり数ミリ秒以上使わない。

これを怠ると、フレームがスキップされ、ユーザー・エクスペリエンスに劇的な影響を与えます。

注意: QMLから呼び出されるC++のコードブロック内でブロッキングを回避するために、QEventLoop を自作したり、QCoreApplication::processEvents()を呼び出したりすることは、 魅力的ではありますが、決して使ってはいけないパターンです。なぜなら、シグナルハンドラやバインディングでイベントループが発生すると、QMLエンジンは他のバインディングやアニメーション、トランジションなどを実行し続けるからです。これらのバインディングは副作用を引き起こし、例えばイベントループを含む階層を破壊してしまう可能性があります。

プロファイリング

Qt Creator に含まれている QML プロファイラを使うことです。アプリケーションのどこに時間が費やされているかを知ることで、潜在的に存在する問題点ではなく、実際に存在する問題点に焦点を当てることができます。QML プロファイリングツールの使い方については、Qt Creator のマニュアルを参照してください。

どのバインディングが最も頻繁に実行されているのか、あるいはアプリケーションがどの関数に最も多くの時間を費やしているのかを判断することで、問題箇所を最適化する必要があるのか、あるいはパフォーマンスを向上させるためにアプリケーションの実装の詳細を再設計する必要があるのかを判断することができます。プロファイリングを行わずにコードを最適化しようとすると、大幅なパフォーマンス改善ではなく、ごくわずかな改善にとどまる可能性が高いです。

JavaScriptコード

ほとんどのQMLアプリケーションには、動的関数、シグナルハンドラ、プロパティバインディング式などのJavaScriptコードが大量に含まれています。これは一般的には問題ではありません。バインディングコンパイラなどQMLエンジンの最適化により、C++関数を呼び出すよりも高速に動作する場合もあります。しかし、不必要な処理が誤って実行されないように注意する必要があります。

型変換

JavaScriptを使用する際の大きなコストの1つは、QML型のプロパティにアクセスする際、ほとんどの場合、基礎となるC++データ(またはその参照)を含む外部リソースを持つJavaScriptオブジェクトが生成されることです。たいていの場合、この処理はかなり安価ですが、かなり高価になる場合もあります。高価な例としては、C++ のQVariantMap Q_PROPERTY を QML の "variant" プロパティに代入することが挙げられます。リストもまた高価になる可能性がありますが、特定の型のシーケンス(QList of int, qreal, bool,QString, andQUrl )は安価であるべきです。他のリスト型は高価な変換コスト(新しいJavaScript Arrayを作成し、C++型インスタンスからJavaScript値への型ごとの変換を行いながら、新しい型を1つずつ追加する)を伴います。

いくつかの基本的なプロパティタイプ("string "プロパティや "url "プロパティなど)間の変換にもコストがかかります。最も近いプロパティタイプを使用することで、不必要な変換を避けることができます。

もしQVariantMap を QML に公開しなければならない場合は、"variant" プロパティではなく、"var" プロパティを使用してください。一般的に、"property var "は、QtQuick 2.0以降のすべてのユースケースにおいて、"property variant "よりも優れていると考えるべきです("property variant "は時代遅れとマークされていることに注意してください)。なぜなら、"property var "は、真のJavaScript参照を格納することができるからです(これにより、特定の式で必要な変換の数を減らすことができます)。

プロパティの解決

プロパティの解決には時間がかかります。ルックアップの結果をキャッシュして再利用できる場合もありますが、可能であれば、不要な作業を完全に行わないようにするのが常に最善です。

次の例では、頻繁に実行されるコードのブロックがあり(この場合は明示的なループの内容ですが、一般的に評価されるバインディング式などでもかまいません)、その中で "rect "idとその "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 "プロパティ・バインディングを6回再評価させる(その結果、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>、QStringListQList<QUrl>)、他の型はかなり遅くなります。遅い型の代わりに可能な限りこれらの型を使用することはもちろんですが、最高のパフォーマンスを達成するために注意しなければならないパフォーマンス関連のセマンティクスがいくつかあります。

まず、シーケンス型には2つの異なる実装があります。1つは、シーケンスがQObjectQ_PROPERTY である場合(これを参照シーケンスと呼びます)、もう1つは、シーケンスがQObjectQ_INVOKABLE 関数から返される場合(これをコピーシーケンスと呼びます)です。

参照シーケンスはQMetaObject::property() を介して読み書きされるため、QVariant として読み書きされます。 つまり、JavaScript からシーケンス内の要素の値を変更すると、3つのステップが発生します。QObject からシーケンス全体を読み込み(QVariant として、正しい型のシーケンスにキャストします)、指定されたインデックスの要素を変更し、QObject にシーケンス全体を書き戻します(QVariant )。

コピーシーケンスは、実際のシーケンスがJavaScriptオブジェクトのリソースデータに格納されているため、読み取り/変更/書き込みのサイクルが発生せず(代わりに、リソースデータが直接変更されます)、はるかに単純です。

したがって、参照シーケンスの要素への書き込みは、コピーシーケンスの要素への書き込みよりもはるかに遅くなります。実際、N要素の参照シーケンスの1つの要素に書き込むことは、N要素のコピーシーケンスをその参照シーケンスに割り当てることと同じコストです。

以下の C++ 型が存在する("Qt.example" 名前空間に事前に登録されている)と仮定します:

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");
    }
}

"qrealListProperty[j] = j" 式による内部ループでのQObject プロパティの読み書きが、このコードを非常に非最適なものにしています。代わりに、機能的には同等だがはるかに高速なものがある:

// 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の要素だけが変更されたとしても、変更シグナルの粒度はプロパティ全体が変更されたということなので、3つのバインディングはすべて再評価されることに注意してください。そのため、中間バインディングを追加することが有益な場合もあります:

// 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");
    }
}

上記の例では、中間バインディングのみが毎回再評価され、パフォーマンスが大幅に向上します。

値タイプのヒント

値型プロパティ(font、color、vector3dなど)は、シーケンス型プロパティと同様のQObject プロパティを持ち、通知セマンティクスを変更します。そのため、上記のシーケンスに関するヒントは、値型プロパティにも当てはまります。通常、値型ではあまり問題になりませんが(通常、値型のサブプロパティの数はシーケンスの要素の数よりもはるかに少ないからです)、不必要に再評価されるバインディングの数が増えれば、パフォーマンスに悪影響を及ぼします。

一般的なパフォーマンスのヒント

言語設計に起因するJavaScriptの一般的な性能に関する考慮事項は、QMLにも当てはまります。最も顕著なのは

  • 可能な限り eval() の使用を避ける。
  • オブジェクトのプロパティを削除しない

共通のインターフェース要素

テキスト要素

テキストレイアウトの計算には時間がかかります。可能な限り、StyledText の代わりにPlainText を使用してください。PlainText を使えない場合(画像を埋め込む必要があったり、タグを使ってテキスト全体ではなく文字の範囲に特定の書式(太字、斜体など)を指定する必要がある場合)は、StyledText を使うべきです。

このモードは解析コストがかかるので、テキストがStyledText の可能性がある(しかしおそらくそうではない)場合にのみAutoText を使うべきです。RichText モードは使うべきではありません。StyledText は、そのわずかなコストでほとんどすべての機能を提供するからです。

画像

画像はどのようなユーザーインターフェースにも欠かせないものです。残念ながら、読み込みにかかる時間、消費するメモリの量、使用方法のために、大きな問題の原因にもなっています。

非同期ロード

画像はかなり大きいことが多いので、画像の読み込みがUIスレッドをブロックしないようにするのが賢明です。QML Image 要素の "asynchronous" プロパティをtrue に設定することで、ローカルファイルシステムからの画像の非同期読み込みが可能になります(リモート画像は常に非同期で読み込まれます)。

非同期」プロパティがtrue に設定された Image 要素は、優先度の低いワーカースレッドで画像を読み込みます。

明示的なソースサイズ

アプリケーションが大きな画像を読み込み、小さなサイズの要素に表示する場合、"sourceSize" プロパティをレンダリングされる要素のサイズに設定し、大きな画像ではなく、小さなサイズの画像がメモリに保持されるようにします。

sourceSizeを変更すると、画像が再読み込みされるので注意してください。

実行時の合成を避ける

また、アプリケーションで合成済みの画像リソースを提供することで、実行時の合成作業を回避できることも覚えておいてください(たとえば、シャドウ効果を持つ要素を提供する場合など)。

画像のスムージングを避ける

必要な場合のみ、image.smooth 。ハードウェアによっては遅くなりますし、画像が自然な大きさで表示される場合には視覚的な効果はありません。

絵画

同じ領域を何度も描画することは避けてください。RectangleではなくItemをルートエレメントとして使用することで、背景を何度も描画することを避けることができます。

アンカーで要素を配置する

アイテムを相対的に配置するには、バインディングではなくアンカーを使用する方が効率的です。バインディングを使って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++モデル

QMLのビューで使用する独自のカスタムモデルをC++で記述することが望ましい場合がよくあります。そのようなモデルの最適な実装は、そのモデルが満たすべきユースケースに大きく依存しますが、いくつかの一般的なガイドラインを以下に示します:

  • 可能な限り非同期であること
  • すべての処理を(優先度の低い)ワーカースレッドで行う。
  • バックエンドの処理をバッチ化し、(潜在的に遅い)I/OとIPCを最小化する。

低優先度のワーカースレッドを使用することは、GUIスレッドを飢餓状態にするリスクを最小化するために推奨されることに注意することが重要です(その結果、知覚されるパフォーマンスが悪化する可能性があります)。また、同期とロックの仕組みはパフォーマンス低下の重大な原因となる可能性があるため、不必要なロックは避けるように注意する必要があることを覚えておいてください。

リストモデル QML型

QMLでは、ListView にデータを供給するためのListModel 型を提供しています。この型を正しく使用する限り、ほとんどのユースケースで十分であり、比較的高いパフォーマンスを発揮します。

ワーカースレッド内でのデータ投入

ListModel 要素は、JavaScriptの(優先順位の低い)ワーカースレッドで入力することができます。開発者は 内から の "sync()" を明示的に呼び出して、変更をメインスレッドに同期させる必要があります。詳しくは のドキュメントを参照してください。WorkerScript ListModel WorkerScript

WorkerScript 要素を使用すると、別の JavaScript エンジンが作成されることに注意してください(JavaScript エンジンはスレッド単位であるため)。その結果、メモリ使用量が増加します。しかし、複数のWorkerScript 要素はすべて同じワーカースレッドを使用するため、アプリケーションがすでにWorkerScript 要素を使用している場合、2 つ目、3 つ目の 要素を使用することによるメモリへの影響はごくわずかです。

ダイナミックロールを使わない

QtQuick 2のListModel 要素は、QtQuick 1よりもはるかに性能が向上しています。パフォーマンスの向上は主に、与えられたモデル内の各要素内のロールのタイプに関する仮定からもたらされます。タイプが変更されない場合、キャッシュのパフォーマンスは劇的に向上します。もし型が要素ごとに動的に変わる可能性がある場合、この最適化は不可能になり、モデルのパフォーマンスは桁違いに悪くなります。

したがって、動的な型付けはデフォルトでは無効になっています。開発者は、動的な型付けを有効にするために、モデルのブール値「dynamicRoles」プロパティを特別に設定する必要があります(そして、それに伴うパフォーマンスの低下を被ることになります)。動的型付けを避けるためにアプリケーションを再設計できる場合は、動的型付けを使用しないことをお勧めします。

ビュー

ビューのデリゲートはできるだけシンプルに保つべきです。デリゲートには、必要な情報を表示するのに十分な QML を記述してください。すぐに必要でない追加機能(例えば、クリックされたときに詳細な情報を表示する場合など)は、必要になるまで作成すべきではありません(遅延初期化に関する次のセクションを参照してください)。

以下のリストは、デリゲートを設計する際に注意すべき点をまとめたものです:

  • デリゲート内の要素が少なければ少ないほど、より速く作成でき、ビューのスクロールも速くなります。
  • 特に、デリゲート内の相対的な位置決めには、バインディングではなくアンカーを使用してください。
  • デリゲート内でShaderEffect 要素を使用することは避けてください。
  • デリゲートでクリッピングを有効にしないでください。

ビューのcacheBuffer プロパティを設定して、可視領域外のデリゲートの非同期作成とバッファリングを許可することができます。cacheBuffer を使用することは、非自明であり、1 フレーム内に作成される可能性が低いビューデリゲートに対して推奨されます。

cacheBuffer は、追加のデリゲートをメモリ内に保持することに留意してください。したがって、cacheBuffer を利用することで得られる価値は、追加のメモリ使用量とのバランスを取る必要があります。cacheBuffer 、まれにスクロール時のフレームレートが低下することがあります。

視覚効果

Qt Quick 2には、開発者やデザイナーが非常に魅力的なユーザーインターフェイスを作成できるようにする機能がいくつか含まれています。流動性や動的なトランジション、視覚効果は、アプリケーションに大きな効果をもたらしますが、QMLのいくつかの機能を使用する際には、パフォーマンスに影響を与える可能性があるため、いくつかの注意が必要です。

アニメーション

一般的に、プロパティをアニメーション化すると、そのプロパティを参照するバインディングが再評価されます。通常はこれが望ましいのですが、アニメーションを実行する前にバインディングを無効にしておき、アニメーションが完了したらバインディングを再割り当てする方がよい場合もあります。

アニメーション中にJavaScriptを実行することは避けてください。例えば、xプロパティのアニメーションの各フレームに対して複雑なJavaScript式を実行することは避けるべきです。

スクリプトアニメーションはメインスレッドで実行されるため、開発者は特に注意する必要があります(そのため、完了までに時間がかかりすぎると、フレームがスキップされる可能性があります)。

パーティクル

Qt Quick Particles モジュールにより、美しいパーティクル効果をユーザーインターフェイスにシームレスに統合できます。しかし、プラットフォームごとにグラフィックハードウェアの性能は異なり、Particlesモジュールはハードウェアが優雅にサポートできる範囲にパラメータを制限することができません。レンダリングしようとするパーティクルの数が多ければ多いほど(そして大きければ大きいほど)、60 FPSでレンダリングするためにはグラフィックハードウェアの高速化が必要になります。より多くのパーティクルに影響を与えるには、より高速なCPUが必要です。したがって、ターゲットプラットフォームですべてのパーティクルエフェクトを慎重にテストし、60 FPSでレンダリングできるパーティクルの数とサイズを調整することが重要です。

パーティクルシステムは、不要なシミュレーションを避けるために、使用していないときは無効にすることができます(たとえば、非可視要素など)。

詳細については、パーティクルシステムパフォーマンスガイドを参照してください。

エレメントのライフタイムの制御

アプリケーションをシンプルなモジュール式のコンポーネントに分割し、それぞれを 1 つの QML ファイルに含めることで、アプリケーションの起動時間を短縮し、メモリ使用量をより適切に制御できるようになります。

遅延初期化

QMLエンジンは、コンポーネントの読み込みや初期化でフレームがスキップされないようにするため、いくつかのトリッキーな処理を行います。しかし、起動時間を短縮するためには、必要のない処理を行わず、必要な処理まで遅延させることに勝る方法はありません。これを実現するには、Loader 、コンポーネントを動的に作成する方法があります。

ローダーの使用

ローダーは、コンポーネントの動的なロードとアンロードを可能にする要素です。

  • ローダーの「active」プロパティを使用すると、初期化を必要な時まで遅らせることができます。
  • setSource()」関数のオーバーロード版を使用すると、プロパティの初期値を指定できます。
  • Loaderasynchronous プロパティを true に設定すると、コンポーネントがインスタンス化される間の流動性が向上します。

動的作成の使用

開発者は Qt.createComponent() 関数を使用して、JavaScript 内から実行時に動的にコンポーネントを作成し、createObject() を呼び出してインスタンス化することができます。呼び出し時に指定される所有権のセマンティクスによっては、開発者は作成したオブジェクトを手動で削除しなければならないかもしれません。詳しくはDynamic QML Object Creation from JavaScriptを参照してください。

未使用要素の破棄

非可視要素の子要素であるために不可視となっている要素 (例えば、タブウィジェットの2番目のタブで、1番目のタブが表示されている場合など) は、ほとんどの場合、遅延的に初期化され、使用されなくなった時点で削除されるべきです。

Loader要素でロードされたアイテムは、Loaderの "source "または "sourceComponent "プロパティをリセットすることで解放できますが、他のアイテムは明示的にdestroy()を呼び出すことで解放できます。場合によっては、アイテムをアクティブなままにしておく必要がありますが、その場合は少なくとも不可視にする必要があります。

アクティブだが不可視の要素については、レンダリングのセクションを参照してください。

レンダリング

QtQuick 2 のレンダリングに使用されるシーングラフにより、高度にダイナミックでアニメーション化されたユーザーインターフェイスを、60 FPS で流れるようにレンダリングすることができます。しかし、レンダリングのパフォーマンスを劇的に低下させるものがいくつかあるので、開発者は可能な限りこれらの落とし穴を避けるように注意する必要があります。

クリッピング

クリッピングはデフォルトでは無効になっており、必要な場合のみ有効にする必要があります。

クリッピングは視覚効果であり、最適化ではありません。レンダラーの複雑さを(軽減するのではなく)増加させます。クリッピングが有効な場合、アイテムは自身のペイントとその子のペイントをバウンディング rectangle にクリップします。これにより、レンダラーが要素の描画順序を自由に並べ替えることができなくなり、シーングラフのトラバーサルが最適化されません。

デリゲート内でのクリッピングは特に良くないので、絶対に避けるべきです。

過剰描画と不可視要素

他の(不透明な)要素で完全に覆われている要素がある場合、その「visible」プロパティをfalse に設定するのが最善です。

同様に、不可視の要素(例えば、タブ・ウィジェットの2番目のタブで、1番目のタブが表示されている場合)であっても、起動時に初期化する必要がある場合(例えば、2番目のタブのインスタンス化に時間がかかりすぎて、タブがアクティブになったときにしか初期化できない場合)は、描画コストを避けるために、"visible" プロパティをfalse に設定する必要があります(ただし、先に説明したように、まだアクティブなので、アニメーションやバインディングの評価コストは発生します)。

半透明と不透明

一般的に、不透明なコンテンツは半透明よりも描画速度が速いです。その理由は、半透明のコンテンツはブレンドが必要であり、レンダラーが不透明なコンテンツをより最適化できる可能性があるからです。

半透明のピクセルが1つある画像は、ほとんどが不透明でも完全に半透明として扱われます。エッジが透明なBorderImage も同様です。

シェーダー

ShaderEffect タイプを使うと、Qt Quick アプリケーションに GLSL コードをインラインで配置することができます。しかし、フラグメントプログラムは、レンダリングされた形状のすべてのピクセルに対して実行する必要があることを理解することが重要です。ローエンドハードウェアに配置し、シェーダが大量のピクセルをカバーする場合、パフォーマンス低下を避けるためにフラグメントシェーダを数命令に抑える必要があります。

GLSL で書かれたシェーダは、複雑な変形や視覚効果を書くことができますが、 使用には注意が必要です。ShaderEffectSource を使うと、描画する前にシーンが FBO にプリレンダリングされます。この余分なオーバーヘッドは、かなり高くつくことがあります。

メモリ割り当てとコレクション

アプリケーションによって割り当てられるメモリの量と、そのメモリの割り当て方法は、非常に重要な考慮事項です。メモリに制約のあるデバイスでのメモリ不足の状態についての明らかな懸念は別として、ヒープ上でのメモリの割り当てはかなり計算量の多い操作であり、ある種の割り当て戦略はページ間のデータの断片化を増やす結果になりかねません。JavaScriptは自動的にガベージコレクションされる管理されたメモリヒープを使用しており、これにはいくつかの利点がありますが、重要な意味合いもあります。

QML で書かれたアプリケーションは、C++ のヒープと自動的に管理される JavaScript のヒープの両方からメモリを使用します。アプリケーション開発者は、パフォーマンスを最大化するために、それぞれの微妙な違いを認識しておく必要があります。

QMLアプリケーション開発者のためのヒント

このセクションに記載されているヒントや提案は、あくまでもガイドラインであり、すべての状況に当てはまるとは限りません。可能な限り最良の判断を下すために、経験的な指標を用いてアプリケーションを注意深くベンチマークし、分析するようにしてください。

コンポーネントを遅延的にインスタンス化し、初期化する

アプリケーションが複数のビュー(例えば、複数のタブ)で構成されているが、一度に必要なのは1つだけである場合、遅延インスタンス化を使用することで、任意の時点で割り当てが必要なメモリ量を最小限に抑えることができます。詳しくは、遅延初期化のセクションを参照してください。

未使用オブジェクトの破棄

コンポーネントを遅延ロードしたり、JavaScript の式の中で動的にオブジェクトを作成したりする場合、自動的なガベージコレクションを待つよりも手動でdestroy() した方がよいことがよくあります。詳しくは「要素の寿命の制御」をご覧ください。

ガベージコレクタを手動で起動しない

ほとんどの場合、ガベージコレクタを手動で起動するのは賢明ではありません。その結果、フレームが飛んだり、アニメーションがぎこちなくなったりすることがあります。

ガベージコレクタを手動で起動してもよい場合もありますが(これについては次のセクションで詳しく説明します)、ほとんどの場合、ガベージコレクタの起動は不要で逆効果です。

同一の暗黙の型を複数定義することは避ける

QML要素にカスタムプロパティが定義されている場合、そのプロパティは暗黙の型となります。これについては、次の章で詳しく説明します。同じ暗黙的な型がインラインで複数定義されている場合、メモリが浪費されます。このような場合、通常は明示的に新しいコンポーネントを定義し、それを再利用する方がよいでしょう。

カスタムプロパティを定義することで、パフォーマンスの最適化(たとえば、必要なバインディングの数や再評価の数を減らすなど)になったり、コンポーネントのモジュール性や保守性が向上したりすることがよくあります。このような場合、カスタムプロパティの使用が推奨される。ただし、新しい型が複数回使用される場合は、メモリを節約するために、独自のコンポーネント(.qmlファイル)に分割する必要があります。

既存のコンポーネントの再利用

新しいコンポーネントを定義することを検討している場合、そのようなコンポーネントがあなたのプラットフォームのコンポーネントセットにすでに存在しないことを再確認する価値があります。そうでない場合、QMLエンジンに型データを生成・保存させることになりますが、この型データは本質的に既存のコンポーネントと重複しており、すでにロードされている可能性があります。

プラグマ・ライブラリ・スクリプトの代わりにシングルトン型を使う

アプリケーション全体のインスタンスデータを格納するためにプラグマ・ライブラリ・スクリプトを使用している場合は、代わりにQObject シングルトン型を使用することを検討してください。そうすることで、パフォーマンスが向上し、JavaScript のヒープメモリの使用量が減ります。

QMLアプリケーションにおけるメモリ割り当て

QMLアプリケーションのメモリ使用量は、C++のヒープ使用量とJavaScriptのヒープ使用量の2つに分けられます。それぞれに割り当てられるメモリは、QML エンジンや JavaScript エンジンによって割り当てられるため、一部は避けられませんが、残りはアプリケーション開発者の判断に依存します。

C++ ヒープには

  • QMLエンジンの固定的で不可避なオーバーヘッド(実装データ構造、コンテキスト情報など);
  • コンポーネントごとにコンパイルされたデータと型情報(型ごとのプロパティメタデータを含む);
  • オブジェクト単位のC++データ(プロパティ値を含む)と、アプリケーションがインスタンス化するコンポーネントに応じた要素単位のメタオブジェクト階層;
  • QMLインポート(ライブラリ)によって特別に割り当てられたデータ。

JavaScriptのヒープには以下が含まれます:

  • JavaScript エンジン自体(組み込みの JavaScript 型を含む)の固定的で不可避なオーバーヘッド;
  • JavaScriptの統合による固定的で不可避なオーバーヘッド(ロードされた型のコンストラクタ関数、関数テンプレートなど);
  • JavaScriptエンジンによって実行時に生成される、型ごとのレイアウト情報やその他の内部型データ(型については以下の注を参照);
  • オブジェクトごとの JavaScript データ ("var" プロパティ、JavaScript 関数とシグナルハンドラ、最適化されていないバインディング式);
  • 式の評価中に割り当てられた変数。

さらに、メインスレッドで使用するために 1 つの JavaScript ヒープが割り当てられ、オプションでWorkerScript スレッドで使用するためにもう 1 つの JavaScript ヒープが割り当てられます。アプリケーションが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"
    }
}

前の例では、矩形r0r1 はカスタムプロパティを持たないため、JavaScript エンジンと QML エンジンは両者を同じ型とみなします。つまり、r0r1 はどちらも明示的に定義されたRectangle 型であるとみなされます。長方形r2r3r4 はそれぞれカスタムプロパティを持っており、それぞれ異なる(暗黙の)型とみなされます。r3r4 は、同じプロパティ情報を持っているにもかかわらず、それぞれ異なる型とみなされることに注意してください。これは、単にカスタムプロパティがインスタンスであるコンポーネントで宣言されていないためです。

r3r4 の両方がRectangleWithString コンポーネントのインスタンスであり、そのコンポーネント定義にcustomProperty という文字列プロパティの宣言が含まれていた場合、r3r4 は同じ型であるとみなされます(つまり、これらは独自の暗黙の型を定義するのではなく、RectangleWithString 型のインスタンスとなります)。

メモリ割り当てに関する詳細な考察

メモリ割り当てや性能のトレードオフについて決定するときは常に、CPUキャッシュの性能、オペレーティングシステムのページング、JavaScriptエンジンのガベージコレクションの影響を念頭に置くことが重要です。最適な解決策を選択するためには、ベンチマークを慎重に行う必要があります。

一般的なガイドラインのセットは、アプリケーション開発者が開発しているプラットフォームの実装の詳細に関する実践的な知識と組み合わされた、コンピュータサイエンスの基礎となる原理の確かな理解に取って代わることはできません。さらに、トレードオフを決定する際に、いくら理論的に計算しても、優れたベンチマークと分析ツールのセットに取って代わることはできません。

断片化

フラグメンテーションはC++開発の問題です。アプリケーション開発者がC++型やプラグインを定義しないのであれば、このセクションは無視してもかまいません。

時間の経過とともに、アプリケーションはメモリの大部分を割り当て、そのメモリに データを書き込み、データの一部が使い終わると、メモリの一部を解放します。その結果、"空き "メモリが連続しないチャンクに配置され、他のアプリケーショ ンが使用できるようにオペレーティング・システムに戻すことができなくなります。また、「生きている」データが物理メモリの多くの異なるページにまたがっている可能性があるため、アプリケーションのキャッシュ特性やアクセス特性にも影響を及ぼします。その結果、オペレーティング・システムにスワップを強いることになり、ファイルシステムI/Oを引き起こす可能性があります。

フラグメンテーションは、プール・アロケータ(および他の連続したメモリ・アロケータ)を利用したり、オブジェクトの寿命を注意深く管理することで一度に割り当てられるメモリ量を減らしたり、定期的にキャッシュをクリーンアップして再構築したり、ガベージ・コレクションを備えたメモリ管理ランタイム(JavaScriptなど)を利用することで回避できる。

ガベージコレクション

JavaScriptはガベージコレクションを提供します。C++ ヒープとは対照的に)JavaScript ヒープに割り当てられたメモリは、JavaScript エンジンが所有します。エンジンはJavaScriptヒープ上の参照されないデータを定期的に収集します。

ガベージコレクションの意味

ガベージコレクションには利点と欠点があります。それは、オブジェクトの寿命を手動で管理することがあまり重要でなくなるということです。しかし、それはまた、アプリケーション開発者が制御できないタイミングで、JavaScriptエンジンによって潜在的に長時間の操作が開始される可能性があることを意味します。JavaScriptのヒープ使用がアプリケーション開発者によって注意深く考慮されない限り、ガベージコレクションの頻度と持続時間はアプリケーション体験に悪影響を及ぼすかもしれません。

ガベージコレクタの手動起動

QMLで書かれたアプリケーションは、(ほとんどの場合)どこかの段階でガベージコレクションを実行する必要があります。ガベージコレクションは利用可能な空きメモリが少なくなったときに JavaScript エンジンによって自動的に起動されますが、 アプリケーション開発者がガベージコレクタを手動で起動するタイミングを決定したほうがよい場合もあります (通常はそうではありませんが)。

アプリケーション開発者は、アプリケーションがいつアイドル状態になるかを最もよく理解しているはずです。QML アプリケーションが JavaScript のヒープメモリを大量に使用し、特にパフォーマンスに敏感なタスク (例えば、リストのスクロール、アニメーションなど) の間に定期的かつ破壊的なガベージコレクションサイクルを発生させる場合、アプリケーション開発者は、アクティビティがゼロの期間中にガベージコレクタを手動で起動するのがよいでしょう。アイドル期間は、アクティビティが発生している間にガベージコレクタを呼び出すことによって生じるユーザエクスペリエンスの低下(フレームスキップ、ぎくしゃくしたアニメーションなど)にユーザが気づかないので、ガベージコレクションを実行するのに理想的です。

ガベージコレクタは、JavaScript内でgc() 。この場合、包括的な収集サイクルが実行され、完了するまでに数百から千ミリ秒以上かかる可能性があるため、可能な限り避けるべきです。

メモリとパフォーマンスのトレードオフ

状況によっては、メモリ使用量の増加と処理時間の短縮をトレードオフにすることが可能です。たとえば、タイトなループで使われるシンボル検索の結果をJavaScriptの式の一時変数にキャッシュすると、その式を評価するときのパフォーマンスが大幅に向上しますが、一時変数の割り当てが必要になります。このようなトレードオフが賢明な場合もありますが(上記のケースのように、ほとんど常に賢明です)、システムのメモリ圧迫を避けるために、処理に少し時間がかかるようにしたほうがよい場合もあります。

場合によっては、メモリ使用量の増加の影響が極端になることもあります。状況によっては、想定される性能向上のためにメモリ使用量をトレードオフすることで、ページスラッシュやキャッシュスラッシュが増加し、性能が大幅に低下することがあります。ある状況においてどのソリューションが最適かを判断するためには、トレードオフの影響を注意深くベンチマークすることが常に必要です。

キャッシュ性能とメモリ時間のトレードオフに関する詳細な情報については、以下の記事を参照してください:

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。