使用Qt6 和现代 c++ 进行跨平台开发 16: 性能注意事项

在本章中,我们将概述性能优化技术以及如何在基于 Qt 的应用程序开发环境中应用它们。 性能是应用程序成功的一个非常重要的因素。 绩效失败可能导致业务失败、客户关系不佳、竞争力下降和收入损失。 延迟性能优化可能会给您的声誉和组织形象带来巨大的损失。 因此,进行性能调优很重要。

您还将了解性能瓶颈以及如何克服它们。 我们将讨论不同的分析工具来诊断性能问题,特别关注一些流行的工具。 然后,您将学习如何分析和基准测试性能。 本章还介绍了 Qt 建模语言 (QML) Profiler 和 Flame Graph,以查找 Qt Quick 应用程序中的潜在瓶颈。 您还将了解在开发 Qt 应用程序时应遵循的一些最佳实践。

我们将讨论以下主题:

• 了解性能优化

• 优化 C++ 代码

• 使用并发、并行和多线程

• 使用 QML Profiler 和 Flame Graph 分析 Qt Quick 应用程序

• 其他Qt Creator 分析工具

• 优化图形性能

• 创建基准

• 不同的分析工具和优化策略

• Qt Widgets 的性能注意事项

• 学习 QML 编码的最佳实践

到本章结束时,您将学会为基于 C++ 和 QML 的应用程序编写高性能优化代码。

了解性能优化

性能优化是为了提高应用程序的性能。 您可能想知道为什么这是必要的。 应用程序需要性能优化的原因有很多。 当您的用户或质量保证 (QA) 团队报告性能问题时,开发人员可能会发现影响整体应用程序性能的问题。 这可能是由于底层硬件限制、代码实施不当或可扩展性挑战所致。

优化是应用程序开发过程的一部分。 这可能涉及优化代码以提高性能或优化内存使用。 优化旨在优化应用程序的行为,以满足产品对速度、内存占用、电源使用等的要求。 因此,优化几乎与生产阶段的编码功能一样重要。 客户可能会将性能问题报告为故障、响应缓慢和缺少功能。 速度更快的应用程序执行效率更高,同时消耗的资源更少,并且可以在与速度较慢的应用程序相同的时间内处理更多任务。 在当今竞争激烈的世界中,更快的软件意味着相对于竞争对手的竞争优势。 性能在嵌入式和移动平台上非常重要,速度、内存和功耗等因素很普遍。

在瀑布流程中,性能改进是在应用程序开发之后的集成和验证阶段进行的。 然而,在当今的敏捷世界中,应该每隔几个冲刺评估一次代码性能,以评估应用程序的整体性能。 性能优化是一个持续的过程,而缺陷修复是一次性任务。 这是一个迭代过程,在这个过程中,您总能找到需要改进的地方,并且您的应用程序总有改进的余地。 根据约束理论 (Theory of Constraints, TOC),复杂应用程序中通常存在一个问题,该问题会限制应用程序实现其最佳性能。 这种约束被称为瓶颈。 应用程序的最佳性能受到瓶颈的限制,因此您应该在应用程序开发生命周期中考虑性能优化。 如果忽视,您的新产品可能会变成一场彻底的灾难,甚至可能毁掉您的声誉。

在开始优化之前,您应该定义一个目标。 然后,您应该确定瓶颈或约束。 之后,考虑如何修复约束。 您可以改进代码并重新评估性能。 如果达不到既定目标,则需要重复该过程。 但是,请记住,过早的优化可能是万恶之源。 在验证您的产品和实施早期用户的反馈之前,您应该首先实施主要功能。 请记住首先让应用程序运行,然后使其功能正确,然后使其更快。

当您设定绩效目标时,您需要选择正确的技术。 可以有多个目标,例如更快的启动时间、更小的应用程序二进制文件或更少的随机存取内存 (RAM) 使用。 一个目标可以影响另一个目标,所以你必须找到一个

基于预期标准的平衡——例如,优化代码以提高性能可能会影响内存优化。 可能有不同的方法来提高整体性能; 但是,您还应该遵循组织编码指南和最佳实践。 如果您正在为开源项目做贡献或者是一名自由应用程序开发人员,您应该遵循标准编码实践以保持整体代码质量。

下面列出了我们将遵循的一些用于提高性能的重要技巧:

• 使用更好的算法和库

• 使用最佳数据结构

• 负责任地分配内存并优化内存

• 避免不必要的复制

• 去除重复计算

• 增加并发性

• 使用编译器二进制优化标志

在以下部分中,我们将讨论提高 C++ 代码整体应用程序性能的机会。

优化 C++ 代码

在大多数 Qt 应用程序中,很大一部分编码是用 C++ 完成的,因此您应该了解 C++ 优化技巧。 本节介绍在编写 C++ 代码时实施一些最佳实践。 当 C++ 实现在没有优化的情况下编写时,它们运行缓慢并且消耗大量资源。 更好地优化 C++ 代码还可以更好地控制内存管理和复制。 有很多改进算法的机会,从小的逻辑块到使用标准模板库 (STL),再到编写更好的数据结构和库。 关于这个主题有几本优秀的书籍和文章。 我们将讨论一些关于更快地运行代码和使用更少资源的要点。

此处列出了一些重要的 C++ 优化技术:

• 关注算法,而不是微观优化

• 不要构造对象并进行不必要的复制

• 使用 C++11 功能,例如移动构造函数、lambda 和 constexpr 函数

• 选择静态链接和位置相关代码

• 首选 64 位代码和 32 位数据

• 尽量减少数组写入并更喜欢数组索引而不是指针

• 更喜欢常规的内存访问模式

• 减少控制流

• 避免数据依赖

• 使用最佳算法和数据结构

• 使用缓存

• 使用预先计算好的表格来避免重复计算

• 更喜欢缓冲和批处理

由于本书要求读者具备 C++ 知识,因此我们希望您了解这些最佳实践。 作为 C++ 程序员,请始终了解最新的 C++ 标准,例如 C++17 和 C++20。 这些将帮助您编写具有强大功能的高效代码。 我们不会在本节中详细讨论这些,而是将其留给您自行探索。

