阅读建议:先阅读 《性能优化的一般策略及方法》
截至目前,C++ Core Guidelines 中关于性能优化的建议共有 18 条,而其中很大一部分是告诫你,不要轻易优化!
前三条可以总结为:非必要,不优化。所谓的“优化”,是指牺牲可读性、可维护性,以换取性能提升(否则应该作为编程的标准实践)。优化可能引入新的 bug,增加维护成本。软件工程师应把重心放在编写简洁、易于理解和维护的代码,而不是把性能作为首要目标。
如果性能非常重要,应该通过精确地测量,找到程序的 hot spots,再有针对性地优化。
// 好:简单直接 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; }
不要低估编译器的优化能力,很多时候编译器产生的代码要比手动编写低级语言更高效!
以上 6 条建议在 《性能优化的一般策略及方法》 中有更详细的描述。
总是需要优化最初的设计,如果设计之初完全忽视了将来优化的可能性,会导致很难修改。
过早优化是万恶之源,但这并不是轻视性能的借口。一些经过实践检验的最佳实践可以帮助我们写出高效、可维护、可优化的代码:
std::vector
,如果你认为需要一个链表,尝试设计接口,使用户看不到这个结构(参考标准库算法的接口设计)。"indirection"(间接)通常指的是通过引入额外的层级或中介来访问数据或功能。在 C++ 中,这可能涉及使用指针、引用或其他间接方式来访问变量、对象或函数。
《C++ Core Guidelines 解析》针对本条目重点补充了移动语义:写算法时,应使用移动语义,而不是拷贝。移动语义有以下好处:
std::bad_alloc
异常std::unique_ptr
需要移动语义的算法遇到不支持移动操作类型,则自动“回退”到拷贝操作。
而只支持拷贝语义的算法遇到不支持拷贝操作的类型时,则编译报错。
弱类型(如 void* )、低级代码(如把 sequence 作为单独的字节来操作)会让编译器难以优化。
《解析》中还给出了一些额外的帮助编译器生成优化代码的技巧:
std::sort
需要一个谓词,传入本地 lambda 可能会比传入函数(指针)更快。const
、noexcept
、final
等关键字可以给编译器提供额外的信息,有了这些额外的信息,编译器可以大胆地做进一步优化。当然要先搞清楚这些关键字的含义及产生的影响。可以减少代码尺寸和运行时间、避免数据竞争、减少运行期的错误处理。
将函数声明为 constexpr
,且参数都是常量表达式,则可以在编译期执行。
注意:
constexpr
函数可以在编译期执行,但不意味着只能在编译期执行,也可以在运行期执行。
constexpr
函数的限制:
static
或 thread_local
变量goto
字面类型:
constexpr
构造的类// 旧风格:动态初始化 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)};
第一种写法很常见,但有两个问题:
注:常量不存在数据竞争的问题
一个常用的技巧,小对象直接存在 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
的宏缓存对性能影响很大,一般缓存算法对相邻数据的简单、线性访问效率更高。
当程序需要从内存中读取一个 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
作为首选容器。
剩下的条目截至目前还只有标题,缺少详细描述:
std::vector
, std::array
, std::string
作为首选