Friday, June 5, 2020

Speed Benchmark: Number crunching in QML versus C++

Performance of Javascript versus C++

The objective is to compute the difference in number crunching performance between:
  • Javascript in QML, versus
  • raw C++

Outcome of Benchmark

Unsurprisingly, Javascript is significantly slower than C++. While it takes Javascript about 2.95 seconds to count from 1 to 1 million, which it takes C++ much lesser than 0.01 second to do the same.

There is no difference in performance between Javascript in .qml file, Javascript in old style Javascript file and Javascript in ECMAScript (ES7) .mjs file.



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

Native QML:                  2.94  seconds (baseline)
Javascript ECMAScript (ES7): 2.94  seconds
Old style Javascript:        2.96  seconds

C++ QObject:                 0.00  seconds


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

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

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 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
46
47
48
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() {
        const stop = 1000000 * 1000;
        let j = 1.0;
        let k = 1;
        for (let i = 0; i < stop; i++) {
        }
    }

    MyCPPObject {
        id: myCppObject
    }

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

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

            let bench = new Bench01mjs.BenchMJS();
                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-21: QML function used to count from 1 to 1 million.

Line 23-25: Instance the C++ QObject.

Line 36: Calls the native QML function.
Line 37: Calls the Javascript ECMAScript (ES7) module.
Line 38: Calls the old style Javascript module.
Line 39: Calls the C++ QObject.

This file is modified four times. Line 36, 37, 38 and 39 are commented out respectively when the benchmark for the particular module is run.

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
4
5
6
7
8
export class BenchMJS {
    bench01() {
        const stop = 1000000 * 1000;
        let j = 1.0;
        for (let i = 0; i < stop; i++) {
        }
    }
}


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
6
7
8
9
.pragma library

function bench02() {
    const stop = 1000000 * 1000;
    let j = 1.0;
    for (let i = 0; i < stop; i++) {
    }

}


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
16
17
18
19
20
#ifndef MYCPPOBJECT_H
#define MYCPPOBJECT_H

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

class MyCPPObject : public QObject
{
    Q_OBJECT

public slots:
    void cppBench01() {
        const int stop = 1000000 * 1000;
        float j = 1.0;
        for (int i = 0; i < stop; i++) {
        }
    }
};

#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: Number Crunching in .qml File

The following is the result when the onClicked handler calls the native QML function. That is, in file main.qml, line 36 is un-commented but line 37, 38 and 39 are commented out.

Result:
Run 1: 2.940000057220459 seconds
Run 2: 2.940999984741211  seconds
Run 3: 2.944000005722046  seconds

Average: 2.94 seconds

Result: Number Crunching in .mjs (ECMAScript) File

The following is the result when the onClicked handler calls the Javascript ECMAScript (ES7) function. That is, in file main.qml, line 37 is un-commented but line 36, 38 and 39 are commented out.

Result:
Run 1: 2.936000108718872  seconds
Run 2: 2.932999849319458 seconds
Run 3: 2.941999912261963  seconds

Average: 2.94 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 38 is un-commented but line 36, 37 and 39 are commented out.

Result:
Run 1: 2.950000047683716  seconds
Run 2: 2.944999933242798  seconds
Run 3: 2.9719998836517334 seconds

Average: 2.96 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 39 is un-commented but line 36, 37 and 38 are commented out.

Result:
Run 1: 0  seconds
Run 2: 0 seconds
Run 3: 0 seconds

Average: 0 seconds

No comments: