C++ 中心攻略 —— 功能
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++ 中,这或许触及运用指针、引证或其他直接办法来拜访变量、目标或函数。
注
- 规划接口时,不要只考虑第一版的用例和完成。初版完成之后,有必要 review,因为一旦布置之后,补偿错误将很困难。
- 初级言语并不总是高效,高档言语的代码不一定慢。
- 任何操作都有开支,不必过火忧虑开支(现代核算机都满意的快),可是需求大致了解各种操作的开支。例如:内存拜访、函数调用、字符串比较、体系调用、磁盘拜访、网络通信。
- 不是每段代码都需求安稳接口,有的接口或许仅仅完成细节。但仍是要停下来想一下:假如要运用多个线程完成这个操作,需求什么样的接口?是否能够向量化?
- 本条目和 Per.2 并不矛盾,而是它的弥补:鼓舞开发者在必要且时机成熟时进行优化。
移动语义
《C++ Core Guidelines 解析》针对本条目要点弥补了移动语义:写算法时,应运用移动语义,而不是复制。移动语义有以下优点:
- 移动开支比复制低
- 算法安稳,因为不需求分配内存,不会呈现
std::bad_alloc
反常 - 算法能够用于“只移类型”,如
std::unique_ptr
需求移动语义的算法遇到不支撑移动操作类型,则主动“回退”到复制操作。
而只支撑复制语义的算法遇到不支撑复制操作的类型时,则编译报错。
Per.10 依靠静态类型体系
弱类型(如 void* )、初级代码(如把 sequence 作为独自的字节来操作)会让编译器难以优化。
《解析》中还给出了一些额定的协助编译器生成优化代码的技巧:
- 本地代码。“本地”指在同一个编译单元(好像一个 .c/.cpp 文件中)。例如
std::sort
需求一个谓词,传入本地 lambda 或许会比传入函数(指针)更快。
因为关于本地 lambda,编译器具有一切可用的信息来生成最优代码,而函数或许界说在另一个编译单元中,编译器无法获取有关该函数的细节,然后无法进行深度优化。 - 简略代码。优化器会搜索能够被优化的已知形式,简略的代码更简略被匹配到。假如是手写的杂乱代码,反而或许失去让编译器优化的时机。
- 额定提示。
const
、noexcept
、final
等要害字能够给编译器供给额定的信息,有了这些额定的信息,编译器能够斗胆地做进一步优化。当然要先搞清楚这些要害字的意义及发生的影响。
Per.11 将核算从运转时提前到编译期
能够削减代码尺度和运转时刻、防止数据竞赛、削减运转期的错误处理。
constexpr
将函数声明为 constexpr
,且参数都是常量表达式,则能够在编译期履行。
留意:constexpr
函数能够在编译期履行,但不意味着只能在编译期履行,也能够在运转期履行。
constexpr
函数的约束:
- 不能运用
static
或thread_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::list
和 std::forward_list
则恰恰相反。
例如在某测验环境中,从容器中读取并累加一切元素:
std::vector
比std::list
或std::forward_list
快 30 倍std::vector
比std::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++ Core Guidelines, Per: Performance
- 《功用优化的一般战略及办法》
- 《C++ Core Guidelines 解析》