C++ 核心指南 —— 性能

阅读建议:先阅读 《性能优化的一般策略及方法》

截至目前,C++ Core Guidelines 中关于性能优化的建议共有 18 条,而其中很大一部分是告诫你,不要轻易优化!

非必要,不优化

  • Per.1: 不要无故优化
  • Per.2: 不要过早优化
  • Per.3: 只优化少数关键代码

前三条可以总结为:非必要,不优化。所谓的“优化”,是指牺牲可读性、可维护性,以换取性能提升(否则应该作为编程的标准实践)。优化可能引入新的 bug,增加维护成本。软件工程师应把重心放在编写简洁、易于理解和维护的代码,而不是把性能作为首要目标。

先测量,再优化

如果性能非常重要,应该通过精确地测量,找到程序的 hot spots,再有针对性地优化。

Per.4: 不要假设复杂的代码比简单的代码快

  • 多线程未必比单线程快:考虑到线程间同步的开销、上下文切换开销,多线程未必比单线程快
  • 利用一系列复杂的优化技巧编写的复杂代码未必比直接编写的简单代码快,如
// 好:简单直接
vector<uint8_t> v(100000); for (auto& c : v)
c = ~c;
// 不好:复杂的优化技巧,本意想更快,但往往更慢!
vector<uint8_t> v(100000); for (size_t i = 0; i < v.size(); i += sizeof(uint64_t)) {
uint64_t& quad_word = *reinterpret_cast<uint64_t*>(&v[i]);
quad_word = ~quad_word;
}

Per.5: 不要假设低级语言比高级语言快

不要低估编译器的优化能力,很多时候编译器产生的代码要比手动编写低级语言更高效!

Per.6: 没有测量就不要对性能妄下断言

  • 性能优化很多时候是反直觉的,针对某些条件下的性能优化技巧在另一个环境下可能会劣化性能,因此必须要测量才知道某个改动到底会“优化”还是“劣化”性能
  • 小于 4% 的代码能占用 50% 的程序执行时间。只有测量才知道时间花在哪里,才能有针对性地优化

以上 6 条建议在 《性能优化的一般策略及方法》 中有更详细的描述。

具体优化建议

Per.7 设计应当允许优化

总是需要优化最初的设计,如果设计之初完全忽视了将来优化的可能性,会导致很难修改。

过早优化是万恶之源,但这并不是轻视性能的借口。一些经过实践检验的最佳实践可以帮助我们写出高效、可维护、可优化的代码:

  • 信息传递:接口设计要干净,但还要携带足够的信息,以便后续改进实现。
  • 紧凑的数据结构:默认情况下,使用紧凑的数据结构,如 std::vector,如果你认为需要一个链表,尝试设计接口,使用户看不到这个结构(参考标准库算法的接口设计)。
  • 函数参数的传递和返回:区分可变和不可变数据。不要把 资源管理 的任务强加给用户。不要把假想的 indirection 强加给用户。使用常规的方式传递信息,非常规或为特定实现“优化”过的数据传递方式可能会导致后续难以修改实现。
  • 抽象:不要过度泛化。试图满足每种可能的使用情况(包括误用),把每个设计决策推迟(编译或运行时 indirection)会导致复杂、臃肿、难以理解。不要基于对未来需求的猜测来进行泛化,从具体示例中进行泛化。泛化时保持性能,理想状态是零开销泛化。
  • 库:选择具有良好接口设计的库。如果没有现成的,自己写一个,模仿具有良好接口风格的库(可以从标准库找灵感)。
  • 隔离:把你的代码和旧的、乱的代码隔离开。可以按照自己的风格,设计一个接口风格良好的 wrapper,把那些不得不用的旧的、乱的代码封装起来,不要污染到我们自己的代码。

"indirection"(间接)通常指的是通过引入额外的层级或中介来访问数据或功能。在 C++ 中,这可能涉及使用指针、引用或其他间接方式来访问变量、对象或函数。

  1. 设计接口时,不要只考虑第一版的用例和实现。初版实现之后,必须 review,因为一旦部署之后,弥补错误将很困难。
  2. 低级语言并不总是高效。高级语言的代码不一定慢。
  3. 任何操作都有开销,不用过分担心开销(现代计算机都足够的快),但是需要大致了解各种操作的开销。例如:内存访问、函数调用、字符串比较、系统调用、磁盘访问、网络通信。
  4. 不是每段代码都需要稳定接口,有的接口可能只是实现细节。但还是要停下来想一下:如果要使用多个线程实现这个操作,需要什么样的接口?是否可以向量化?”
  5. 本条目和 Per.2 并不矛盾,而是它的补充:鼓励开发者在必要且时机成熟时进行优化。

