QML 性能考虑与建议#

关于性能相关权衡和最佳实践的讨论

时间考虑#

作为应用开发者,您必须努力确保渲染引擎达到一致的每秒60帧更新率。60 FPS意味着每帧之间大约有16毫秒的时间可以作为处理时间,这包括将绘图原语上传到图形硬件所需处理的响应。

在实践中,这意味着应用开发者应该

  • 尽可能使用异步、事件驱动的编程

  • 使用工作线程进行大量处理

  • 绝不要手动旋转事件循环

  • 在每帧阻塞函数中不超过几毫秒

未能做到这一点将导致省略帧,这将对用户体验产生严重影响。

注意

一个看似诱人但应该绝不要使用模式是创建自己的QEventLoop或者在从QML调用的C++代码块中调用QCoreApplication::processEvents()以避免阻塞。这是危险的,因为当事件循环进入信号处理程序或绑定时,QML引擎会继续运行其他绑定、动画、转换等。这些绑定可以导致副作用,例如,销毁包含您事件循环的层次结构。

分析#

最重要的提示是:使用Qt Creator附带的所有QML分析器。了解时间在应用程序中的使用情况将允许您关注真实存在的问题区域,而不是可能存在的问题区域。有关如何使用QML分析工具的更多信息,请参阅Qt Creator手册

确定哪些绑定被运行最频繁,或您的应用在哪些函数上花费了最多时间,这将允许您决定是否需要优化问题区域,或者重新设计一些应用实现细节以提高性能。在没有分析的情况下尝试优化代码很可能只会带来非常小的而不是显著的性能提升。

JavaScript 代码#

大多数QML应用将包含大量JavaScript代码,形式为动态函数、信号处理程序和属性绑定表达式。这通常不会是问题。多亏了QML引擎的一些优化,例如对绑定编译器的优化,在某些情况下它可以比调用C++函数更快。但是,必须小心,确保不会意外触发不必要的处理。

类型转换#

使用JavaScript的一大成本在于,在大多数情况下,当访问从QML类型属性时,会创建一个包含底层C++数据(或其引用)的JavaScript对象(外部资源)。在大多数情况下,这相当成本低廉,但在某些情况下可能会非常昂贵。一个成本昂贵的例子是将C++ QVariantMap Q_PROPERTY分配给QML“变体”属性。列表也可能很昂贵,尽管特定类型的序列(如QList、qreal、bool、QString和QUrl)应该是相对便宜的;其他列表类型则涉及昂贵的转换成本(创建新的JavaScript Array,逐个添加新类型,每个类型都需要从C++类型实例转换为JavaScript值)。

在某些基本属性类型之间转换(如“字符串”和“URL”属性)可能也很昂贵。使用最接近匹配的属性类型可以避免不必要的转换。

如果必须将QVariantMap公开给QML,请使用“var”属性而不是“变体”属性。一般来说,从QtQuick 2.0及更新的版本开始,“property var”应该被认为优于“property variant”(注意“property variant”已被标记为已过时),因为它允许存储真正的JavaScript引用(这可以在某些表达式中的转换中减少所需的转换次数)。

解析属性#

属性解析需要时间。虽然在某些情况下,查找结果可以被缓存和重用,但如果可能的话,始终最好避免完全不必要的操作。

在下面的示例中,我们有一个经常运行的代码块(在这种情况下,它是显式循环的内容;但这也可能是常用的计算绑定表达式,例如)并且在其中多次解析具有“rect” ID和“color”属性的对象

我们可以将此块中的常见基础解析仅执行一次

仅仅是这个简单的变化就带来了显著的性能提升。请注意,上述代码可以通过如下方式进一步改进(因为在这个循环处理过程中要查找的属性永远不会改变),即将属性解析提升出循环

属性绑定#

如果它引用的任何属性都发生了改变,属性绑定表达式将被重新评估。因此,绑定表达式应该尽可能简单。

如果你在循环中进行一些处理,但只有处理结果的最终结果很重要,通常会更好:更新一个临时的累积器,然后再分配给你要更新的属性,而不是逐步更新属性本身,以此来避免在累积过程中的中间阶段触发绑定表达式的重新评估。