您可以在以下链接中阅读有关 C++ 核心指南的更多信息:https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines

您可以在以下链接了解有关优化 C++ 代码的更多信息:Software optimization resources

通过列出的方法改进您的 C++ 代码。 接下来,我们将在下一节讨论如何使用并发和多线程来提高应用程序性能。

使用并发、并行和多线程

由于您已经是一名 C++ 开发人员,您可能知道这些术语,它们可以互换使用。 但是,这些术语存在差异。 让我们在这里重新审视这些术语:

• 并发是多个程序同时执行(并发)。

• 并行是利用多核处理器中的多个内核同时并行运行程序的一部分。

• 多线程是中央处理单元(CPU) 为同一程序运行多个线程的能力,由操作系统同时支持。

例如,您可以启动可移植文档格式 (PDF) 阅读器和 Qt Creator 的多个实例。 Qt Creator 可以单独运行多个工具。 您的系统任务管理器可以向您显示同时运行的所有进程。 这称为并发。 它通常也称为多任务处理。

但是如果你使用并行计算技术来处理你的数据,那么这就叫做并行。 具有大量数据处理要求的复杂应用程序使用此技术。 请注意,单核处理器上的并行计算是一种错觉。

线程是进程的最小可执行单元。 一个进程中可以有多个线程,但只有一个主线程。 多线程是同一进程内的并发。 传统的单线程应用程序只使用一个内核。 具有多个线程的程序可以分布到多个内核,从而实现真正的并发。 因此,多线程应用程序可在多核硬件上提供更好的性能。

下面讨论Qt中提供并发和多线程的几个重要类,如下:

• QThread 用于管理程序中的一个控制线程。

• QThreadPool 用于管理和回收单个QThread 对象,以帮助减少多线程应用程序中的线程创建成本。

• QRunnable 是一个接口类,用于表示需要执行的任务或代码段。

• QtConcurrent 提供高级应用程序编程接口 (API),有助于在不使用低级线程原语的情况下编写多线程程序。

• QFuture 允许线程与稍后可用的多个计算结果同步。

• QFutureWatcher 使用信号和槽提供有关QFuture 对象的信息和通知。

• QFutureSynchronizer 是一个方便的类,它简化了一个或多个 QFuture 对象的同步。

线程主要用于两种场景,如下:

• 利用多核 CPU 加速处理

• 将长时间运行的处理或阻塞调用卸载到其他线程,以保持图形用户界面 (GUI) 线程或其他时间关键线程的响应

让我们简要讨论称为线程的最基本的并发概念。 QThread 类在 Qt 中使用便利方法提供线程抽象。 您可以通过子类化 QThread 类来启动一个新的自定义线程,如下所示:

class CustomThread : public QThread
{
 public:
 void run(){…}
};

您可以创建此类的新实例并调用其 start() 函数。 这将创建一个新线程,然后在这个新线程的上下文中调用 run() 函数。 另一种方法是直接创建一个 QThread 对象并调用 start() 函数,这将启动一个事件循环。 与传统的 C++ 线程类相比,QThread 支持线程中断,这在 C++11 及更高版本中不受支持。 您可能想知道为什么我们不能只使用 C++ 标准线程类。 这是因为您可以通过 QThread 以多线程安全的方式使用信号和槽机制。

您还可以使用 WorkerScript 在 QML 中使用多线程机制。 JavaScript 代码可以使用 WorkerScript QML 类型与 GUI 线程并行执行。 要在 Qt Quick 应用程序中启用线程,请按如下方式导入模块:

import QtQml.WorkerScript

一个 JavaScript 可以附加到每个 WorkerScript 对象。 当调用 WorkerScript.sendMessage() 时,脚本将在不同的线程和 QML 上下文中运行。 脚本完成后,它可以向 GUI 线程发送响应,调用 WorkerScript.onMessage() 信号处理程序。 您可以使用信号和信号处理程序在线程之间交换数据。 我们来看一个简单的WorkerScript用法,如下:

WorkerScript {
 id: messagingThread
 source: "messaging.mjs"
 onMessage: (messageObject)=> textElement.text = 
 messageObject.reply
}

前面的代码片段使用 JavaScript 文件 messaging.mjs,它在新线程中执行操作。 我们来看示例脚本,如下:

WorkerScript.onMessage = function(message) {
 //Perform complex operations here
 WorkerScript.sendMessage({ 'reply': 'Message '+ message})
}

您可以通过单击按钮或基于某些用户操作来发送消息。 它将调用 sendMessage(jsobject message) 方法,您的复杂消息传递操作将在该方法中发生。 您可以在以下链接中阅读有关不同线程机制和用例的更多信息:Multithreading Technologies in Qt

由于本书是为经验丰富的 C++ 开发人员编写的,因此希望您熟悉互斥锁、信号量、读写锁等术语。 Qt 提供了方便的类来在实现多线程应用程序时使用这些机制。 我们不会通过示例深入研究这些 Qt 类。 您可以在以下链接了解更多关于 QMutex、QSemaPhore、QReadWriteLock 和 QWaitCondition 的使用:Synchronizing Threads

在本节中,我们了解了如何使用并发机制来提高整体应用程序性能。 不要为简单任务不必要地实施它,因为这可能会导致性能下降。 在下一节中,我们将讨论使用 QML Profiler 工具来分析 Qt Quick 应用程序。

使用 QML Profiler 和 Flame Graph 分析 Qt Quick 应用程序

Qt 6 中的 QML 利用图形处理单元 (GPU) 并使用硬件加速进行渲染。 这个特性使得 QML 在性能上优于 Qt Widgets。 但是,您的 QML 代码中可能存在影响整体应用程序性能的瓶颈。 在本节中,我们将重点介绍使用内置工具来查找这些瓶颈。 Qt Creator 提供与多种工具的无缝集成。 最重要的工具是 QML Profiler。 它由 Qt 提供,适用于所有 Qt 支持的平台。 除了 QML Profiler,Qt Creator 还提供了 Valgrind、Heob 和 Performance Analyzer 等第三方工具。 您可以启用新插件或从“关于插件(About Plugins)...”中删除一些插件,这些插件位于“帮助”菜单下。