移动语义

《C++ Core Guidelines 解析》针对本条目重点补充了移动语义:写算法时,应使用移动语义,而不是拷贝。移动语义有以下好处:

  • 移动开销比拷贝低
  • 算法稳定,因为不需要分配内存,不会出现 std::bad_alloc 异常
  • 算法可以用于“只移类型”,如 std::unique_ptr

需要移动语义的算法遇到不支持移动操作类型,则自动“回退”到拷贝操作。

而只支持拷贝语义的算法遇到不支持拷贝操作的类型时,则编译报错。

Per.10 依赖静态类型系统

弱类型(如 void* )、低级代码(如把 sequence 作为单独的字节来操作)会让编译器难以优化。

《解析》中还给出了一些额外的帮助编译器生成优化代码的技巧:

  1. 本地代码。“本地”指在同一个编译单元(如同一个 .c/.cpp 文件中)。例如 std::sort 需要一个谓词,传入本地 lambda 可能会比传入函数(指针)更快。

    因为对于本地 lambda,编译器拥有所有可用的信息来生成最优代码,而函数可能定义在另一个编译单元中,编译器无法获取有关该函数的细节,从而无法进行深度优化。
  2. 简单代码。优化器会搜寻可以被优化的已知模式,简单的代码更容易被匹配到。如果是手写的复杂代码,反而可能错失让编译器优化的机会。
  3. 额外提示。constnoexceptfinal 等关键字可以给编译器提供额外的信息,有了这些额外的信息,编译器可以大胆地做进一步优化。当然要先搞清楚这些关键字的含义及产生的影响。

Per.11 将计算从运行时提前到编译期

可以减少代码尺寸和运行时间、避免数据竞争、减少运行期的错误处理。

constexpr

将函数声明为 constexpr,且参数都是常量表达式,则可以在编译期执行。

注意:constexpr 函数可以在编译期执行,但不意味着只能在编译期执行,也可以在运行期执行。

constexpr 函数的限制:

  • 不能使用 staticthread_local 变量
  • 不能使用 goto
  • 不能使用异常
  • 所有变量必须初始化为字面类型

字面类型:

  • 内置类型(及其引用)
  • constexpr 构造的类
  • 字面类型的数组

例 1

// 旧风格:动态初始化
double square(double d) { return d*d; }
static double s2 = square(2); // 现代风格:编译期初始化
constexpr double ntimes(double d, int n) // 假设 0 <= n
{
double m = 1;
while (n--) m *= d;
return m;
}
constexpr double s3 {ntimes(2, 3)};

第一种写法很常见,但有两个问题:

  • 运行时函数调用开销
  • 另一个线程可能在 s2 初始化之前访问 s2

注:常量不存在数据竞争的问题

例 2

一个常用的技巧,小对象直接存在 handle 里,大对象存在堆上。

constexpr int on_stack_max = 20;

// 直接存储
template<typename T>
struct Scoped {
T obj;
}; // 在堆上存储
template<typename T>
struct On_heap {
T* objp;
}; template<typename T>
using Handle = typename std::conditional<
(sizeof(T) <= on_stack_max),
Scoped<T>,
On_heap<T>
>::type; void f()
{
// double 在栈上
Handle<double> v1;
// 数组在堆上
Handle<std::array<double, 200>> v2;
}

编译期可以计算出最佳类型,类似地技术也可用于在编译期选择最佳函数。

实际上大多数计算取决于输入,不可能把所有的计算全部放到编译期。除此之外,复杂的编译期计算可能大幅增加编译时间,并且导致调试困难。甚至在极少场景下,可能导致性能劣化。

代码检查建议

  • 检查是否有简单的、可以作为(但没有) constexpr 的函数
  • 检查是否有函数的所有参数都是常量表达式
  • 检查是否有可以改为 constexpr 的宏