以下构造示例阐述了这一点

在onCompleted处理程序中的循环会导致“text”属性绑定重新评估六次(这会导致任何依赖于文本值的其他属性绑定以及每次重新评估的onTextChanged信号处理程序和每次布局文本以供显示)。在这种情况下,这显然是不必要的,因为我们真正关心的是累积的最终值。

它可以改写如下

序列提示#

如前所述,某些序列类型很快(例如,QList、QList、QList、QList、QStringList和QList),而其他类型的速度则慢得多。除了在任何可能的地方使用这些类型而不是更慢的类型之外,还有一些其他与性能相关的语义,你需要了解这些语义以实现最佳性能。

首先,对于序列类型有两种不同的实现:一种是将序列作为一个QObject的Q_PROPERTY(我们将这种序列称为引用序列),另一种是将序列从一个QObject的Q_INVOKABLE函数返回(我们将这种序列称为复制序列)。

引用序列通过QMetaObject::property()进行读取和写入,因此它被读取和写入为QVariant。这意味着从JavaScript更改序列中任何元素的值将导致以下三个步骤发生:将从QObject(作为QVariant,但随后被转换为正确的序列类型)中读取完整序列;更改指定索引处的元素;将完整序列写回QObject(作为QVariant)。

复制序列要简单得多,因为实际序列存储在JavaScript对象的资源数据中,所以不会发生读取/修改/写入周期(而是直接修改资源数据)。

因此,对引用序列中的元素进行写入的速度将远远慢于对复制序列中的元素进行写入。事实上,将N个元素的引用序列中的单个元素写入的成本与将N个元素的复制序列分配给该引用序列的成本相当,所以在计算过程中通常最好修改一个临时的复制序列,然后将结果分配给引用序列。

假设以下C++类型的存在及其先前注册到“Qt.example”命名空间中的存在

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

以下示例在一个紧循环中写入引用序列的元素,从而造成性能问题

由于《code class="docutils literal notranslate">"qrealListProperty[j] = j"表达式在内循环中导致QObject属性的读取和写入,这使得该代码非常低效。相反,某个功能上等效但速度要快的多的是

其次,如果序列属性中的任何元素发生更改,就会发出属性更改信号。如果您对序列属性中的特定元素有多个绑定,则最好创建一个动态属性,该属性绑定到该元素,并在绑定表达式中使用该动态属性作为符号代替序列元素,因为它只会在其值更改时导致绑定重新评估。

这是一个非常特殊的使用场景,大多数客户端永远不会遇到,但在您发现自己进行类似操作的情况下,了解这一点是有价值的

注意,即使仅在循环中修改索引2处的元素,但由于更改信号粒度是整个属性已更改,所以三个绑定都会被重新评估。因此,添加一个中间绑定有时可能是有益的

在上面的示例中,只有中间绑定每次都会重新评估,这会导致显著提高性能。

值类型技巧#

值类型属性(字体、颜色、向量3d等)具有类似于序列类型属性的QObject属性和更改通知语义。因此,上面关于序列给出的提示也适用于值类型属性。虽然它们通常与值类型问题较少(因为值类型的子属性数量通常远少于序列中的元素数量),但任何不必要的重新评估绑定数量的增加都会对性能产生负面影响。

一般性能技巧#

由于语言设计而产生的一般JavaScript性能考虑事项也适用于QML。最显著的是

  • 只要可能就避免使用eval()

  • 不要删除对象属性

常见接口元素#

文本元素#

文本布局的计算可能是一个耗时操作。尽可能使用 PlainText 格式而不是 StyledText,因为这能减少对布局引擎所需的工作量。如果不能使用 PlainText(例如需要嵌入图片或使用标签指定字符的格式化范围(粗体、斜体等)而不是整个文本),则应使用 StyledText

只有在文本可能(但可能性不大)是 StyledText 时才应使用 AutoText,因为这种模式将产生解析成本。不应使用 RichText 模式,因为 StyledText 以其成本的一小部分提供了几乎所有功能。