让我们讨论 QML Profiler,您将在大部分时间使用它来查找 QML 代码中的瓶颈。 QML Profiler 的目标是通过为您提供代码块执行特定操作所花费的时间等详细信息来帮助您识别瓶颈,之后您可以决定使用合适的 GUI 元素或更好的数据结构或算法重新实现代码 .

按照以下步骤开始分析和优化您的 Qt Quick 应用程序:

1. 打开一个现有的 Qt Quick 项目或使用 Qt Creator 的新建项目创建向导创建一个新的 Qt Quick 应用程序。

2. 创建项目后,向其中添加一些代码。 然后,选择 Analyze 菜单下的 QML Profiler 以运行 QML Profiler 工具。 根据安装的插件,分析上下文菜单可能因平台而异。 以下屏幕截图显示了 Windows 平台中的 QML Profiler 选项。 在 Linux 中,您可能会看到更多选项,例如 Valgrind Memory Analyzer、Valgrind Memory Analyzer with GDB 和 Valgrind Function Profiler:

图 12.1 – Qt Creator 集成开发环境 (IDE) 中的 QML Profiler 选项

3. 当您点击 QML Profiler 选项时,您的 Qt Quick 应用程序将由 QML Profiler 运行。 您将看到 QML Profiler 窗口出现在代码编辑器下方。 您可能还会看到以下消息:

图 12.2 – QML Profiler 重试消息

4. 如果您收到此弹出窗口,只需点击重试。 您会注意到分析将开始,您还会注意到输出屏幕。 在示例应用程序中,我们通过单击鼠标创建新的矩形,如以下屏幕截图所示:

图 12.3 – 示例 Qt Quick 应用程序的输出

5. 在用户界面 (UI) 上,执行一些用户交互(例如单击按钮)以执行特定操作。 然后,单击位于探查器窗口标题栏上的停止按钮。 您还会在“停止”按钮的两侧看到另外两个按钮。 如果将鼠标悬停在它们上面,您将看到它们的功能,例如 Start QML Profiler analysis 和 Disable Profiling。

以下屏幕截图显示了 QML Profiler 窗口的概览:

图 12.4 – 显示停止按钮和选项卡式视图的 QML Profiler 窗口

6. 停止分析器后,您将看到 QML 分析器窗口更新了一些视图。 您会注意到分析器窗口下有三个选项卡,即时间线、火焰图和统计信息。

7. 让我们看一下 QML Profiler 上的第一个选项卡——单击“时间线”选项卡。 以下屏幕截图显示了输出的示例视图:

图 12.5 – QML Profiler 显示时间线细节

您会注意到时间轴显示下有六个不同的部分:场景图、内存使用、编译、创建、绑定和 JavaScript。 这些部分向我们概述了应用程序处理的不同阶段,例如编译、组件创建和逻辑执行。

8.您可以在时间线上找到彩色条。 您可以使用鼠标滚轮放大和缩小特定的时间轴部分。 您还可以通过在时间线底部区域按下鼠标左键来移动时间线,然后向任一方向移动以找到感兴趣的区域。

以下屏幕截图说明了“时间轴”选项卡的不同部分:

图 12.6 – 显示不同部分的时间轴选项卡

9. 您可以单击展开按钮以查看每个部分下的更多详细信息,如以下屏幕截图所示:

图 12.7 – 时间轴选项卡显示场景图和分析选项下的不同子部分

10. 如果单击“创建”部分下的其中一个栏,您可以找到组件详细信息,例如 QtQuick/Rectangle 类型、创建对象所花费的总时间以及代码在弹出窗口中显示的位置,超过 QML Profiler 窗口。 您可以使用左上角的黄色箭头跳转到上一个或下一个事件。 此部分在以下屏幕截图中进行了说明:

图 12.8 – 创建部分下对象的详细信息

11. 您可以在 QML Profiler 窗口底部的不同选项卡之间切换。 浏览完 Timeline 选项卡后,让我们打开 Flame Graph 选项卡。 在此选项卡下,您将以百分比的形式找到应用程序的总时间、内存和分配的可视化。 您可以通过单击位于 QML Profiler 窗口右上角的下拉菜单在这些视图之间切换,如以下屏幕截图所示:

图 12.9 – 显示分配视图的火焰图

12. Flame Graph 视图提供了更紧凑的统计摘要。 水平条描绘了为特定功能收集的样本的一个方面与所有样本组合的相同方面的比较。 嵌套表示调用树,例如显示哪些函数调用另一个函数。

13. 如以下屏幕截图所示,您还可以看到代码编辑器左侧显示的百分比值。 根据哪个组件消耗更多时间,您可以调整代码:

图 12.10 – QML Profiler 显示代码特定部分花费的时间百分比

14. 由于数据收集需要时间,您可能会注意到数据显示前有一点延迟。 当您单击 Enable Profiling 按钮时,数据将传输到 QML Profiler,因此不要立即终止应用程序。

15. 要禁用启动应用程序时自动开始数据收集,请选择“禁用分析”按钮。 当您切换按钮时,数据收集将再次开始。

16. 让我们转到下一个选项卡:QML Profiler 窗口。 此选项卡显示有关表结构中进程的统计详细信息。 以下屏幕截图说明了我们示例代码的代码执行统计信息:

图 12.11 – QML Profiler 显示代码执行的统计数据

17. 也可以通过Analyze菜单下的QML Profiler(Attach to Waiting Application)将QML Profiler附加到外部启动的应用程序。 选择该选项后,您将看到以下对话框:

图 12.12 – QML Profiler 显示远程执行选项

18. 要保存所有收集的数据,请右键单击任何 QML Profiler 视图并在上下文菜单中选择保存 QML 跟踪。 您可以选择加载 QML 跟踪以查看保存的数据。 您还可以将保存的数据发送给其他人以供查看或加载他们保存的数据。

在本节中,我们讨论了 QML Profiler 中可用的不同选项。 通过使用此工具,您可以轻松找到导致性能问题的代码。 此链接提供了更多详细信息:Profiling QML Applications

在下一节中,我们将进一步讨论如何使用其他分析工具来优化您的 Qt 代码。

