한국어
Qt
 

성능 고려 및 제안 사항

makersweb 2022.03.07 20:38 조회 수 : 493

타이밍 관련 고려 사항

응용 프로그램 개발자는 렌더링 엔진이 초당 60프레임의 일관된 재생 빈도를 달성할 수 있도록 노력해야 한다. 60 FPS는 처리가 완료될 수 있는 각 프레임 사이에 약 16밀리초가 간격이 있음을 의미하며, 여기에는 프리미티브를 그래픽 하드웨어에 업로드하는 데 필요한 처리가 포함된다.

실제로 이것은 애플리케이션 개발자가 다음사항을 지켜야 함을 의미한다.

  • 가능한 한 비동기식 이벤트 기반 프로그래밍을 할 것.
  • 작업자 스레드를 사용하여 중요한 처리를 수행할 것.
  • 이벤트 루프를 수동으로 돌리지 말 것.
  • 차단 함수 내에서 2 milliseconds 이상을 소비하지 말 것.

그렇게 하지 않으면 프레임이 건너뛰어 사용자 경험에 큰 영향을 끼친다.

 
Note: 솔깃하지만 절대 사용해서는 안 되는 패턴은 QML에서 호출된 C++ 코드 블록 내에서 차단을 피하기 위해 직접 QEventLoop를 생성하거나 QCoreApplication::processEvents()를 호출하는 것이다. 이벤트 루프가 시그널 핸들러 또는 바인딩에 입력되면 QML 엔진이 다른 바인딩, 애니메이션, 트랜지션 등을 계속 실행하기 때문에 위험하다. 이벤트 루프가 포함된 계층 구조를 파괴하는 부작용을 일으킬 수 있다.

프로파일링(Profiling)

가장 중요한 팁은 Qt Creator에 포함된 QML 프로파일러를 사용하는 것이다. 애플리케이션에서 시간이 소비되는 위치를 알면 잠재적으로 존재하는 문제 영역이 아닌 실제로 존재하는 문제 영역에 접근할 수 있다. QML 프로파일링 도구를 사용하는 방법에 대한 자세한 내용은 Qt Creator 메뉴얼을 통해 얻을 수 있다.

어떤 바인딩이 가장 자주 실행되는지 또는 애플리케이션에서 가장 많은 시간을 소비하는 함수를 파악하면 문제 영역을 최적화해야 하는지 또는 성능이 향상되도록 애플리케이션의 일부 구현 및 세부 사항을 재설계해야 하는지를 결정할 수 있다. 프로파일링 없이 코드를 최적화하려고 하면 성능 향상이 크기보다는 아주 미미할 수 있다.

JavaScript Code

대부분의 QML 응용 프로그램에는 동적 함수, 시그널 핸들러 및 속성 바인딩 표현식의 형태로 많은 양의 JavaScript 코드가 있다. 이것은 일반적으로 문제가 되지 않는다. 바인딩 컴파일러에서 수행된 것과 같은 QML 엔진의 일부 최적화 덕분에 (일부 사용 사례에서는) C++ 함수를 호출하는 것보다 빠를 수 있다. 그러나 불필요한 처리가 실수로 이어지지 않도록 주의해야 한다.

형변환(Type-Conversion)

JavaScript 사용 시 주요 비용 중 하나는 대부분의 경우 QML 유형의 속성(Property)에 액세스할 때. 즉, 기본 C++ 데이터(또는 이에 대한 참조)를 포함하는 외부 리소스가 있는 JavaScript 개체가 생성된다. 대부분의 경우 이것은 상당히 저렴하지만 어떤 경우에는 상당히 비쌀 수 있다. 비용이 많이 드는 한 가지 예는 C++ QVariantMap Q_PROPERTY를 QML variant 속성에 할당하는 것이다. 특정 유형의 시퀀스(int, qreal, bool, QStringQUrlQList)는 저렴할 테지만 List도 비용이 많이 들 수 있다. 다른 리스트 유형에는 값비싼 변환 비용이 요구된다(새 JavaScript 배열을 만들고 새 유형을 하나씩 추가하고 C++ 유형 인스턴스에서 JavaScript 값으로 유형별 변환).

일부 기본 속성 유형(예: "string" 및 "url" 속성) 간의 변환도 비용이 많이 들 수 있다. 가장 근접하게 일치하는 속성 유형을 사용하면 불필요한 변환을 피할 수 있다.

QVariantMap을 QML에 노출해야 하는 경우 variant 속성 대신 var 속성을 사용한다. 일반적으로 property varQtQuick 2.0 이상의 모든 사용 사례에 대해 property variant보다 우수한 것으로 간주된다(property variant는 사용되지 않는 것으로 표시됨). 진정한 JavaScript 참조를 저장할 수 있기 때문이다(특정 표현식에 필요한 변환 횟수를 줄일 수 있음).

속성 확인(Resolving Properties)

속성(Property) 확인에는 시간이 걸린다. 어떤 경우에는 조회 결과를 캐시하고 재사용할 수 있지만, 가능하면 항상 불필요한 작업을 피하는 것이 가장 좋다.