图片

图片是任何用户界面的重要组成部分。不幸的是,它们也是问题的一大来源,因为加载它们需要时间,它们消耗的内存量以及它们的使用方式。

异步加载

图片通常相当大,因此确保加载图片不会阻塞UI线程是很明智的。将QML Image元素的“异步”属性设置为true以启用从本地文件系统(远程图片始终异步加载)异步加载图片,而这不会对用户界面的美学产生负面影响。

将“异步”属性设置为true的Image元素将在低优先级的工作线程中加载图片。

显式源大小

如果您的应用程序加载了大图片,但又在小尺寸的元素中显示,则将“sourceSize”属性设置为您要渲染的元素的大小,以确保将图像的小缩放版本保留在内存中,而不是大版本。

请注意,更改sourceSize会导致重新加载图片。

避免运行时合成

需要注意的是,您可以通过提供与您的应用程序一起预合成的图像资源来避免在运行时执行合成工作(例如,提供带有阴影效果的元素)。

避免平滑图片

仅在需要时启用 image.smooth。它可能在某些硬件上更慢,且在图片以自然大小显示时没有视觉效果。

绘图

避免多次绘制同一区域。使用Item作为根元素,而不是Rectangle,以避免多次绘制背景。

使用锚点定位元素

相对于彼此定位项时,使用锚点而不是绑定更为高效。考虑以下绑定使用情况来定位rect2相对于rect1

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、宽度和高度属性,而不是使用锚点)相对较慢,尽管它提供了最大的灵活性。

如果布局不是动态的,指定布局最高效的方式是使用静态初始化的 x、y、width 和 height 属性。项目坐标始终相对于它们的父级,因此如果您想从父级的 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
    }
}

模型和视图#

大多数应用程序都至少有一个模型向视图提供数据。为了实现最佳性能,应用开发者需要注意一些语义。

自定义 C++ 模型#

通常,写入自定义模型以在 QML 视图中使用的分析师是很有必要的。虽然任何此类模型的最佳实现将主要取决于必须满足的用例,但以下是一些一般性指南:

  • 尽可能异步

  • 在(较低优先级)工作线程中处理所有操作

  • 批量处理后端操作,以最大程度地减少(可能是缓慢)的 I/O 和 IPC

请注意,使用低优先级的工作线程可最小化饿死 GUI 线程的风险(这可能导致感知性能变差)。此外,同步和锁定机制可能是性能缓慢的主要原因,因此应尽量避免不必要的锁定。

ListModel QML 类型#

QML 提供了一种 ListModel 类型,可用来向 ListView 提供数据。只要正确使用,它应该适用于大多数用例并且具有较高的性能。

在工作线程中进行填充#

可以在 JavaScript 中使用(低优先级)工作线程来填充 ListModel 元素。开发者必须在 WorkerScript 中显式调用 ListModel 的“sync()”,以确保更改同步到主线程。有关更多信息,请参阅 WorkerScript 文档。

请注意,使用 WorkerScript 元素将导致创建单独的 JavaScript 引擎(因为 JavaScript 引擎是线程级的)。这将导致内存使用量增加。但是,多个 WorkerScript 元素将使用同一个工作线程,因此使用第二个或第三个 WorkerScript 元素的内存影响可以忽略不计,一旦应用程序已经使用了一个。

不要使用动态角色#

QtQuick 2 中的 ListModel 元素在性能上比 QtQuick 1 要好得多。性能提升主要来自对每个给定模型的元素中角色类型的假设 - 如果类型没有变化,缓存性能将显著提高。如果类型可以元素到元素地从动态更改,这种优化变得不可能,模型的性能将降低一个数量级。

因此,动态类型默认是已禁用的;必须明确将模型的布尔“dynamicRoles”属性设置为启用动态类型(并承受相应的性能下降)。我们建议在可能的情况下,不要使用动态类型重新设计您的应用程序以避免使用它。

视图#