其他 Qt Creator 分析工具

在前面的部分中,我们讨论了 QML Profiler,但您可能需要分析您的 C++ 和 Qt Widgets 代码。 Qt Creator 提供与一些著名分析工具的集成,以帮助您分析您的 Qt 应用程序。 此处列出了 Qt Creator 附带的一些工具:

• Heob

• 性能分析器(Performance Analyzer)

• Valgrind

• Clang 工具:Clang-Tidy 和 Clazy

• Cppcheck

• Chrome 跟踪格式 (CTF) 可视化工具

在进入它们的文档之前,让我们简要讨论一下这些工具并熟悉它们。

要使用 Heob,您首先需要下载并安装它。 使用 Heob 可以轻松检测到缓冲区溢出和内存泄漏。 它通过覆盖调用进程的堆函数来工作。 当发生缓冲区溢出时会引发访问冲突,并且记录违规代码和缓冲区分配的堆栈跟踪。 当应用程序正常退出时,您会发现堆栈跟踪。 它不需要对目标应用程序进行任何重新编译或重新链接。

您可以在 Detecting Memory Leaks with Heob 的官方文档链接上了解它的用法

您可以从 SourceForge.net 下载二进制文件或从源代码构建它。 Heob 的源代码可以在以下链接找到:GitHub - ssbssa/heob: Detects buffer overruns and memory leaks.

Linux Performance Analyzer 工具与 Qt Creator 集成,可用于分析应用程序在 Linux 桌面或基于 Linux 的嵌入式系统上的 CPU 和内存利用率。 perf 工具使用 Linux 内核附带的实用程序定期拍摄应用程序调用树的快照,并在时间线视图或火焰图中将它们可视化。 您可以从 Analyze 菜单下的 Performance Analyzer 选项在您的 Linux 机器上启动它,如以下屏幕截图所示:

图 12.13 – Qt Creator 显示性能分析器选项

请注意,性能分析器无法在 Windows 平台上运行。 即使在 Linux 发行版上,如果它找不到 perf 实用程序,您也会看到一个等效的警告对话框,如下一个屏幕截图所示:

图 12.14 – Qt Creator 显示性能分析器警告对话框

使用以下命令在您的 Ubuntu 计算机上安装 perf 工具:

$sudo apt install linux-tools-common

如果您使用的是不同的 Linux 发行版,则可以使用相应的命令。 对于特定的 Linux 内核,perf 可能会失败,并显示有关内核版本的警告。 在这种情况下,使用适当的内核版本键入以下命令:

$sudo apt install linux-tools-5.8.0-53-generic

完成 perf 设置后,您可以使用以下命令在命令提示符中看到预定义的事件:

$perf list

接下来,启动 Qt Creator 并打开一个 Qt 项目。 从分析菜单中选择性能分析器。 一旦您开始检查应用程序,Performance Analyzer 将开始收集数据,并且 Recorded 字段将显示持续时间的详细信息。 由于数据是通过 perf 工具处理的,并且 Qt Creator 中包含一个额外的辅助程序,因此它可能会在创建后几秒钟出现在 Qt Creator 中。 处理延迟字段包含对此延迟的估计。 数据收集将继续,直到您单击停止收集配置文件数据按钮或关闭应用程序。

您还可以从 Analyze 菜单下的 Performance Analyzer Options 加载 perf.data 并分析应用程序,如下所示:

图 12.15 – 显示性能分析器选项的上下文菜单

您可以在以下链接中阅读有关性能分析器用法的更多信息:Analyzing CPU Usage

在 macOS 上,有一个名为 Instructions 的等效工具; 但是,它没有与 Qt Creator 集成。 您可以单独启动它并查看 Time Profiler 部分。

在 Linux 和 macOS 上,Valgrind 是调试各种问题的首选工具。 分析和内存检查等个别技术用于专门分析。 Qt Creator 中的分析菜单结合了 Valgrind,并允许从 IDE 中进行内存测试和分析。 要使用 Valgrind,必须安装它。 它在 Windows 上不可用。 但是,由于内存问题通常不是特定于平台的,因此您可以在 Linux 或 macOS 上进行分析。 KCachegrind 是 Valgrind 分析结果的可视化工具。 当您运行 Valgrind 时,您会注意到使用 memcheck 打开的分析器窗口。 您可以从探查器下拉选项将其更改为 callgrind。

您可以通过以下链接了解有关 Valgrind 的更多信息:Using Valgrind Code Analysis Tools

Qt Creator 中的下一个可用工具是 Clang-Tidy 和 Clazy……。 这些工具可用于通过静态分析定位 C++ 代码中的问题。 Clang-Tidy 为常见的编程错误(例如样式违规或界面误用)提供诊断和修复。 另一方面,Clazy 强调了与 Qt 相关的编译器错误,例如浪费的内存分配和 API 使用,并建议进行重构活动以解决一些问题。 Clang-Tidy 包括 Clang 静态分析器功能。 您无需单独设置 Clang 工具,因为它们已分发并与 Qt Creator 集成。 当您运行 Clang-Tidy 和 Clazy… 时,如下面的屏幕截图所示,您将在 Profiler 窗口下看到分析详细信息,在代码编辑器下方的 Application Output 窗口下看到进度:

图 12.16 – 显示 Clang-Tidy and Clazy… 选项的上下文菜单

让我们在现有的 Qt 示例上运行该工具。 在应用程序窗口中,您将看到正在运行的分析,而在探查器窗口中,您将看到结果。

您可以通过以下链接进一步浏览文档:Using Clang Tools

Qt Creator 还包括另一个名为 cppcheck 的工具。 该工具与 Qt Creator 进行了实验性集成。 您可以从“帮助”菜单下的“关于插件...”中启用它。 您可以使用它来检测未定义的行为和危险的编码结构。 该工具提供了检查警告、样式、性能、可移植性和信息的选项。