Per.19 以可预测的方式访问内存

缓存对性能影响很大,一般缓存算法对相邻数据的简单、线性访问效率更高。

当程序需要从内存中读取一个 int 时,现代计算机架构会一次读取整个缓存行(通常 64 字节),储存在 CPU 缓存中,如果接下来要读取的数据已经在缓存中,则会直接使用,快很多。

例如:

int matrix[rows][cols];

// 不好
for (int c = 0; c < cols; ++c)
for (int r = 0; r < rows; ++r)
sum += matrix[r][c]; // 好
for (int r = 0; r < rows; ++r)
for (int c = 0; c < cols; ++c)
sum += matrix[r][c];

在 C++ 标准库中,std::vector, std::array, std::string 将数据存在连续的内存块中的数据结构对缓存行很友好。而 std::liststd::forward_list 则恰恰相反。

例如在某测试环境中,从容器中读取并累加所有元素:

  • std::vectorstd::liststd::forward_list 快 30 倍
  • std::vectorstd::deque 快 5 倍

很多场景下,即使需要在中间插入/删除元素,由于缓存行的原因,std::vector 的性能也可能好于 std::list

除非测量的结果表明其他容器性能好于 std::vector,否则应将 std::vector 作为首选容器。

其他

剩下的条目截至目前还只有标题,缺少详细描述:

  • Per.12 Eliminate redundant aliases/消除冗余别名
  • Per.13 Eliminate redundant indirections/消除冗余间接引用(指针解引用)
  • Per.14 Minimize the number of allocations and deallocations/尽可能减少分配和释放
  • Per.15 Do not allocate on a critical branch/不在关键分支上分配
  • Per.16 Use compact data structures/使用紧凑的数据结构:性能主要由内存访问决定
  • Per.17 Declare the most used member of a time-critical struct first/对于时间关键的结构体,把最常用的成员定义在前
  • Per.18 Space is time/空间就是时间:性能主要由内存访问决定
  • Per.30 Avoid context switches on the critical path/避免关键路径上的上下文切换

总结

  • 非必要,不优化
  • 先测量,再优化
  • 为编译器优化提供必要信息:
    • 正确使用 const、final、noexcept 等关键字
    • 为函数实现移动语义、如果可能,使之成为 constexpr
  • 现代计算机架构为连续读取内存而进行了优化,应该将 std::vector, std::array, std::string 作为首选

Reference

C++ 核心指南 —— 性能的更多相关文章

  1. GCD介绍(二): 多核心的性能

    GCD介绍(二): 多核心的性能  概念         为了在单一进程中充分发挥多核的优势,我们有必要使用多线程技术(我们没必要去提多进程,这玩意儿和GCD没关系).在低层,GCD全局dispatc ...

  2. 三年磨一剑,robot framework 自动化测试框架核心指南,真正讲透robot framework自动化测试框架(笔者新书上架)。

    序 关于自动化测试的工具和框架其实有很多.自动化测试在测试IT行业中扮演着越来越重要的角色,不管是在传统的IT行业还是高速发展的互联网行业或是如今的大数据和大热的人工智能领域,都离不开测试,也更加离不 ...

  3. Robot Framework自动化测试框架核心指南-如何使用Java编写自定义的RobotFramework Lib

    如何使用Java编写自定义的RobotFramework Lib 本文包括2个章节 1. Robot Frdamwork中如何调用java Lib库 2.使用 java编写自定义的Lib 本文作者为: ...

  4. Robot Framework自动化测试框架核心指南-如何做好自动化测试平台框架的设计

    自动化测试如果需要能高效快速的支撑软件项目的测试,项目的快速迭代以及上线,除了以上我们介绍的需要许多的Lib来支持以及需要高效的去编写自动化测试案例外,还需要一个好的自动化测试框架平台来支撑我们的自动 ...

  5. GCD教程(二):多核心的性能

    接上一篇,原帖地址:http://www.dreamingwish.com/dream-2012/of-of-of-performance-of-of-of-of-of-of-of-gcd-intro ...

  6. 深入GCD(二): 多核心的性能

    概念为了在单一进程中充分发挥多核的优势,我们有必要使用多线程技术(我们没必要去提多进程,这玩意儿和GCD没关系).在低层,GCD全局dispatch queue仅仅是工作线程池的抽象.这些队列中的Bl ...

  7. GCD系列 之(二): 多核心的性能

    全局队列的并发执行 for(id obj in array) [self doSomethingIntensiveWith:obj]; 假设,每个元素要做的事情-doSomethingIntensiv ...

  8. (转)ELK Stack 中文指南--性能优化

    https://www.bookstack.cn/read/ELKstack-guide-cn/elasticsearch-README.md https://blog.csdn.net/cjfeii ...

  9. [译] JavaScript核心指南(JavaScript Core) 【转】

    本文转自:http://remember2015.info/blog/?p=141#scope-chain 零.索引 对象(An Object) 原型链(A Prototype Chain) 构造函数 ...

  10. 如何学习Linux性能优化?

    如何学习Linux性能优化? 你是否也曾跟我一样,看了很多书.学了很多 Linux 性能工具,但在面对 Linux 性能问题时,还是束手无策?实际上,性能分析和优化始终是大多数软件工程师的一个痛点.但 ...