视图委托应当尽量简单。委托中应该只有足够多的 QML 来显示必要的信息。任何不是立即需要的功能(例如,当点击时显示更多信息)都应该在需要时才创建(请参阅即将到来的关于懒加载的章节)。

以下列表是在设计委托时应注意的一些要点总结:

  • 委托中的元素越少,创建速度越快,从而视图滚动速度也越快。

  • 尽量减少委托中的绑定数量;特别是,在委托内部使用锚点而不是绑定来实现相对定位。

  • 避免在委托中使用ShaderEffect元素。

  • 不要在委托上启用裁剪。

您可以将视图的cacheBuffer属性设置为允许在可见区域外异步创建和缓存委托。对于不太复杂且不太可能在一帧内创建的视图委托,使用cacheBuffer是推荐的做法。

请注意,cacheBuffer会在内存中保留额外的委托。因此,利用cacheBuffer带来的收益必须与额外的内存使用进行权衡。开发人员应通过基准测试找到最适合他们场景的最佳值,因为使用cacheBuffer引起的内存压力增加,在某些罕见情况下,可能导致滚动时的帧率降低。

视觉效果

Qt Quick 2包括一些功能,这些功能允许开发人员和设计师创建极具吸引力的用户界面。流畅性和动态过渡以及视觉效果可以有效地用于应用程序,但在使用QML中的一些功能时必须谨慎,因为它们可能会影响性能。

动画

通常,对属性进行动画处理会导致引用该属性的所有绑定被重新评估。通常,这正是我们想要的,但在其他情况下,可能最好在动画之前禁用绑定,并在动画完成后重新分配绑定。

避免在动画过程中运行JavaScript。例如,应避免在x属性动画的每一帧上运行复杂的JavaScript表达式。

开发者应该特别注意脚本动画的使用,因为这些在主线程中运行(因此,如果它们运行时间过长,可能会导致帧跳过)。

粒子效果

Qt Quick Particles模块允许将美丽的粒子效果无缝集成到用户界面中。然而,每个平台都有不同的图形硬件功能,粒子模块无法限制参数到硬件可以优雅支持的程度。你尝试渲染的粒子越多(粒子越大),你的图形硬件渲染60 FPS所需的性能就越高。影响更多粒子需要更快的CPU。因此,仔细测试目标平台上的所有粒子效果非常重要,以校准你可以在60 FPS下渲染的粒子的数量和大小。

请注意,当未使用时(例如,在不可见元素上)可以禁用粒子系统,以避免进行不必要的模拟。

有关更深入的信息,请参阅粒子系统性能指南。

控制元素寿命

通过将应用程序划分为简单、模块化的组件,每个组件都包含在单个QML文件中,可以缩短应用程序的启动时间,更好地控制内存使用,并减少应用程序中活跃但不可见元素的数量。

延迟初始化

QML引擎做一些复杂的事情,以尝试确保组件的加载和初始化不会导致帧跳过。然而,最好的降低启动时间的方法是避免执行不需要的工作,并在需要时再进行工作。这可以通过使用Loader或动态创建组件来实现。

使用加载器#

加载器是一种允许动态加载和卸载组件的元素。

  • 使用加载器的“active”属性,初始化可以延迟到需要时进行。

  • 使用重载的“setSource()”函数版本,可以提供初始属性值。

  • 将加载器的异步属性设置为true可能也会提高组件实例化时的流畅性。

使用动态创建#

开发者可以使用Qt.createComponent()函数,在JavaScript中动态地在运行时创建组件,然后调用createObject()来实例化它。根据调用中指定的所有者语义,开发者可能需要手动删除创建的对象。有关更多信息,请参阅从JavaScript创建动态QML对象。

销毁未使用的元素#

大多数情况下,因为它们是非可见元素的子元素而不可见的元素(例如,在显示第一个选项卡时,选项卡小部件中的第二个选项卡)应该被懒加载初始化,并在不再使用时删除,以避免让它们保持活跃所产生的持续成本(例如,渲染、动画、属性绑定评估等)。

