QML 性能考虑与建议¶
讨论与性能相关的权衡和最佳实践
时间考虑¶
作为应用程序开发者,你必须努力让渲染引擎实现一致的每秒60帧刷新率。60 FPS意味着每帧之间大约有16毫秒的时间可以进行处理,这包括将绘图原语上传到图形硬件所需的处理。
实际上,这意味着应用程序开发者应该:
尽可能使用异步、事件驱动的编程
使用工作线程进行重要处理
永远不要手动旋转事件循环
在阻塞函数中每帧花费的时间不要超过几毫秒
如果不这样做,将会导致跳帧,这对用户体验有极大的影响。
注意
一个诱人但永远不应该使用的模式是创建自己的QEventLoop或调用QCoreApplication::processEvents(),以避免在从QML调用的C++代码块中阻塞。这是危险的,因为当在信号处理程序或绑定中进入事件循环时,QML引擎会继续运行其他绑定、动画、过渡等。这些绑定可能会导致副作用,例如破坏包含你的事件循环的层次结构。
性能分析¶
最重要的建议是:使用Qt Creator中包含的QML分析器。了解应用程序中时间花费在哪里,将使你能够专注于实际存在的问题区域,而不是潜在的问题区域。有关如何使用QML分析工具的更多信息,请参阅Qt Creator手册。
确定哪些绑定运行得最频繁,或者您的应用程序在哪些函数上花费了最多时间,将使您能够决定是否需要优化问题区域,或者重新设计应用程序的一些实现细节,以提高性能。在没有进行性能分析的情况下尝试优化代码,可能会导致非常小的性能改进,而不是显著的性能提升。
JavaScript 代码¶
大多数QML应用程序中都会包含大量的JavaScript代码,这些代码以动态函数、信号处理程序和属性绑定表达式的形式存在。这通常不是问题。由于QML引擎中的一些优化,例如对绑定编译器所做的优化,在某些使用情况下,它可能比调用C++函数更快。然而,必须小心确保不会意外触发不必要的处理。
类型转换¶
使用JavaScript的一个主要成本是,在大多数情况下,当访问QML类型的属性时,会创建一个包含底层C++数据(或对其的引用)的外部资源的JavaScript对象。在大多数情况下,这相当便宜,但在其他情况下可能非常昂贵。一个昂贵的例子是将C++ QVariantMap Q_PROPERTY分配给QML“variant”属性。列表也可能很昂贵,尽管特定类型的序列(int、qreal、bool、QString和QUrl的QList)应该很便宜;其他列表类型涉及昂贵的转换成本(创建一个新的JavaScript数组,并逐个添加新类型,每种类型从C++类型实例转换为JavaScript值)。
在某些基本属性类型(如“string”和“url”属性)之间进行转换也可能代价高昂。使用最接近匹配的属性类型将避免不必要的转换。
如果你必须将QVariantMap暴露给QML,请使用“var”属性而不是“variant”属性。一般来说,从QtQuick 2.0及更新版本开始,“property var”应被认为优于“property variant”用于所有用例(请注意,“property variant”已被标记为过时),因为它允许存储真正的JavaScript引用(这可以减少某些表达式中所需的转换次数)。
解析属性¶
属性解析需要时间。虽然在某些情况下查找的结果可以被缓存并重复使用,但如果可能的话,最好完全避免做不必要的工作。
在下面的示例中,我们有一段经常运行的代码块(在这种情况下,它是一个显式循环的内容;但例如,它可能是一个常用的绑定表达式),在其中,我们多次解析具有“rect” id的对象及其“color”属性:
我们可以在块中只解析一次公共基础:
仅仅这个简单的改变就带来了显著的性能提升。请注意,上面的代码还可以进一步改进(因为在循环处理期间查找的属性从未改变),通过将属性解析提升到循环外部,如下所示:
属性绑定¶
如果引用的任何属性发生变化,属性绑定表达式将重新评估。因此,绑定表达式应尽可能保持简单。
如果你有一个循环,在其中进行一些处理,但只有处理的最终结果是重要的,通常最好更新一个临时累加器,然后在最后将其赋值给你需要更新的属性,而不是逐步更新属性本身,以避免在累加的中间阶段触发绑定表达式的重新评估。
以下人为设计的例子说明了这一点:
在onCompleted处理程序中的循环导致“text”属性绑定被重新评估六次(这进而导致任何依赖于文本值的其他属性绑定以及onTextChanged信号处理程序每次都被重新评估,并且每次都会布局文本以进行显示)。在这种情况下,这显然是不必要的,因为我们实际上只关心累加的最终值。
它可以重写如下:
序列提示¶
如前所述,某些序列类型速度较快(例如,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; };
以下示例在紧密循环中写入引用序列的元素,导致性能不佳:
由"qrealListProperty[j] = j"表达式引起的内循环中的QObject属性读写使得这段代码非常不理想。相反,功能上等效但速度更快的替代方案是:
其次,如果序列属性中的任何元素发生变化,则会发出该属性的更改信号。如果您对序列属性中的特定元素有许多绑定,最好创建一个动态属性,该属性绑定到该元素,并在绑定表达式中使用该动态属性作为符号,而不是序列元素,因为它只会在其值发生变化时导致绑定的重新评估。
这是一个大多数客户端永远不会遇到的异常用例,但值得了解,以防你发现自己正在做类似的事情:
请注意,即使在循环中只修改了索引为2的元素,由于更改信号的粒度是整个属性已更改,因此所有三个绑定都将重新评估。因此,添加一个中间绑定有时可能是有益的:
在上述示例中,只有中间绑定会每次重新评估,从而显著提高性能。
值类型提示¶
值类型属性(如字体、颜色、vector3d等)具有与序列类型属性相似的QObject属性和更改通知语义。因此,上述关于序列的建议也适用于值类型属性。虽然值类型通常问题较少(因为值类型的子属性数量通常远少于序列中的元素数量),但任何不必要的绑定重新评估次数的增加都会对性能产生负面影响。
通用性能提示¶
由于语言设计导致的通用JavaScript性能考虑也适用于QML。最显著的是:
如果可能的话,尽量避免使用eval()
不要删除对象的属性
常见界面元素¶
文本元素¶
计算文本布局可能是一个缓慢的操作。尽可能考虑使用PlainText格式而不是StyledText,因为这减少了布局引擎所需的工作量。如果你不能使用PlainText(因为你需要嵌入图像,或者使用标签来指定字符范围以具有某些格式(粗体、斜体等)而不是整个文本),那么你应该使用StyledText。
只有当文本可能是(但可能不是)StyledText时,才应使用AutoText,因为这种模式会产生解析成本。不应使用RichText模式,因为StyledText以较低的成本提供了几乎所有的功能。
图片¶
图像是任何用户界面的重要组成部分。不幸的是,由于加载所需的时间、消耗的内存量以及它们的使用方式,它们也是问题的主要来源。
异步加载¶
图片通常非常大,因此确保加载图片不会阻塞UI线程是明智的。将QML Image元素的“asynchronous”属性设置为true以启用从本地文件系统异步加载图片(远程图片总是异步加载),这不会对用户界面的美观产生负面影响。
具有“asynchronous”属性设置为true的图像元素将在低优先级的工作线程中加载图像。
显式源大小¶
如果你的应用程序加载了一个大图像,但在一个小尺寸的元素中显示它,请将“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、width和height属性,而不是使用锚点)相对较慢,尽管它允许最大的灵活性。
如果布局不是动态的,指定布局的最有效方式是通过静态初始化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++模型¶
通常希望在C++中编写自己的自定义模型以与QML中的视图一起使用。虽然任何此类模型的最佳实现将很大程度上取决于它必须满足的用例,但一些一般性指南如下:
尽可能异步
在(低优先级)工作线程中进行所有处理
批量处理后端操作,以最小化(可能较慢的)I/O和IPC
需要注意的是,建议使用低优先级的辅助线程,以最小化GUI线程饥饿的风险(这可能导致感知性能变差)。此外,请记住,同步和锁定机制可能是导致性能缓慢的重要原因,因此应小心避免不必要的锁定。
ListModel QML 类型¶
QML 提供了一个 ListModel 类型,可用于向 ListView 提供数据。对于大多数用例来说,它应该足够使用,并且只要正确使用,性能相对较好。
在工作线程中填充¶
ListModel 元素可以在 JavaScript 的(低优先级)工作线程中填充。开发者必须从 WorkerScript 中显式调用 ListModel 的“sync()”方法,以使更改同步到主线程。有关更多信息,请参阅 WorkerScript 文档。
请注意,使用WorkerScript元素将导致创建一个单独的JavaScript引擎(因为JavaScript引擎是每个线程的)。这将导致内存使用量增加。然而,多个WorkerScript元素都将使用相同的工作线程,因此一旦应用程序已经使用了一个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 模块允许将美丽的粒子效果无缝集成到用户界面中。然而,每个平台的图形硬件能力不同,Particles 模块无法将参数限制为您的硬件能够优雅支持的范围。您尝试渲染的粒子越多(并且它们越大),您的图形硬件就需要越快才能以 60 FPS 的速度渲染。影响更多粒子需要更快的 CPU。因此,在目标平台上仔细测试所有粒子效果以校准您可以在 60 FPS 下渲染的粒子数量和大小非常重要。
应该注意的是,当粒子系统不使用时(例如,在不可见的元素上)可以禁用它,以避免进行不必要的模拟。
有关更深入的信息,请参阅粒子系统性能指南。
控制元素生命周期¶
通过将应用程序划分为简单、模块化的组件,每个组件包含在一个单独的QML文件中,您可以实现更快的应用程序启动时间,更好地控制内存使用,并减少应用程序中活动但不可见的元素数量。
延迟初始化¶
QML引擎做了一些巧妙的事情,试图确保组件的加载和初始化不会导致帧被跳过。然而,减少启动时间的最佳方法莫过于避免做不必要的工作,并将工作推迟到必要时进行。这可以通过使用Loader或动态创建组件来实现。
使用加载器¶
加载器是一个允许动态加载和卸载组件的元素。
使用Loader的“active”属性,可以将初始化延迟到需要时进行。
使用“setSource()”函数的重载版本,可以提供初始属性值。
将Loader的异步属性设置为true也可能在组件实例化时提高流畅性。
使用动态创建¶
开发者可以使用Qt.createComponent()函数在运行时从JavaScript中动态创建一个组件,然后调用createObject()来实例化它。根据调用中指定的所有权语义,开发者可能需要手动删除创建的对象。有关更多信息,请参阅从JavaScript动态创建QML对象。
销毁未使用的元素¶
由于它们是不可见元素的子元素(例如,在标签小部件中,当显示第一个标签时,第二个标签是不可见的),在大多数情况下应该延迟初始化这些元素,并在不再使用时删除它们,以避免保持它们活动状态的持续成本(例如,渲染、动画、属性绑定评估等)。
使用Loader元素加载的项目可以通过重置Loader的“source”或“sourceComponent”属性来释放,而其他项目可以通过调用destroy()来显式释放。在某些情况下,可能需要保持项目处于活动状态,此时至少应使其不可见。
请参阅即将到来的渲染部分,以获取有关活动但不可见元素的更多信息。
渲染¶
用于在QtQuick 2中渲染的场景图允许以60 FPS流畅地渲染高度动态、动画的用户界面。然而,有一些事情可能会显著降低渲染性能,开发人员应尽可能小心避免这些陷阱。
裁剪¶
默认情况下,裁剪是禁用的,只有在需要时才应启用。
裁剪是一种视觉效果,而不是优化。它增加了(而不是减少)渲染器的复杂性。如果启用了裁剪,一个项目将裁剪自己的绘制,以及其子项的绘制,到其边界矩形。这阻止了渲染器自由地重新排序元素的绘制顺序,导致场景图遍历的最佳情况不理想。
在委托内部进行裁剪尤其糟糕,应不惜一切代价避免。
过度绘制和不可见元素¶
如果你有完全被其他(不透明)元素覆盖的元素,最好将它们的“visible”属性设置为false,否则它们将被不必要地绘制。
同样,那些不可见的元素(例如,选项卡小部件中的第二个选项卡,而第一个选项卡正在显示)但需要在启动时初始化(例如,如果实例化第二个选项卡的成本太高,无法仅在选项卡激活时进行),应该将其“visible”属性设置为false,以避免绘制它们的成本(尽管如前所述,它们仍然会产生任何动画或绑定评估的成本,因为它们仍然是活动的)。
半透明 vs 不透明¶
不透明内容的绘制速度通常比半透明内容快得多。原因是半透明内容需要混合,而渲染器可能更好地优化不透明内容。
即使图像大部分是不透明的,具有一个半透明像素的图像也被视为完全半透明。对于具有透明边缘的BorderImage也是如此。
着色器¶
ShaderEffect 类型使得在 Qt Quick 应用程序中内联 GLSL 代码成为可能,且开销非常小。然而,重要的是要意识到片段程序需要为渲染形状中的每个像素运行。当部署到低端硬件且着色器覆盖大量像素时,应将片段着色器保持为少量指令,以避免性能不佳。
用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堆用于主线程,并可选择性地分配另一个JavaScript堆用于WorkerScript线程。如果应用程序不使用WorkerScript元素,则不会产生该开销。JavaScript堆的大小可能为几兆字节,因此为内存受限设备编写的应用程序最好避免使用WorkerScript元素,尽管它在异步填充列表模型方面非常有用。
请注意,QML引擎和JavaScript引擎都会自动生成它们自己关于观察到的类型的类型数据缓存。应用程序加载的每个组件都是一个独特的(显式的)类型,而每个在QML中定义了自己自定义属性的元素(组件实例)都是一个隐式类型。任何没有定义任何自定义属性的元素(组件的实例)都被JavaScript和QML引擎视为由组件显式定义的类型,而不是它自己的隐式类型。
考虑以下示例:
在前面的例子中,矩形 r0 和 r1 没有任何自定义属性,因此 JavaScript 和 QML 引擎认为它们属于同一类型。也就是说,r0 和 r1 都被认为是显式定义的 Rectangle 类型。矩形 r2、r3 和 r4 各自具有自定义属性,并且各自被认为是不同的(隐式)类型。请注意,r3 和 r4 各自被认为是不同的类型,即使它们具有相同的属性信息,仅仅因为自定义属性没有在它们所属的组件中声明。
如果 r3 和 r4 都是 RectangleWithString 组件的实例,并且该组件定义包括一个名为 customProperty 的字符串属性的声明,那么 r3 和 r4 将被视为相同类型(即它们将是 RectangleWithString 类型的实例,而不是定义它们自己的隐式类型)。
深入的内存分配考虑¶
每当做出关于内存分配或性能权衡的决策时,重要的是要记住CPU缓存性能、操作系统分页和JavaScript引擎垃圾回收的影响。应该仔细对潜在的解决方案进行基准测试,以确保选择最佳方案。
没有任何一套通用指南可以取代对计算机科学基本原理的扎实理解,以及应用程序开发者所开发平台实现细节的实践知识。此外,在做出权衡决策时,再多的理论计算也无法取代一套良好的基准测试和分析工具。
碎片化¶
碎片化是C++开发中的一个问题。如果应用程序开发者没有定义任何C++类型或插件,他们可以安全地忽略本节。
随着时间的推移,应用程序会分配大块内存,将数据写入该内存,并在使用完部分数据后释放一些内存。这可能导致“空闲”内存位于不连续的块中,无法返回给操作系统供其他应用程序使用。这也会影响应用程序的缓存和访问特性,因为“活动”数据可能分布在物理内存的许多不同页面上。这反过来可能迫使操作系统进行交换,从而导致文件系统I/O——相对而言,这是一个极其缓慢的操作。
可以通过使用池分配器(和其他连续内存分配器)来避免碎片化,通过仔细管理对象的生命周期来减少任何一次分配的内存量,通过定期清理和重建缓存,或者通过使用带有垃圾回收的内存管理运行时(如JavaScript)。
垃圾回收¶
JavaScript 提供了垃圾回收机制。在 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/ 。