다음 예제에는 자주 실행되는 코드 블록이 있다(이 경우 명시적 루프의 내용이지만 예를 들어 일반적으로 평가되는 바인딩 표현식일 수 있음). "rect" id와 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");
    }
}

대신 공통 base로 블록에서 한 번만으로 확인할 수 있다.

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

Property Bindings

속성 바인딩 표현식은 참조하는 속성이 변경되면 다시 평가된다. 따라서 바인딩 표현식은 최대한 단순하게 유지해야 한다.

어떤 처리의 최종 결과만 중요한 루프가 있는 경우, 예를 들어 값의 누적연산 루프에서 누적의 중간 단계에서 바인딩 식의 재평가를 방지하기 위해 속성 자체를 점진적으로 업데이트하는 대신 나중에 속성에 할당하는 임시 변수를 업데이트하는 것이 좋다.

다음 예제에서는 이 점을 보여 준다.

// 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 핸들러의 for 루프는 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;
    }
}

Sequence tips

앞서 언급했듯이 일부 시퀀스 유형(예: QList<int>, QList<qreal>, QList<bool>, QList<QString>, QStringListQList<QUrl>)은 빠르지만 다른 시퀀스 유형은 훨씬 느리다. 가능한 한 느린 유형 대신 이러한 유형을 사용하는 것 외에도 최상의 성능을 달성하기 위해 알아야 의미가 있다.

첫째, 시퀀스 유형에 대한 두 가지 다른 구현이 있다. 하나는 시퀀스가 QObjectQ_PROPERTY인 경우(우리는 이것을 참조 시퀀스라고 부를 것이다.)이고, 다른 하나는 시퀀스가 QObject의 Q_INVOKABLE 함수에서 반환되는 경우(우리는 이것을 복사 시퀀스라고 부를 것이다.)에 대한 것이다.

참조 시퀀스는 QMetaObject::property()를 통해 읽고 쓰므로 QVariant로 읽고 쓴다. 즉, JavaScript에서 시퀀스의 요소 값을 변경하면 세 단계가 발생한다. 전체 시퀀스는 QObject에서 읽혀질 것이다(QVariant로서, 그러나 올바른 유형의 시퀀스로 캐스트됨). 지정된 인덱스의 요소는 해당 순서로 변경된다. 그리고 완전한 시퀀스는 QObject에 다시 쓰여질 것이다(QVariant로서).

복사 시퀀스는 실제 시퀀스가 JavaScript 개체의 리소스 데이터에 저장되기 때문에 훨씬 간단하므로 읽기/수정/쓰기 주기가 발생하지 않는다(대신 리소스 데이터가 직접 수정됨).

따라서 참조 시퀀스에 대한 쓰기는 복사 시퀀스의 요소에 대한 쓰기보다 훨씬 느리다. 사실, N-요소 참조 시퀀스의 단일 요소에 쓰는 것은 N-요소 복사 시퀀스를 해당 참조 시퀀스에 할당하는 비용과 동일하다. 따라서 일반적으로 임시 복사 시퀀스를 수정한 다음 계산 중에 결과를 참조 시퀀스에 할당하는 것이 좋다.

다음 C++ 유형이 있다고("Qt.example 1.0" 네임스페이스에 대한 사전 등록 포함) 가정하자.

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

"qrealListProperty[j] = j" 표현식으로 인해 발생하는 내부 루프에서 QObject 속성을 읽고 쓰기 때문에 이 코드는 최적이 아니다. 대신 기능적으로 동일하지만 훨씬 더 빠른 코드는 다음과 같다.

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

위의 예에서 중간 바인딩만 매번 재평가되어 상당한 성능 향상을 가져온다.

Value-Type tips

값 유형(font, color, vector3d 등)속성은 유사한 QObject 속성을 가지며 알림 의미 체계를 시퀀스 형식 속성으로 변경한다. 따라서 시퀀스에 대해 위에 언급된 팁은 값 유형 속성에도 적용할 수 있다. 일반적으로 값 유형의 경우 문제가 덜하지만(값 유형의 하위 속성 수가 일반적으로 시퀀스의 요소 수보다 훨씬 적기 때문에) 재평가되는 바인딩 수가 증가하면 불필요하게 성능에 부정적인 영향을 미칠 수 있다.

기타 JavaScript 객체

또 다른 JavaScript 엔진은 또 다른 최적화를 제공한다. Qt Quick 2가 사용하는 JavaScript 엔진은 객체 인스턴스화 및 속성 조회에 최적화되어 있지만 제공하는 최적화는 특정 기준에 의존한다. 응용프로그램이 기준을 충족하지 않는 경우, JavaScript 엔진은 성능이 훨씬 떨어지는 "저속" 모드로 돌아간다. 따라서 항상 다음 기준을 충족하는지 확인한다.

  • 가능하면 eval()을 사용하지 말 것.
  • 개체의 속성을 삭제하지 말 것.

공통 인터페이스 요소

Text Elements