一个使用加载器元素加载的项可以通过重置加载器的“source”或“sourceComponent”属性来释放,而其他项可以通过在它们上调用destroy()来显式释放。在某些情况下,可能需要保持项的活跃,这种情况下,至少应该使其不可见。

有关有关活跃但不可见元素的信息,请参阅接下来关于渲染的部分。

渲染#

QtQuick 2中用于渲染的场景图允许以60 FPS的流畅速度渲染高度动态的、动态的用户界面。然而,有一些事情会大大降低渲染性能,因此开发者应该尽量避免这些陷阱。

裁剪#

默认情况下裁剪是禁用的,并且只在需要时启用。

裁剪是一种视觉效果,NOT一种优化。它增加了(而不是减少了)渲染器的复杂度。如果启用裁剪,一个项将裁剪其自己的绘画以及其子项的绘画到其边界矩形。这阻止了渲染器能够自由地重新排序元素的绘制顺序,从而导致次优的最佳场景图遍历。

在委托中裁剪尤其糟糕,应该不惜一切代价避免。

过度绘制和不可见元素#

如果你有被其他(不透明)元素完全覆盖的元素,最好将其“visible”属性设置为false,否则它们将被无用地绘制。

类似地,虽然如前所述,它们仍然会因活跃而产生任何动画或绑定评估的成本,但需要初始化启动时间(例如,如果创建第二个选项卡的代价太高,不能只在选项卡被激活时进行)的不可见元素(例如,当第一个选项卡显示时选项卡小部件中的第二个选项卡)应将其“visible”属性设置为false,以避免绘制它们的成本。

半透明与不透明#

不透明内容通常比半透明内容绘制得快得多。原因是半透明内容需要混合,而渲染器可以更好地优化不透明内容。

如果一个图像中有一个半透明的像素,它将被视为完全透明,即使它大部分是不透明的。对于具有透明边缘的BorderImage也同样如此。

着色器

ShaderEffect类型使得将GLSL代码内联放置在Qt Quick应用中成为可能,而开销非常小。然而,重要的是要认识到片段程序必须为渲染形状中的每个像素运行。当部署到低端硬件,并且着色器覆盖大量像素时,应该将片段着色器限制为少量指令,以避免性能不佳。

用GLSL编写的着色器可以编写复杂的变换和视觉效果,但应该谨慎使用。使用ShaderEffectSource会引起场景在绘制之前预先渲染到FBO中。这种额外的开销可能相当高昂。

内存分配和回收

应用程序将分配的内存量及其分配方式都是非常重要的考虑因素。除了在内存受限的设备上可能出现的内存不足问题之外,在堆上分配内存是一种相当昂贵的计算操作,并且某些分配策略可能导致跨页面的数据碎片化。JavaScript使用一个自动垃圾回收的托管内存堆,这有一些优点,但也有一系列重要的含义。

用QML编写的应用程序会使用C++堆和自动管理的JavaScript堆内存。应用程序开发者需要了解每个堆的内情,以便最大限度地提高性能。

为QML应用程序开发者提供的技巧

本节中包含的技巧和建议仅为指导性,可能不适用于所有情况。请务必使用经验指标仔细基准测试和分析您的应用程序,以便做出最佳决策。

惰性实例化和初始化组件

如果您的应用程序包含多个视图(例如,多个标签页),但任何时间点只需要一个,您可以使用惰性实例化来最小化在任何给定时间需要分配的内存量。有关更多信息,请参阅有关惰性初始化的前一节。

销毁未使用的对象

如果您惰性地加载组件,或在JavaScript表达式中动态创建对象,通常最好手动执行destroy()而不是等待自动垃圾回收。有关更多信息,请参阅有关控制元素生命周期的前一节。

不要手动调用垃圾回收器

在大多数情况下,手动调用垃圾回收器并不是明智之举,因为这会在相当长的时间内阻塞GUI线程。这可能会导致跳帧和卡顿动画,应该不惜一切代价避免。

有些情况下手动调用垃圾回收器是可以接受的(在接下来的章节中将更详细地解释),但在大多数情况下,调用垃圾回收器既不必要也是适得其反的。

