The new Qt Quick Compiler technology

The new Qt Quick Compiler technology

It's been a while since we've heard about what goes on inside and around Qt QML, our engine to interpret the QML language (not counting the recent announcement, that is). The last post strictly about this topic was what Lars wrote in 2018.

We've been so silent because we've been prototyping new ways to make your QML run faster, and some of them turned out to be dead ends. There is no tracing JIT after all. This isn't cool, so we were somewhat silent. But now there is something to say. And, mind you, it's not cool either. It's hot. But let me take a step back first.

The Cold: Old Qt Quick Compiler

If you've been around for a while, you may have heard the term "Qt Quick Compiler" before. There used to be a qmlcompiler tool that compiled QML script code to C++. It first shipped with Qt 5.3. In our archives you can still find some of its documentation. What did it do? Quite simply, it compiled QML script code into a C++ representation that "remote controlled" the QML engine through private API. This way, the QML engine didn't have to touch the QML source code anymore, which lead to greatly improved startup performance.

qmlcompiler architecture

This tool was later replaced by the byte code caching technology introduced in Qt 5.8 and 5.9, as that eventually did a better job at improving QML performance. How does this caching work? When interpreting a QML file from scratch, without any caching or other preprocessing, the QML engine will parse the source code, and produce a compilation unit from it. The compilation unit contains a compact byte code representation for each function and expression. The interpreter then uses this byte code as needed to evaluate bindings and execute signal handlers or JavaScript functions. Furthermore, the JIT can compile the byte code to native machine code for improved performance of hot functions. The cache retains the byte code across invocations of your program, so that the source code doesn't have to be parsed and analyzed over and over. The qmlcachegen tool does this at compile time and causes the byte code to be bundled into your binary.

qmlcachegen architecture

If you look at those two diagrams, you may wonder. In the first one we don't have to run any interpreter or JIT if we just compile all our QML files ahead of time. This should be simpler and faster, shouldn't it? So, why did we drop the direct compilation to C++?

There are two dimensions to "fast" here: You not only want your QML components to load quickly, but you also want each function and expression to take the least possible time to execute. The old Qt Quick Compiler was supposed to address both aspects. However, it turned out that the new interpreter and JIT introduced in Qt 5.11 did a better job at optimizing the run time aspect. Now you may ask how the interpreter or the JIT can be faster than native machine code compiled from C++ as generated by qmlcompiler. We did ask ourselves the same question. It's important to remember that JavaScript is a heavily dynamic language. A function in JavaScript makes no promises whatsoever on the types of arguments that may be passed in or returned. At run time, we do have some indication about the types because we can introscpect the QObjects at play and check what kinds of properties and methods they offer. However, in early Qt 5 none of this information was available at compile time. Therefore, the old Qt Quick Compiler had to generate extremely generic code that worked with the equivalent of a QVariant for each and every value it manipulated. In order to do anything meaningful, the resulting code had to check the current type of these values at every step. This was slow. When interpreting, on the other hand, we could use our run time knowledge of the Qt metatype system to avoid some of the type checking overhead. This made the new interpreter and JIT eventually surpass the C++ code generated by the old Qt Quick Compiler.

So, that was that, the old Qt Quick Compiler had hit a dead end. What could we do to further improve on QML performance?

The Cool: Declaring type information ahead of time

In order to generate better C++ code ahead of time, any QML-to-C++ compiler needs to know what kind of objects it's dealing with. If we know that a and b are integers, then the expression a + b becomes a much simpler affair than if we have to assume they are strings, objects, arrays, or anything else. There is one facility commonly used to provide type information to QML at run time:

qmlRegisterType<MyType>("Some.Module", 3, 12, "T");

Here, you declare that your type MyType shall be available to QML with all its properties and methods. If MyType then has some properties a and b, we can determine their types by looking at its QMetaObject. At run time, we just process those registration calls as they come in. In order to do it at compile time we have to know in advance what types will have been registered at the time a QML file is interpreted. As the registration calls happen anywhere in generic C++ code, this problem is equivalent to the halting problem. Bad news.

Yet, a large number of types is in fact well known. We know what properties a Rectangle has, and what a Timer is. Why do we know? Because we always register those types with the same names at the same places. So, we formalized this approach and provided a way to declare your QML types together with the C++ types backing them. The relevant facilities were provided in Qt 5.15 and you can read up on them at the blog post written back then.

In addition, Qt 5.14 introduced the possibility to provide type information in function signatures, similar to how it is done in TypeScript:

function add(a: int, b: int) : int { return a + b }

With these tools at hand, we could try the QML-to-C++ route again. Or so we thought. Something was missing, unfortunately.

The Warm: Organizing QML components into modules

In order to know what QML files relate to which other QML files and to which C++ code, we also need to know the file system locations and import paths of all the bits and pieces at compile time. In Qt5, however, the only requirement for using qmlcachegen was that the QML files be added added to the resource file system. In the resource file system, you could map them to any paths you liked. Furthermore, you didn't need to tell anyone where those nice C++-declared QML types would end up and whether they would be accessible by your QML code. You didn't even need to declare what QML module a QML file would belong to, and therefore what types it would import implicitly. For all of this, interesting and brittle heuristics can be written, but ultimately a proper solution had to be found.

In Qt 6.2, there is a CMake API for building QML modules. You can read up on this in our recent blog posts. Yes, the new way of writing QML modules is somewhat restrictive. You cannot just move your QML files all over the place anymore, and you have to declare all the contents of your modules. But, with all this information, we can be confident that we know enough about your QML modules to generate good C++ code.

The Hot: New Qt Quick Compiler

In Qt 6.2 we've released a technology preview of our new QML Script Compiler, short qmlsc, that uses all the information explained above to generate C++ code from your QML functions and expressions. It replaces qmlcachegen, and simply generates C++ code in addition to byte code for functions it can exhaustively analyze.

qmlsc architecture

qmlsc is available with Qt for Device Creation. In Qt 6.3, some of its features are merged into qmlcachegen, which continues to be available with all versions of Qt.

Mind that there are many JavaScript constructs that qmlsc cannot be represented in C++ in an efficient way. In contrast to the old qmlcompiler, qmlsc will skip the C++ code generation for functions that contain such constructs and only generate byte code to be interpreted. Yet, most common QML expressions are of a rather simple nature: Value lookups on QObjects, arithmetics, simple if/else or loop constructs. Those can easily be expressed in C++, and doing so provides a nice speedup.

Additionally, in Qt 6.3, there will be another compiler: The QML type compiler, or qmltc. With the QML type compiler, you will be able to compile your object structure to C++. Each QML component becomes a C++ class this way. Instantiating those classes from C++ will be much faster than the usual detour through QQmlComponent. qmltc, like qmlsc, is built on the availability of complete type information at compile time. In contrast to qmlsc, though, qmltc has no graceful fallback in case the type information is lacking. In order to use qmltc, you will have to provide all the information it requires. qmltc will be available with all versions of Qt.

In a further post, the performance benefits of using qmlsc, and the differences between qmlsc and qmlcachegen will be explored.


Blog Topics:

Comments