最后一个与 Qt Creator 集成的分析工具是 CTF 可视化工具。 您可以将其与 QML Profiler 一起使用。 跟踪信息可能会让您进一步了解 QML Profiler 收集的数据。 您会发现为什么一个简单的绑定需要这么长时间,例如可能受到 C++ 代码或磁盘操作缓慢的影响。 全堆栈跟踪可用于从顶层 QML 或 JavaScript 向下跟踪到 C++,一直向下跟踪到内核区域。 这使您可以评估应用程序的性能,并确定性能不佳是由 CPU 还是同一系统上的其他程序引起的。 跟踪可以深入了解系统正在做什么以及应用程序为何以不希望的方式运行。 要查看 Chrome 跟踪事件,请使用 CTF 可视化工具。

您可以通过以下链接了解有关 CTF 可视化工具的更多信息:Visualizing Chrome Trace Events

在本节中,我们讨论了 Qt Creator 中可用的不同分析工具。 在下一节中,我们将进一步讨论如何优化和定位图形性能问题。

优化图形性能

我们在第 8 章“图形和动画”中讨论了图形和动画。 在本节中,我们将探讨影响图形和动画性能的因素。 图形性能在任何应用程序中都是必不可少的。 如果您的应用程序实施不当,则用户可能会看到 UI 闪烁,或者 UI 可能不会按预期更新。 作为开发人员,您必须尽一切努力确保渲染引擎保持 60 帧每秒 (FPS) 的刷新率。 每帧之间只有 16 毫秒 (ms),处理速度应为 60 FPS,其中包括将绘图图元上传到图形硬件所需的处理。

为避免图形性能出现任何故障,您应该尽可能使用异步的、事件驱动的编程。 如果你的应用程序有庞大的数据处理需求和复杂的计算,那么使用工作线程来处理。 您永远不应该手动旋转事件循环。 在阻塞函数上每帧花费的时间不要超过几毫秒。 如果您不遵循这些要点,用户将看到 GUI 闪烁或冻结,从而导致糟糕的用户体验 (UX)。 在 UI 上生成图形和动画时,QML 引擎非常高效和强大。 但是,您可以使用一些技巧使事情变得更快。 不要自己编写,而是利用 Qt 6 的内置功能。

绘制图形时,应尽可能选择不透明的图元。 不透明图元可以更快地由渲染器渲染并在 GPU 上绘制。 因此,在便携式网络图形 (PNG) 和联合图像专家组 (JPEG) 文件之间,呈现 JPEG 格式的速度更快。 将照片传递给 QQuickImageProvider 时,您应该使用 QImage::Format_RGB32。 请注意,不能对重叠的复合项进行批处理。 尽可能避免裁剪,因为它会破坏批处理。 不是裁剪图像,而是使用 QQuickImageProvider 生成裁剪图像。 需要单色背景的应用程序应该使用 QQuickWindow::setColor() 而不是顶级 Rectangle 元素。

QQuickWindow::setColor() 调用 glClear(),速度更快。使用 Image 时,请使用 sourceSize 属性。 sourceSize 属性使 Qt 能够在将图像加载到内存之前缩小图像的大小,确保巨大的图像消耗的内存不会超过所需的内存。 当 smooth 属性设置为 true 时,Qt 会过滤图像,使其在缩放或改变原始大小时看起来更平滑。 如果图像以与其 sourceSize 属性相同的大小呈现,则这没有区别。 在某些较旧的硬件上,此属性会影响应用程序的性能。 抗锯齿属性指示 Qt 平滑图像边缘周围的锯齿伪影。 此属性将影响您的程序的性能。

通过有效的批处理可以获得更好的图形性能。 渲染器可以提供有关批处理运行情况、使用了多少批次、保留哪些批次、哪些不透明以及哪些不透明的统计信息。 要启用此功能,请添加环境变量(例如 QSG_RENDERER_DEBUG)并将值设置为渲染。除非图像太大,否则 Image 和 BorderImage QML 类型会使用纹理图集。 如果您使用 C++ 创建纹理,则调用 QQuickWindow::createTexture() 并传递 QQuickWindow::TextureCanUseAtlas。 您可以使用另一个环境变量 QSG_ATLAS_OVERLAY 为图集纹理着色,这有助于轻松识别它们。

为了可视化场景图默认渲染器的各个方面,可以将 QSG_VISUALIZE 环境变量设置为其中一个值。 您可以在 Qt Creator 中执行此操作,方法是转到“项目”选项卡,展开“构建环境”部分,单击“添加”,然后输入变量名称 QSG_VISUALIZE 并设置该变量的值,如下所示:

• QSG_VISUALIZE = overdraw

• QSG_VISUALIZE = batches

• QSG_VISUALIZE = clip

• QSG_VISUALIZE = changes

当 QSG_VISUALIZE 设置为透支时,透支在渲染器中可视化。 为了突出透支,所有元素都在三个维度 (3D) 中可视化。 在某种程度上,此模式也可用于识别视口外的几何体。 半透明项目显示为红色调,而不透明项目显示为绿色调。 视口的边界框显示为蓝色。 不要仅仅为了绘制白色背景而使用 Rectangle,因为 Window 也有白色背景。 在这种情况下,使用 Itemproperty 而不是 Rectangle 可以提高性能。

将 QSG_VISUALIZE 设置为批次会导致批次在渲染器中可视化。 未合并的批次使用对角线图案绘制,而合并的批次使用纯色绘制。 少量不同的颜色表示有效的批处理。 如果未合并的批次包含大量单个节点,则它们是不可取的。 所有派生自 Item 的 QML 组件都有一个名为 clip 的属性。 默认情况下,裁剪值设置为 false。 此属性通知场景图不要渲染任何超出其父元素边界的子元素。 当 QSG_VISUALIZE 设置为裁剪时,场景顶部会出现红点以指示裁剪。 因为默认情况下 Qt Quick Items 不裁剪,裁剪通常不会显示。 裁剪会阻止将多个组件一起批处理的能力,这会影响图形性能。

当 QSG_VISUALIZE 设置为更改时,将显示渲染器中的更改。 随机颜色的闪烁叠加用于突出显示场景图中的变化。 对基元的修改以纯色显示,但对祖先的更改(例如矩阵或不透明度的更改)以图案显示。

在您的 Qt Quick 应用程序中试验这些环境变量。 您可以在以下链接了解有关这些渲染标志的更多信息:Qt Quick Scene Graph Default Renderer