避免定义多个相同的隐式类型

如果一个QML元素在QML中定义了自定义属性,它就变成了它自己的隐式类型。这将在接下来的部分中解释得更详细。如果一个组件中定义了多个相同的隐式类型,则可能会浪费一些内存。在这种情况下,通常最好是显式定义一个新的组件,然后可以重用它。

自定义属性的定义往往是一种有益的性能优化(例如,减少所需的或重新评估的绑定数量),或可以提高组件的模块化和可维护性。在这些情况下,鼓励使用自定义属性。然而,如果新类型被多次使用,则应该将其拆分为它自己的组件(.qml文件),以节省内存。

重用现有组件

如果您正在考虑定义一个新的组件,值得检查一下,看看该组件是否已经存在于您的平台组件集中。否则,您将迫使QML引擎为本质上与另一个预存在的可能已经加载的组件重复的类型生成和存储类型数据。

使用单例类型而不是pragma库脚本

如果您使用pragma库脚本存储应用程序范围内的实例数据,请考虑使用QObject单例类型。这应该会导致更好的性能,并将减少使用的JavaScript堆内存量。

QML应用程序中的内存分配

QML应用程序的内存使用可能分为两部分:其C++堆使用量和JavaScript堆使用量。每部分都会有一些不可避免的内存分配,因为这些内存是由QML引擎或JavaScript引擎分配的,而其余部分取决于应用程序开发者的决策。

C++堆将包含

  • QML引擎固定且不可避免的额外开销(包括实现数据结构、上下文信息等);

  • 每个组件编译的数据和类型信息,包括每类型的属性元数据,这些数据由QML引擎根据应用程序加载的模块和组件生成;

  • 每个对象的C++数据(包括属性值)加上一个元件元对象层次结构,这取决于应用程序实例化的组件;

  • 由QML导入(库)专门分配的数据。

JavaScript堆将包含

  • JavaScript引擎本身的固定且不可避免的额外开销(包括内置的JavaScript类型);

  • 我们JavaScript集成的固定且不可避免的额外开销(加载类型的构造函数、函数模板等);

  • 每个类型的布局信息和其他在运行时由JavaScript引擎生成的内部类型数据,对于每个类型(关于类型的说明见下文);

  • 每个对象的JavaScript数据(“var”属性、JavaScript函数和信号处理程序,以及未优化的绑定表达式);

  • 在表达式评估过程中分配的变量。

此外,将有一个用于主线程的JavaScript堆分配,可选地还有一个用于WorkerScript线程的JavaScript堆分配。如果应用程序不使用WorkerScript元素,则不会产生这些开销。JavaScript堆可以有几兆字节大小,因此为内存受限的设备编写的应用程序可能最好避免使用WorkerScript元素,尽管它有助于异步填充列表模型。

请注意,QML引擎和JavaScript引擎都会自动生成关于观察类型的数据类型缓存。应用程序加载的每个组件都是一个独特的(显式)类型,而每个在QML中定义了自己的自定义属性的元素(组件实例)都是一个隐式类型。任何未定义任何自定义属性的元素(组件实例)在JavaScript和QML引擎中都被认为是其由组件显式定义的类型,而不是其自身的隐式类型。

考虑以下示例:

在上面的示例中,矩形r0r1没有自定义属性,因此JavaScript和QML引擎都将它们视为相同类型。也就是说,r0r1都被认为是显式定义的Rectangle类型。矩形r2r3r4各自有自定义属性,因此被认为是不同的(隐式)类型。请注意,尽管r3r4具有相同的属性信息,但它们各自被认为是不同的类型,仅仅是因为它们所实例化的组件未声明自定义属性。

如果r3r4都是RectangleWithString组件的实例,并且该组件的定义中包含了名为customProperty的字符串属性声明,那么r3r4将被认为是同一类型(也就是说,它们将是RectangleWithString类型的实例,而不是定义它们自己的隐式类型)。

深入考虑内存分配#