텍스트 레이아웃 계산은 느린 작업일 수 있다. 가능한 경우 Text.StyledText 대신 Text.PlainText 형식을 사용하는 것이 좋다. 이렇게 하면 레이아웃 엔진에 필요한 작업량이 줄어들기 때문이다. Text.PlainText를 사용할 수 없는 경우(이미지를 포함하거나 태그를 사용하여 전체 텍스트가 아닌 특정 형식(굵게, 기울임꼴 등)을 갖도록 문자 범위를 지정해야 하는 경우)에는 Text.StyledText를 사용해야 한다.

이 모드에서는 구문 분석 비용이 발생하므로 텍스트가 Text.StyledText일 수 있는 경우에만(하지만 아마 아닐 것이다) Text.AutoText를 사용해야 한다. 텍스트 형식이 Text.AutoText인 경우 텍스트 항목은 텍스트를 Text.StyledText이 지정된 텍스트로 처리해야 하는지 여부를 자동으로 결정한다. Text.StyledText는 적은 비용으로 거의 모든 기능을 제공하므로 Text.RichText 모드 사용을 지양한다.

Images

이미지는 모든 사용자 인터페이스의 중요한 부분이다. 불행히도 로드하는 데 걸리는 시간, 사용하는 메모리의 양, 사용 방식으로 인해 문제의 큰 원인이되기도 한다.

이미지는 종종 상당히 클 수 있므로 이미지를 로드해도 UI 스레드가 차단되지 않도록 하는 것이 좋다. QML Image 요소의 asynchronous 속성을 true로 설정하면 로컬 파일 시스템에서 이미지를 비동기적으로 로드할 수 있다(원격 이미지는 항상 비동기식으로 로드됨). 이 경우 사용자 경험 및 인터페이스의 미관에 부정적인 영향을 미치지 않는다.

asynchronous 속성이 true로 설정된 이미지 요소는 우선 순위가 낮은 작업자 스레드에서 이미지를 로드한다.

애플리케이션이 큰 이미지를 로드하지만 작은 요소로 표시하는 경우 sourceSize 속성을 렌더링되는 요소의 크기로 설정하여 이미지의 작은 크기 버전이 큰 이미지가 아닌 메모리에 유지되도록 한다. sourceSize를 변경하면 이미지가 다시 로드된다는 점에 유의하자.

또한 애플리케이션에 미리 구성된 이미지 리소스를 제공하면 런타임에 작성 작업을 수행하지 않아도 된다(예: 요소에 그림자 효과 제공).

필요한 경우에만 image.smooth를 활성화한다. 일부 하드웨어에서는 느리고 이미지가 원래 크기로 표시되면 시각적 효과가 없다.

같은 영역을 여러 번 칠하지 말자. 배경을 여러 번 칠하지 않으려면 Rectangle보다 Item을 루트 요소로 사용하는 것이 좋다.

앵커가 있는 위치 요소(Position Elements With Anchors)

항목을 서로 상대적으로 배치하기 위해 바인딩보다 앵커를 사용하는 것이 더 효율적이다. rect1에 상대적인 위치 rect2에 대한 바인딩 사용을 고려하자.

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 속성의 정적 초기화를 사용하는 것이다. Item 좌표는 항상 부모를 기준으로 하므로 부모의 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
    }
}

 

번호 제목 글쓴이 날짜 조회 수
공지 Qt프로그래밍(QtQuick) Beginner를 위한 글 읽는 순서 운영자 2019.01.05 85359
156 단일 인스턴스 Qt 응용 프로그램(Single-instance Application) makersweb 2022.06.23 693
155 Qt로 작성된 iOS 앱에서 시리얼 통신 file makersweb 2022.04.30 807
154 VirtualKeyboard 스타일 커스터 마이징 makersweb 2022.03.13 448
» 성능 고려 및 제안 사항 makersweb 2022.03.07 493
152 Binding 타입으로 객체 속성 간 묶기 makersweb 2022.03.04 404
151 Qt Bluetooth Low Energy 개요 makersweb 2022.02.13 650
150 Qt Android 앱에 AdMob 배너달기 file makersweb 2021.12.04 385
149 Qt 6의 C++ 프로퍼티 바인딩 예제 makersweb 2021.11.01 845
148 QML에서 앵커(anchors)로 위치 지정 file makersweb 2021.10.05 3703
147 안드로이드용 Qt 6.2 makersweb 2021.10.02 490
146 Qt 응용프로그램에서 PDF 문서 렌더링 file makersweb 2021.09.23 654
145 QML에서 Websocket 서버와 통신 file makersweb 2021.09.18 807
144 QML 코딩 규칙 makersweb 2021.09.05 3174
143 QML 에서 QR코드 생성 file makersweb 2021.08.20 882
142 앱을 종료할 때 QML 바인딩 오류를 피하는 방법 makersweb 2021.08.08 487
141 Qt 응용프로그램에서 Lottie Animation사용 file makersweb 2021.05.30 867
140 싱글 샷(Single-Shot) 시그널/슬롯 연결 makersweb 2021.05.12 740
139 응용프로그램 자동실행 설정 (on Windows) makersweb 2021.05.08 555
138 Qt 6 에서 프로퍼티 바인딩 makersweb 2021.04.03 761
137 QML과 JavaScript 의 숫자 관련 내장된 함수 makersweb 2021.03.28 1353