皇上,还记得我吗?我就是1999年那个Linux伊甸园啊-----24小时滚动更新开源资讯,全年无休!

新的 Qt 快速编译器技术

新的 Qt 快速编译器技术

我们已经有一段时间没有听说 Qt QML 内部和周围发生了什么,这是我们解释 QML 语言的引擎(不包括最近的公告)。最后一篇关于这个话题的帖子是拉斯在 2018 年写的

我们一直保持沉默,因为我们一直在设计新方法以使您的 QML 运行得更快,而其中一些方法被证明是死胡同。毕竟没有跟踪JIT。这并不酷,所以我们有些沉默。但现在有话要说。而且,请注意,这也不酷。很热。但让我先退一步。

寒冷:旧的 Qt 快速编译器

如果您已经使用了一段时间,那么您之前可能听说过“Qt Quick Compiler”这个术语。曾经有一个qmlcompiler工具可以将 QML 脚本代码编译成 C++。它首先与 Qt 5.3 一起发布。在我们的档案中,您仍然可以找到它的一些文档。它做了什么?很简单,它将 QML 脚本代码编译成 C++ 表示,通过私有 API“远程控制”QML 引擎。这样,QML 引擎就不必再接触 QML 源代码,从而大大提高了启动性能。

新的 Qt 快速编译器技术

该工具后来被 Qt 5.8 和 5.9 中引入的字节码缓存技术所取代,因为它最终在提高 QML 性能方面做得更好。这种缓存是如何工作的?当从头开始解释 QML 文件时,没有任何缓存或其他预处理,QML 引擎将解析源代码,并从中生成一个编译单元。编译单元包含每个函数和表达式的紧凑字节码表示。然后解释器根据需要使用此字节码来评估绑定并执行信号处理程序或 JavaScript 函数。此外,JIT 可以将字节码编译为本机机器码,以提高热功能的性能。缓存会在程序的调用中保留字节码,这样就不必一遍又一遍地解析和分析源代码。

新的 Qt 快速编译器技术

如果您查看这两个图表,您可能会想知道。在第一个中,如果我们只是提前编译所有 QML 文件,我们就不必运行任何解释器或 JIT。这应该更简单,更快,不是吗?那么,为什么我们放弃直接编译到 C++ 呢?

这里有两个“快速”维度:您不仅希望 QML 组件快速加载,还希望每个函数和表达式花费尽可能少的时间来执行。旧的 Qt Quick Compiler 应该解决这两个方面的问题。然而,事实证明,Qt 5.11 中引入的新解释器和 JIT 在优化运行时方面做得更好。现在您可能会问,解释器或 JIT 如何比由qmlcompiler. 我们确实问过自己同样的问题。重要的是要记住 JavaScript 是一种高度动态的语言。JavaScript 中的函数对可能传入或返回的参数类型不做任何承诺。运行时时间,我们确实对类型有一些指示,因为我们可以自省 QObjects 并检查它们提供了哪些类型的属性和方法。然而,在早期的 Qt 5 中,这些信息在编译时都不可用。因此,旧的 Qt Quick Compiler 必须生成非常通用的代码,这些代码使用相当于 QVariant 的代码来处理它所操作的每个值。为了做任何有意义的事情,生成的代码必须在每一步检查这些值的当前类型。这很慢。另一方面,在解释时,我们可以使用 Qt 元类型系统的运行时知识来避免一些类型检查开销。这使得新的解释器和 JIT 最终超越了旧的 Qt Quick Compiler 生成的 C++ 代码。

所以,就是这样,旧的 Qt Quick Compiler 已经走到了死胡同。我们可以做些什么来进一步提高 QML 性能?

酷:提前声明类型信息

为了提前生成更好的 C++ 代码,任何 QML-to-C++ 编译器都需要知道它正在处理什么样的对象。如果我们知道a并且b是整数,那么表达式a + b就变得比我们必须假设它们是字符串、对象、数组或其他任何东西要简单得多。有一种工具通常用于在运行时向 QML 提供类型信息:

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