在做出关于内存分配或性能权衡的决定时,重要的是要记住CPU缓存性能、操作系统分页和JavaScript引擎垃圾收集的影响。应仔细基准测试潜在解决方案以确保选择最佳方案。

没有一套通用指南可以代替对计算机科学基本原理的深刻理解和针对应用开发者正在开发的应用程序平台的实现细节的实践知识。此外,在做出权衡决定时,没有任何理论计算可以替代一套优秀的基准测试和分析工具。

碎片化#

碎片化是C++开发问题。如果应用程序开发者未定义任何C++类型或插件,可以安全地忽略本节。

随着时间的推移,应用程序将分配大量内存,并将数据写入该内存。一旦使用了一些数据后,它会随后释放其中的一些部分。这可能导致“空闲”内存位于非连续块中,无法返回给操作系统供其他应用程序使用。它还会影响应用程序的缓存和访问特性,因为“活动”数据可能分散在物理内存的许多不同页面上。这反过来可能会迫使操作系统进行交换,从而引起文件系统的I/O操作——这是一种相对而言非常慢的操作。

可以通过使用池分配器(和其他连续内存分配器)、通过仔细管理对象生命周期以减少在任何时刻分配的内存量、通过定期清理和重建缓存或使用具有垃圾回收功能的内存托管运行时(如JavaScript)来避免碎片化。

垃圾回收#

JavaScript提供了垃圾回收功能。与C++堆相比,分配在JavaScript堆(而非C++堆)上的内存归JavaScript引擎所有。引擎将定期收集JavaScript堆上所有未引用的数据。

垃圾回收的影响#

垃圾回收具有优点和缺点。这意味着手动管理对象生命周期不再是那么重要。然而,这也意味着JavaScript引擎可能在不由应用开发者控制的时间段内发起一个可能持续很长时间的操作。除非应用程序开发者仔细考虑JavaScript堆的使用情况,否则垃圾回收的频率和持续时间可能会对应用体验产生负面影响。

手动调用垃圾回收器#

用QML编写的应用程序(很可能)需要在某个阶段进行垃圾回收。尽管当可用空闲内存量低时,垃圾回收将由JavaScript引擎自动触发,但有时让应用开发者手动调用垃圾回收器可能更好(尽管通常并非如此)。

应用开发者可能最能理解何时应用程序会闲置一段时间。如果QML应用程序使用了大量的JavaScript堆内存,在特别关键性能任务(例如列滚动、动画等)期间会导致定期且破坏性的垃圾回收周期,那么开发者在零活动期间手动调用垃圾回收器可能是一个不错的选择。空闲阶段是进行垃圾回收的理想时间,因为用户不会注意到任何由于在活动期间调用垃圾回收器而导致的用户体验下降(如跳帧、不流畅的动画等)。

可以通过在JavaScript中调用gc()手动调用垃圾回收器。这将引发一个全面的收集周期,可能需要几百到一千多毫秒才能完成,因此应尽可能避免。

内存与性能权衡#

在某些情况下,可以通过增加内存使用量来换取减少处理时间。例如,将用在封闭循环中的符号查找结果缓存到JavaScript表达式的临时变量中,在评估该表达式时将显著提高性能,但这涉及到分配一个临时变量。在某些情况下,这些权衡是合理的(如上述情况,几乎总是合理的),但有时可能最好让处理过程稍微慢一些,以避免增加系统内存压力。

在某些情况下,增加内存压力的影响可能非常严重。在某些情况下,以假设的性能提升为代价来权衡内存使用,可能会导致页面碎片的增加或缓存碎片增加,从而导致性能急剧下降。总是需要仔细基准测试权衡的影响,以确定在特定情况下哪种解决方案最好。

有关缓存性能和内存-时间权衡的详细信息,请参阅以下文章。

  • Ulrich Drepper的出色文章:“每个程序员都应该了解有关内存的事情”,在:[链接](https://people.freebsd.org/~lstewart/articles/cpumemory.pdf)。

  • Agner Fog关于优化C++应用程序的优秀手册,在:[链接](http://www.agner.org/optimize/)。