Qt Quick 有助于构建具有流畅 UI 和动态转换的出色应用程序。 但是,您应该考虑一些因素以避免性能影响。 当您将动画添加到属性时,所有绑定都会受到影响并重新评估,这会引用该属性。 为避免性能问题,您可以在运行动画之前删除绑定,然后在动画完成后重新分配它。 在动画期间,避免使用 JavaScript。 应谨慎使用脚本动画,因为它们在主线程中运行。

您可以使用 Qt Quick 粒子来创建漂亮的粒子效果。 但是,其性能取决于底层硬件功能。 要渲染更多粒子,您将需要更快的图形硬件。 您的图形硬件应该能够以 60 FPS 或更高的速度绘制。 您可以在以下链接中了解有关优化粒子性能的更多信息:

Particle System Performance Guide

在本节中,我们讨论了优化图形性能的不同注意事项。 在下一节中,我们将进一步讨论如何对您的应用程序进行基准测试。

创建基准

我们在第 9 章“测试和调试”中了解了基准测试。 让我们看一下用于评估性能问题的基准测试的某些方面。 我们已经讨论过 Qt Test 对基准测试的支持,基准测试是计算特定任务所需的平均时间。 QBENCHMARK 宏用于对函数进行基准测试。

以下代码片段显示了对行编辑的基准键点击:

void LineEditTest::testClicks()
{
 auto tstLineEdit = ui->lineEdit;
 QBENCHMARK {QTest::keyClicks(tstLineEdit, "Some  Inputs");}
}

您还可以对 Qt 提供的便利函数进行基准测试。 以下代码对 QString::localeAwareCompare() 函数进行基准测试。 让我们看看这里的示例代码:

void TestQStringBenchmark::simpleBenchmark()
{
 QString string1 = QLatin1String("Test string");
 QString string2 = QLatin1String("Test string");
 QBENCHMARK {string1.localeAwareCompare(string2);}
}

您还可以在 QML 中运行基准测试。 Qt 基准测试框架将多次运行名称以 benchmark_ 开头的函数,并记录运行的平均时间值。 它类似于 QTestLib C++ 版本中的 QBENCHMARK 宏。 您可以在测试函数名称前加上 benchmark_once_ 以获得 QBENCHMARK_ONCE 宏的效果。

您还可以使用 Qt Labs 提供的 qmlbench 工具。 这是一个基准测试工具,将您的 Qt 应用程序作为单个堆栈而不是孤立地进行评估,基准测试可以深入了解 Qt 应用程序的整体性能。 它有几个带有内置基准测试逻辑的现成外壳。 您可以使用 qmlbench 执行两种不同类型的基准测试,例如普通基准测试或 CreationBenchmark。 它还允许您执行自动和手动基准测试。 自动化测试可用于回归测试,而手动测试可用于了解新硬件的功能。 它带有 FPS 计数器等内置功能,这对于 GUI 应用程序非常重要。 您可以通过运行以下命令找到帧速率:

>qmlbench --shell frame-count

您还可以使用一个简单的命令运行所有自动化测试,如下所示:

>qmlbench benchmarks/auto/

要探索有关该工具的更多信息并查看示例,请参考以下链接:

https://github.com/qt-labs/qmlbench

我们已经看到在 Qt Widgets 和 QML 中对对象创建进行基准测试,我们还对 Qt 函数进行了基准测试。 您也可以在不使用任何宏的情况下进行分析。 您可以简单地使用 QTime 或 QElapsedTimer 来测量部分代码或函数所花费的时间,如以下代码片段所示:

QTime* time = new QTime;
time->start();
int lastElapsedTime = 0;
qDebug()<<"Start:"<<(time->elapsed()-
 lastElapsedTime)<<"msec";
//Do some operation or call a function
qDebug()<<"End:"<<(time->elapsed()-
 lastElapsedTime)<<"msec";

在前面的代码片段中,我们使用了 elapsed() 来测量代码段所花费的时间。 不同之处在于您可以在一个函数内评估几行代码——您不必编写单独的测试项目。 这是一种无需评估整个项目即可快速发现性能问题的方法。

您还可以对 Qt Quick 3D 应用程序进行基准测试。 这是一篇关于如何做的文章:Introducing Qt Quick 3D Benchmarking Application

在本节中,我们讨论了基准测试技术。 在下一节中,我们将讨论更多的分析工具。

不同的分析工具和优化策略

您可以在多个级别优化您的应用程序,而不仅仅是在代码级别。 优化也可以在内存或二进制文件中完成。 您可以修改您的应用程序,使其使用更少的资源更有效地工作。 但是,可以在内存和性能之间进行权衡。 根据您的硬件配置,您可以决定内存使用或处理时间是否重要的策略。 在某些具有内存限制的嵌入式平台中,您可以允许处理时间稍长一些,以使用更少的内存并保持应用程序的响应速度。 您还可以将部分优化任务委托给编译器。

让我们来看看我们可以用来更快地构建、分析和部署的不同策略。

内存分析和分析工具

在本节中,我们将讨论一些可用于分析应用程序的附加工具。 请注意,我们不会详细讨论这些工具。 您可以访问相应的工具网站并从其文档中学习。 除了 Qt Creator 中的可用工具外,您还可以在 Windows 计算机上使用以下工具。

我们来看看工具列表,如下:

• AddressSanitizer (ASan) 是Google 打造的地址监控工具,属于Sanitizers 的一部分。

• AQTime Pro 通过应用程序运行时分析和性能分析发现问题和内存泄漏。

• Deleaker 是一款适用于希望在其项目中查找所有可能的已知泄漏的 C++ 开发人员的工具。 它可以检测内存泄漏、图形设备接口 (GDI) 泄漏和其他泄漏。

• Intel Inspector XE 是Intel 的内存和线程调试器。

• PurifyPlus 是一个运行时分析工具套件,可在程序运行时对其进行监控并报告其行为的关键方面。

• Visual Leak Detector 是一个免费、强大、开源的 Visual C++ 内存泄漏检测系统。

• Very Sleepy 是一个基于采样的CPU 分析器。

• Visual Studio Profiler (VSTS) 可用于CPU 采样、检测和内存分配。