在这里,您声明您的类型MyType应可用于 QML 及其所有属性和方法。如果 MyType 那么有一些属性ab,我们可以通过查看它的 QMetaObject 来确定它们的类型。在运行时,我们只是在注册调用进入时对其进行处理。为了在编译时进行处理,我们必须提前知道在解释 QML 文件时将注册哪些类型。由于注册调用发生在通用 C++ 代码中的任何地方,因此这个问题等同于停机问题。坏消息。

然而,大量类型实际上是众所周知的。我们知道 aRectangle有什么属性,aTimer是什么。为什么我们知道?因为我们总是在相同的地方注册那些具有相同名称的类型。因此,我们将这种方法形式化并提供了一种方法来声明您的 QML 类型以及支持它们的 C++ 类型。Qt 5.15 中提供了相关功能,您可以在当时写的博客文章中阅读它们。

此外,Qt 5.14 引入了在函数签名中提供类型信息的可能性,类似于在 TypeScript 中的做法:

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

有了这些工具,我们可以再次尝试 QML-to-C++ 路线。或者我们是这么认为的。不幸的是,有些东西不见了。

温暖:将 QML 组件组织成模块

为了知道哪些 QML 文件与哪些其他 QML 文件以及与哪些 C++ 代码相关,我们还需要在编译时知道文件系统位置和所有零碎的导入路径。然而,在 Qt5 中,使用 qmlcachegen 的唯一要求是将 QML 文件添加到资源文件系统中。在资源文件系统中,您可以将它们映射到您喜欢的任何路径。此外,您无需告诉任何人这些漂亮的 C++ 声明的 QML 类型将在哪里结束,以及您的 QML 代码是否可以访问它们。您甚至不需要声明 QML 文件属于哪个 QML 模块,因此也不需要声明它将隐式导入的类型。对于所有这些,可以编写有趣且脆弱的启发式方法,但最终必须找到适当的解决方案。

在 Qt 6.2 中,有一个用于构建 QML 模块的 CMake API。您可以在我们最近的博客 文章中阅读此内容。是的,编写 QML 模块的新方法有些限制。你不能再到处移动你的 QML 文件,你必须声明你的模块的所有内容。但是,有了所有这些信息,我们可以确信我们对您的 QML 模块有足够的了解,可以生成好的C++ 代码。

热门:新的 Qt 快速编译器

在 Qt 6.2 中,我们发布了新 QML 脚本编译器(简称)的技术预览qmlsc,它使用上述所有信息从您的 QML 函数和表达式生成 C++ 代码。它取代了 qmlcachegen,除了可以详尽分析的函数的字节码之外,还简单地生成 C++ 代码。

新的 Qt 快速编译器技术

qmlsc可与 Qt 一起用于设备创建。在 Qt 6.3 中,它的一些功能被合并到qmlcachegen中,所有版本的 Qt 都可以继续使用。

请注意,有许多 JavaScript 结构qmlsc无法在 C++ 中以有效的方式表示。与旧的 qmlcompiler 相比,qmlsc对于包含此类结构的函数,将跳过 C++ 代码生成,并且只生成要解释的字节码。然而,最常见的 QML 表达式具有相当简单的性质:QObjects 上的值查找、算术、简单的 if/else 或循环构造。这些可以很容易地用 C++ 表达,这样做提供了很好的加速。

此外,在 Qt 6.3 中,还会有另一个编译器:QML 类型编译器,或qmltc. 使用 QML 类型编译器,您将能够将您的对象结构编译为 C++。每个 QML 组件都以这种方式成为一个 C++ 类。从 C++ 实例化这些类将比通过 QQmlComponent 的通常迂回快得多。qmltc, 像qmlsc, 是建立在编译时完整类型信息的可用性之上的。qmlsc但是,与 相比,qmltc在缺少类型信息的情况下没有优雅的回退。为了使用qmltc,您必须提供它所需的所有信息。qmltc将适用于所有版本的 Qt。

在另一篇文章中,将探讨使用 的性能优势以及和qmlsc之间的差异。qmlscqmlcachegen