Wednesday, June 3, 2020

Speed Benchmark: Calling QML function versus Javascript function versus QObject

QML Function Call Performance Comparison

Is calling QML functions faster than calling Javascript functions? How about calling QObject functions? Which has speed advantage?

Note that this is merely calling functions but not running for-loops and while-loops. The intent is to compare this speed of calling Javascript functions to the speed of calling QObjects functions.

Outcome of Benchmark

Surprisingly, calling Javascript .mjs functions (referred to by Qt as ECMAScript modules, which conforms to Javascript ES7 standards) is 40% faster than calling either QML functions or old style Javascript functions.

Calling C++ QObjects from QML is 344% slower than calling another function in a native QML object!



Time taken for a QML object to call functions in the following objects:

Native QML:                  51.04  seconds (baseline)
Javascript ECMAScript (ES7): 30.53  seconds
Old style Javascript:        51.61  seconds

C++ QObject:                 175.49 seconds


The reader may be interested in a higher level summary of these data in the article:
Javascript is faster than C++.

 Conclusion

If the bottleneck of an application is cross function calls, write your functions and methods in .mjs methods. You get all the benefit of Javascript classes, ability to load modules, arrow functions plus better speed!

Even calling functions in "native" QML objects are not as fast as calling functions in .mjs ECMAScript modules. ECMAScript modules is the module that supports Javascript ES7 syntax.

Make as little QML to C++ calls as possible (again, only if cross function calls is a bottleneck of the application).

Note that all these functions are called without any function parameters. Should function parameters be included in this benchmark, it is expected that C++ functions will perform worse than the current numbers. This is because conversion of QML object types to C++ QVariant and then to native C++ types requires more processing than converting from QML objects to Javascript objects.

Equipment Used in Benchmark

Hardware

  1. MacBook Pro
  2. 8GB memory
  3. 2.3 GHz Intel Core i5

Software

  1. MacOS Version 10.14.5
  2. Qt version 15.5.0
  3. XCode version 11.2.1
  4. Zero additional compiler tuning and optimization

Benchmark Procedure

  1. Compile project in release mode (not debug mode which does not reflect actual customer experience)
  2. Run benchmark a total of 7 times
  3. Drop result of first four runs to allow hardware cache to take effect
  4. Compute average of last 3 runs (i.e. run 5, 6 and 7)

The Code Used for Benchmark

The code below is the exact code used for this benchmark.

File: main.qml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.0

import "benchm01.mjs" as Bench01mjs
import "benchm01.js" as Bench01js
import BenchCPP 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    function qmlFunction() { /* do nothing */ }

    MyCPPObject {
        id: myCppObject
    }

    Button {
        text: "Start benchmark"
        x: 20
        y: 20

        onClicked: {
            let startTime = new Date().getTime() / 1000;

            let bench = new Bench01mjs.BenchMJS();
            const stop = 1000000 * 1000
            for (let i = 0; i < stop ; i++) {
//                qmlFunction();
//                bench.bench01();
//                Bench01js.bench02();
                  myCppObject.cppBench01();
            }

            let stopTime = new Date().getTime() / 1000;

            console.log("startTime:", startTime);
            console.log("stopTime:", stopTime);
            console.log("diff:", stopTime - startTime);
        }
    }
}

Line 5: Import new style Javascript ECMAScript (ES7) modules.
Line 6: Import old style Javascript modules.
Line 7: Import C++ QObject.

Line 15: QML function used in the benchmark.

Line 17-18: Instance the C++ QObject.

Line 32: Calls the native QML function.
Line 33: Calls the Javascript ECMAScript (ES7) module.
Line 34: Calls the old style Javascript module.
Line 35: Calls the C++ QObject.

This file is modified four times. Line 32, 33, 34 and 35 are commented out respectively when the benchmark for the particular module is run.

A mistake was made while creating this benchmark. Ideally, line 27 and line 29 should swap. The cost of creating a new Javascript object should not be charged to every other benchmark. A run was made to ensure that the cost of creating a new Javascript object is negligible to the overall benchmarks. However, there was no time to re-run all benchmarks.

The following is source code for the ECMAScript (ES7) module that is loaded from line 5 in the file main.qml above.

File: benchm01.mjs
1
2
3
export class BenchMJS {
    bench01() { }
}

The source code for old style Javascript module that is loaded from line 6 in the file main.qml above.

File: benchm01.js
1
2
3
4
5
.pragma library

function bench02() {

}

The source code for QObject that is loaded from line 7 in the file main.qml above.

File: mycppobject.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#ifndef MYCPPOBJECT_H
#define MYCPPOBJECT_H

#include <QObject>
#include <qqml.h>

class MyCPPObject : public QObject
{
    Q_OBJECT

public slots:
    void cppBench01() { /* do nothing */ }
};

#endif // MYCPPOBJECT_H


The source code for the entry point of the project.

File: main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <QGuiApplication>
#include <QQmlApplicationEngine>

#include "mycppobject.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);

    qmlRegisterType<MyCPPObject>("BenchCPP", 1, 0, "MyCPPObject");

    engine.load(url);

    return app.exec();
}


The Application

The boring look of the final application used for benchmark.


Result: QML calling QML Function Benchmark

The following is the result when the onClicked handler calls the native QML function. That is, in file main.qml, line 32 is un-commented but line 33, 34 and 35 are commented out.

Result:
Run 1: 50.962000131607056 seconds
Run 2: 51.23300004005432  seconds
Run 3: 50.91599988937378  seconds

Average: 51.04 seconds

Result: QML calling Javascript .mjs (ECMAScript) Function Benchmark

The following is the result when the onClicked handler calls the Javascript ECMAScript (ES7) function. That is, in file main.qml, line 33 is un-commented but line 32, 34 and 35 are commented out.

Result:
Run 1: 30.49500012397766  seconds
Run 2: 30.520999908447266 seconds
Run 3: 30.58300018310547  seconds

Average: 30.53 seconds


Result: QML calling Javascript (old style) Function Benchmark

The following is the result when the onClicked handler calls the old style Javascript function. That is, in file main.qml, line 34 is un-commented but line 32, 33 and 35 are commented out.

Result:
Run 1: 50.532999992370605 seconds
Run 2: 50.375             seconds
Run 3: 50.68599987030029  seconds

Average: 51.61 seconds

Result: QML calling C++ QObject Function Benchmark

The following is the result when the onClicked handler calls the C++ QObject function. That is, in file main.qml, line 35 is un-commented but line 32, 33 and 34 are commented out.

Result:
Run 1: 175.6309998035431  seconds
Run 2: 175.26600003242493 seconds
Run 3: 175.57200002670288 seconds

Average: 175.49 seconds

2 comments:

Anonymous said...

What version of Qt was used for this test?

fner said...

Very interesting. I'd also like to know which version you were testing with?