• MTuner 利用一种新颖的方法来进行内存分析和分析,保留完整的基于时间的内存操作历史记录。

• Memory Leak Detection Tool 是一种高性能的内存泄漏检测工具。

• Heob 检测缓冲区溢出和内存泄漏。 集成到 Qt Creator 中。

• Process Explorer 可以查询和可视化每个进程的多个系统和性能计数器,我经常使用它进行初步调查。

• System Explorer 在一个长列表中显示任何正在运行的进程发出的所有系统调用,并支持过滤器以选择我们想要观察的进程。

• RAMMap 检查系统的全局内存使用情况,这需要相当多的Windows 内部知识。

• VMMap 显示有关单个应用程序内存使用情况的详细信息。

• Coreinfo 提供有关处理器的详细信息,以及您在进行低级优化工作时可能需要的信息。

• Bloaty 对二进制文件进行深入分析。 它旨在将二进制文件的每个字节准确地归因于一个符号或编译生成它的单元。

在本节中,我们向您简要介绍了一些第三方分析工具。 在下一节中,我们将讨论如何在链接期间优化二进制文件。

在链接期间优化

在前面的部分中,我们讨论了如何找到瓶颈并优化影响应用程序性能的代码段。 幸运的是,大多数编译器现在都包含一种机制,允许您在进行此类优化的同时保持代码的模块化和简洁性。 这称为链接时代码生成 (LTCG) 或链接时优化 (LTO)。 LTO 是链接过程中程序的优化。 链接器收集所有目标文件并将它们集成到一个程序中。 因为链接器可以查看整个程序,所以可以对整个程序进行分析和优化。 然而,链接器通常只能在程序被翻译成机器代码后才能看到它。 我们不是将每个源文件一个一个地转换为机器代码,而是将代码生成过程推迟到最后——链接时间。

链接时的代码生成不仅可以智能内联代码,还可以进行优化,例如去虚拟化函数和更好地消除冗余代码。 此技术可用于缩短应用程序启动时间。要在 Qt 中启用此机制,您必须从源代码构建。 在配置步骤中,将 -ltcg 添加到命令行选项。 在编译阶段一次编译所有源代码将为您提供完整 LTO 的所有优化优势。 您可以在工具链、平台和应用程序级别优化应用程序启动时间。

通过以下链接了解有关这些性能提示的更多信息:Performance Tip Startup Time

您有时可以将优化任务委托给编译器。 当您启用优化标志时,编译器将尝试提高性能并优化代码块,但代价是编译时间和(可能)调试能力。 您可以为所需的编译器启用编译器级优化标志,例如 GNU Compiler Collection (GCC) 或 Clang。

在以下链接中查看可用 C++ 编译器的 GCC 优化选项:

Optimize Options (Using the GNU Compiler Collection (GCC))

您可以通过以下链接了解 Clang 中的不同标志:clang - the Clang C, C++, and Objective-C compiler

在本节中,您了解了链接时优化。 在下一节中,我们将讨论如何更快地构建 Qt 应用程序。

更快地构建 Qt 应用程序

在大型复杂项目中,花在构建项目上的时间越来越有价值。 一般来说,构建时间越长,你每天损失的时间就越多。 如果将其乘以一个完整团队的时间,您将浪费大量时间等待构建完成。 虽然必须等待数小时才能重建每个小更改可能会让您更加注意细节并促使您深入思考每个步骤,但它也可能会限制更敏捷的流程或协作。 在本节中,我们将提供有关使用 Qt 在 C++ 中进行优化的简短指南。

请注意您应该遵循以下几点来加快构建过程:

• 使用平行构建标志

• 使用预编译标头 (pch)

• 从 makefile 中删除冗余目标

• 在类中使用前向声明

构建大型项目时最有效的方法是使用并行构建方法。 可以通过传递附加参数来启用并行构建。 在 Qt Creator 中,您可以在构建设置下启用并行构建。 您可以在构建步骤下找到以 Make 和 Details 按钮开头的可编辑字段。 单击 Details 按钮,然后在 Make arguments 字段中输入 -j8。 您可以通过以下命令行语句指示您的编译器以并行方式构建:

>make -j8

最后一个数字取决于您的硬件。 -j8 指示并行运行八个线程。 根据您的机器配置,您可以使用 -j4。

您还可以通过启用 -MP 标志为 Microsoft Visual C++ (MSVC) 编译器启用并行构建。 您可以通过在 .pro 文件中添加以下标志来指示 cl 并行运行:

*msvc* {
 QMAKE_CXXFLAGS += -MP
}

预编译标头是一种极好的技术,可以大大减少编译器的负载。 当编译器解析文件时,它必须解析整个代码,以及标准头文件和其他第三方源代码。 pch 允许您定义哪些文件经常使用,以便编译器可以在开始构建之前预编译它们,并在构建每个 .cpp 文件时利用结果。

要使用预编译的头文件,请将以下代码行添加到 .pro 文件中:

PRECOMPILED_HEADER = ../pch/your_precompiled_header.h
CONFIG += precompile_header

如果使用 Q_OBJECT 宏,元对象编译器会生成额外的文件。 不要不必要地使用 Q_OBJECT 宏,除非您需要相关功能,例如信号和槽机制或翻译。 当你添加 Q_OBJECT 宏时,moc 将生成一个 moc_<ClassName>.cpp 文件,这增加了编译的复杂性。

您可以将此文件包含在 .cpp 文件的末尾,如下所示:

#include "moc_<ClassName>.cpp"

您还可以通过对小型项目使用前向声明和在大型项目中使用前向标头来降低每个 .cpp 文件的依赖性。 转发类将缩短标准工作期间部分构建的持续时间。 大多数类都可以在 forwards.h 文件中包含前向声明。 通过拥有这样一个文件,您可以大大减少头文件中包含的数量,通常是通过包含 forwards.h。

结果,qmake 会注意到这一点并从目标列表中删除该文件。 这将减少编译器的负载。

在本节中,您学习了如何减少应用程序构建时间。 在下一节中,我们将讨论基于 Qt Widgets 的应用程序中的一些最佳实践。

Qt Widgets 的性能注意事项