随机推荐

  1. 【译】.NET 8 拦截器(interceptor)

    通常情况下,出于多种原因,我不会说我喜欢写关于预览功能的文章.我的大多数帖子旨在帮助人们解决他们可能遇到的问题,而不是找个肥皂盒或打广告.但是我认为我应该介绍这个 .NET 预览特性,因为它是我在 . ...

  2. HTML一键打包APK工具1.9.5更新,新增一机一码功能

    HMTL网址打包APK,可以把本地HTML项目, Egret游戏,网页游戏,或者网站打包为一个安卓应用APK文件,无需编写任何代码,也无需配置安卓开发环境,支持在最新的安卓设备上安装运行. 打包软件会 ...

  3. 如何理解DDD中的值对象

    引言 实体和值对象是领域驱动设计中的两个重要概念.相对实体而言,值对象更加抽象,理解起来也更晦涩一些.那么该如何理解值对象?我们先来看一下<实现领域驱动设计>书中对值对象的定义: 值对象 ...

  4. Dubbo3应用开发—XML形式的Dubbo应用开发和SpringBoot整合Dubbo开发

    Dubbo3程序的初步开发 Dubbo3升级的核心内容 易⽤性 开箱即⽤,易⽤性⾼,如 Java 版本的⾯向接⼝代理特性能实现本地透明调⽤功能丰富,基于原⽣库或轻量扩展即可实现绝⼤多数的 微服务治理能 ...

  5. Python 列表操作指南3

    示例,将新列表中的所有值设置为 'hello': newlist = ['hello' for x in fruits] 表达式还可以包含条件,不像筛选器那样,而是作为操纵结果的一种方式: 示例,返回 ...

  6. Python使用socket的UDP协议实现FTP文件服务

    简介 本示例主要是用Python的socket,使用UDP协议实现一个FTP服务端.FTP客户端,用来实现文件的传输.在公司内网下,可以不适用U盘的情况下,纯粹使用网络,来实现文件服务器的搭建,进而实 ...

  7. oauth2单点登录集成

    单点登陆 概念: 单点登录其实就是在多个系统之间建立链接, 打通登录系统, 让同一个账号在多个系统中通用 举个例子: 登录Gmail的时候可以用账号密码登录, 也可以用google账号登录, 而使用g ...

  8. 数据结构与算法 | 深搜(DFS)与广搜(BFS)

    深搜(DFS)与广搜(BFS) 在查找二叉树某个节点时,如果把二叉树所有节点理理解为解空间,待找到那个节点理解为满足特定条件的解,对此解答可以抽象描述为: 在解空间中搜索满足特定条件的解,这其实就是搜 ...

  9. Jenkins软件平台安装部署

    1.Jenkins软件平台概念剖解: 基于主流的Hudson/Jenkins平台工具实现全自动网站部署.网站测试.网站回滚会大大的减轻网站部署的成本,Jenkins的前身为Hudson,Hudson主 ...

  10. QT(9)-QStyleOption及其子类

    1 QStyleOption QStyleOption及其子类包含QStyle函数绘制图形元素所需的所有信息. 出于性能考虑,成员函数很少,对成员变量的访问是直接的(即使用.或者->运算符).这 ...