Qt Widgets 模块使用光栅引擎呈现小部件,这是一种使用 CPU 而不是 GPU 进行呈现的软件。 在大多数情况下,它可以提供所需的性能。

然而,Qt Widgets 模块非常老旧,缺乏最新的功能。 由于 QML 完全是硬件加速的,您应该考虑将其用于应用程序的 UI。

如果您的小部件不需要 mouseTracking、tabletTracking 或类似的事件捕获,请将其关闭。 由于此跟踪,您的应用程序将使用更多的 CPU 时间。 维护一个较小的样式表并将其全部保存在一个样式表中,而不是将其应用于单个小部件。 一个大的样式表将花费更长的时间让 Qt 将信息处理到渲染系统中,这可能会影响应用程序的性能。 使用自定义样式而不是样式表,因为这可以为您提供更好的性能。

不要不必要地创建屏幕并将它们隐藏起来。 仅在需要时创建屏幕。 在使用 QStackedWidget 时,避免添加太多页面并用许多小部件填充它们。 需要Qt在渲染和事件处理阶段全部递归发现,导致程序运行缓慢。

在可行的情况下对大型操作使用异步方法,以避免阻塞主进程,并保持软件平稳运行。 多线程对于并行化事件循环中的多个进程非常有用。 但是,如果操作不当,例如重复创建和删除线程或执行不当的线程间通信,则可能会导致不良结果。

不同的 C++ 容器产生不同的速度。 Qt 的向量容器比STL 中的向量容器稍慢。 总的来说,旧的 C++ 数组仍然是最快的,但它缺乏排序功能。 使用最适合您需要的东西。

在本节中,您了解了使用 Qt Widgets 模块时的最佳实践。 在下一节中,我们将讨论 QML 中的最佳实践。

学习 QML 编码的最佳实践

在 QML 中编码时遵循某些最佳实践很重要。 您应该将文件保持在一定的行数限制内,并且应该具有一致的缩进和结构属性,并遵循标准的命名约定。

您可以按以下顺序构造您的 QML 对象属性:

Rectangle {
// id of the object
// property declarations
// signal declarations
// javascript functions
// object properties
// child objects
// states
// transitions
}

如果您使用一组属性中的多个属性,请使用组表示法,如下所示:

Rectangle {
 anchors {
 left: parent.left; top: parent.top
 right: parent.right; leftMargin: 20
 }
}

将属性组视为一个块可以减少混淆并有助于将属性与其他属性联系起来。

QML 和 JavaScript 不像 C++ 那样强制执行私有属性。 需要隐藏这些私有属性——例如,当属性是实现的一部分时。 为了有效地获得 QML 项中的私有属性,您可以嵌入 QtObject{...} 以隐藏属性。 这可以防止在 QML 文件和 JavaScript 之外访问属性。 为了尽量减少对性能的影响,请尝试将所有私有属性分组到相同的 QtObject 范围内。

下面的代码片段说明了 QtObject 的使用:

Item {
 id: component
 width: 40; height: 40
 QtObject {
 id: privateObject
 property real area: width * height //private 
 //property
 }
}

财产解决需要时间。 虽然查找的结果有时可以缓存和重用,但如果可行的话,通常最好避免做额外的工作。 您应该尝试在一个循环中只使用一次公共基础。

如果任何属性更改,则重新评估属性绑定表达式。 如果您有一个循环,您可以在其中进行一些处理但只有结果很重要,那么最好创建一个临时累加器,然后将其分配给您要更新的属性,而不是增量更新属性本身,以防止触发重新评估 的绑定表达式。

为防止因不可见活动元素的子元素而留下不可见项的持续开销,应延迟初始化它们并在不再使用时销毁它们。 使用 Loader 元素加载的对象可以通过重置 Loader 的 source 或 sourceComponent 属性来释放,但其他项目可以显式销毁。 在某些情况下可能需要使项目保持活动状态,在这种情况下,应该将其设置为不可见。

一般来说,不透明的内容比半透明的内容绘制起来要快得多。 这样做的原因是半透明内容需要混合,渲染器可能能够更好地优化不透明内容。 即使图像只有一个半透明像素,它也被视为完全透明。 对于具有半透明边缘的 BorderImage 元素来说也是如此。

避免在 QML 中进行长时间的逻辑计算。 使用 C++ 实现业务逻辑。 如果您仍然需要使用基于 JavaScript 的实现来进行一些复杂的操作或处理,那么请使用 WorkerScript。

Qt Quick Compiler 允许您将 QML 源代码编译成最终的二进制文件。 启用此功能可以大大缩短应用程序的启动时间。 您不必将 .qml 文件与应用程序一起部署。 您可以通过将以下行添加到您的 Qt 项目 (.pro) 文件来启用 Qt Quick Compiler:

CONFIG += qtquickcompiler

要了解有关 Qt Quick 最佳实践的更多信息,请阅读以下链接中的文档:Best Practices for QML and Qt Quick

您还可以在以下链接中找到的文档中探索有关 Qt Quick 性能的更多信息:QML Performance Considerations And Suggestions

在本节中,我们了解了使用 QML 编码时的一些最佳实践。 我们现在将在本章中总结我们的学习。

概括

在本章中,我们讨论了性能注意事项以及如何提高应用程序的整体性能。 我们从改进 C++ 代码开始。 然后,我们解释了并发技术如何帮助您提高应用程序的速度。 您了解了 QML Profiler 和其他分析工具。 您还了解了在 Qt 中编码时使用最佳实践的重要性。 现在,您可以在日常编码中使用这些技术。 您不必成为出色的应用程序开发人员即可进行性能优化。 如果您遵循最佳实践、设计模式并编写更好的算法,那么您的应用程序就会有更少的缺陷和更少的客户投诉。 这是一个持续的过程,你会逐渐变得更好。

恭喜! 您已经学习了性能优化的基础知识。 如果你好奇想知道更多,那么你可以阅读更多专门为性能调优而写的书籍。 在 Qt 中快乐编码。 请记住——编写更好的高性能代码可以减少 CPU 周期,从而减少碳足迹,因此,如果您编写更好的代码,就可以有效地拯救地球并应对气候变化!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值