一句话核心结论 :C++ 效率优化的”上半部分”——80-20 法则 告诉你优化要从”关键 20%”入手,lazy evaluation 让你”按需计算”节省 80% 的浪费,分期摊还 让你”提前 vs 缓算”做 trade-off,临时对象 是隐藏的”性能小偷”,RVO 是 C++ 编译器的”神器”。这 5 个条款是性能调优的”工具箱”。
系列导航 # 文章 状态 0 系列总览 ✅ 已发布 1 基础议题 ✅ 已发布 2 操作符 ✅ 已发布 3 异常(上) ✅ 已发布 4 异常(下) ✅ 已发布 5 本文:效率(上) ✅ 已发布 6 效率(下):重载、operator new、内存池、inline 🔜 计划中 7 技术:虚拟构造、智能指针、引用计数、双重分派 🔜 计划中 8 杂项 + 总结:未来时态、标准库、命名空间 🔜 计划中
前言:为什么”效率”是 More Effective C++ 的核心? Effective C++ 关注正确性 ——你怎么写”对的” C++。
More Effective C++ 关注性能 ——你怎么写”快”的 C++。
graph TB
A["C++ 效率优化(上)"] --> B["80-20 法则\n(条款 16)"]
A --> C["lazy evaluation\n(条款 17)"]
A --> D["分期摊还\n(条款 18)"]
A --> E["临时对象\n(条款 19)"]
A --> F["RVO\n(条款 20)"]
B -.->|优化目标| B1["关键 20%"]
C -.->|按需计算| C1["4 大场景"]
D -.->|trade-off| D1["提前 vs 缓算"]
E -.->|隐藏成本| E1["隐式转换\n传值参数"]
F -.->|编译器优化| F1["RVO / NRVO"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style F fill:#FFF9C4,stroke:#F9A825,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333
style E1 fill:#FFF9C4,stroke:#F9A825,color:#333
style F1 fill:#FFF9C4,stroke:#F9A825,color:#333一、条款 16:谨记 80-20 法则 1.1 什么是 80-20 法则? 这个 20% 通常是 :
热点循环(hot loop) 频繁调用的函数 IO / 网络 / 内存分配 复杂算法 剩下 80% 的代码 = 1 次执行或很少执行 ——优化它们没有意义 。
1.2 案例:优化一个”慢”程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void processItems (const std::vector<Item>& items) { for (const auto & item : items) { expensiveCalculation (item); } } void setup () { loadConfig (); initDatabase (); }
错误优化 :
结果 :setup 从 1 秒 → 0.01 秒,省 0.99 秒。总时间从 10 秒 → 9.01 秒 ——肉眼无感。
正确优化 :
1 2 3 4 5 void processItems (const std::vector<Item>& items) { }
结果 :每次 0.5 微秒 × 100 万 = 0.5 秒 。总时间从 10 秒 → 9.5 秒 ——更不明显但针对性强 。
真正大的优化 :
1.3 实战:用 Profiler 找热点 1 2 3 4 5 6 perf record -g ./myprogram perf report
1 2 3 4 5 6 #include <gperftools/profiler.h> ProfilerStart ("myprogram.prof" );ProfilerStop ();
1.4 关键启示 80-20 法则 ——优化要从”关键 20%”入手不要凭直觉优化 ——用 profiler优化 1 次执行的代码 ——毫无意义优化 100 万次循环 ——10 倍收益二、条款 17:考虑使用 lazy evaluation 2.1 什么是 lazy evaluation? 1 2 3 4 5 6 std::string bigString = computeExpensiveString (); LazyString bigString = [] { return computeExpensiveString (); };
2.2 lazy 的 4 大场景 场景 1:引用计数(写时复制) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class String { char * data_; public : String (const String& other) { data_ = new char [other.size_]; std::memcpy (data_, other.data_, other.size_); } }; class String { std::shared_ptr<std::string> data_; public : String (const String& other) : data_ (other.data_) { } void modify (const char * newData) { if (data_.use_count () > 1 ) { data_ = std::make_shared <std::string>(*data_); } *data_ = newData; } };
场景 2:区分读 / 写 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Matrix { double data_[N][N]; public : double & operator [](int i, int j) { return data_[i][j]; } }; Matrix m; m[0 ][0 ] = 3.14 ; double x = m[0 ][0 ]; class MatrixElement { Matrix& m_; int i_, j_; public : operator double () const { return m_.data_[i_][j_]; } MatrixElement& operator =(double v) { m_.data_[i_][j_] = v; return *this ; } };
场景 3:lazy fetch(数据库场景) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Employee { std::string name_; std::vector<Project> projects_; public : Employee (int id) { loadAllProjects (); } }; class Employee { int id_; std::optional<std::vector<Project>> projects_; public : Employee (int id) : id_ (id) {} const std::vector<Project>& projects () { if (!projects_) { projects_ = loadAllProjects (); } return *projects_; } };
场景 4:表达式缓算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 std::vector<int > bigVec (1000000 ) ;int sum = 0 ;for (int i = 0 ; i < 1000000 ; ++i) { sum += bigVec[i] * 2 + 1 ; } template <typename L, typename R>class VecExpr { L lhs_; R rhs_; public : auto operator [](size_t i) const { return lhs_[i] * 2 + 1 ; } };
2.3 实战:lazy 的”代价” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class LazyString { std::function<std::string()> compute_; std::optional<std::string> cached_; public : const std::string& value () { if (!cached_) { cached_ = compute_ (); } return *cached_; } };
判断标准 :
计算真的昂贵 (如 DB 查询) 计算结果可能不用 (如异常分支) 调用不频繁 否则——直接计算 更简单。
2.4 关键启示 lazy = 按需计算 ——节省”用不到”的浪费4 大场景 :引用计数 / 读 vs 写 / lazy fetch / 表达式缓算有成本 :optional + 函数调用 + 代码复杂不要”为了 lazy 而 lazy” ——简单优先三、条款 18:分期摊还预期的计算成本 3.1 什么是分期摊还(Amortize)? 3.2 案例 1:vector 扩容 数学上 :摊销 = O(1) / push_back。
3.3 案例 2:字符串的 c_str() 调用 1 2 3 4 5 6 7 8 9 10 class String { char * data_; public : const char * c_str () { return data_; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class String { std::vector<char > data_; mutable bool needsNull_ = true ; public : const char * c_str () const { if (needsNull_) { data_.push_back ('\0' ); needsNull_ = false ; } return data_.data (); } };
3.4 案例 3:cache 友好的数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class LRUCache { std::list<int > lru_; std::unordered_map<int , int > cache_; public : int get (int key) { if (cache_.count (key)) { lru_.splice (lru_.begin (), lru_, cache_[key]); } return cache_[key]; } };
3.5 案例 4:数据库连接池 1 2 3 4 5 6 7 8 9 10 11 12 class ConnectionPool { std::vector<Connection> pool_; public : Connection& get () { if (pool_.empty ()) { pool_.push_back (Connection::create ()); } return pool_.back (); } };
3.6 关键启示 分期摊还 = 一次大开销 → 多次小开销典型场景 :vector 扩容 / 缓存 / 连接池好处 :平均成本低代价 :偶尔有”长尾”延迟四、条款 19:了解临时对象的来源 4.1 什么是临时对象? 4.2 临时对象的 4 大来源 来源 1:隐式类型转换 1 2 3 4 5 6 void f (const std::string& s) ;f ("hello" ); f (std::string ("hello" ));
开销 :构造 + 析构 = 2 次操作。
来源 2:传值参数 1 2 3 4 5 6 7 8 void f (std::string s) ; std::string original = "hello" ; f (original); void f (const std::string& s) ; f (original);
来源 3:函数返回值 1 2 3 4 5 6 std::string makeString () { return "hello" ; } std::string s = makeString ();
来源 4:运算结果 1 2 3 int a = 1 , b = 2 ;int c = a + b;
4.3 临时对象的”成本” 1 2 3 4 5 6 7 8 class Matrix { double data_[100 ][100 ]; }; void f (Matrix m) ;f (Matrix (...));
4.4 实战:减少临时对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 std::string s1 = "a" ; std::string s2 = "b" ; std::string s3 = "c" ; std::string result = s1 + s2 + s3; std::string result; result.reserve (s1. size () + s2. size () + s3. size ()); result = s1; result += s2; result += s3;
4.5 关键启示 临时对象 = 隐式转换 + 传值 + 返回 + 运算 隐式转换最危险 ——看似”没成本”大类的临时对象很贵 ——传 const refreserve + append 比 + 链高效五、条款 20:协助完成”返回值优化(RVO)” 5.1 什么是 RVO? 1 2 3 4 5 6 7 8 9 std::string makeString () { return "hello" ; } std::string s = makeString ();
5.2 RVO vs NRVO 1 2 3 4 5 6 7 8 9 10 std::string makeString () { return "hello" ; } std::string makeString () { std::string s = "hello" ; return s; }
C++17 起 :RVO 几乎强制 ——编译器必须省略拷贝。
5.3 反例:阻止 RVO 的写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::string makeString (bool ok) { std::string a = "yes" ; std::string b = "no" ; return ok ? a : b; } std::string makeString (bool ok) { if (ok) { return "yes" ; } else { return "no" ; } }
5.4 实战:怎么”协助”RVO? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 return Widget (1 , 2 , 3 ); return "hello" ; Widget make () { Widget w (...) ; return w; } Widget& make () { }
5.5 C++11 的 std::move 1 2 3 4 5 6 7 8 9 10 11 std::string make () { std::string s = "hello" ; return std::move (s); } std::string make () { std::string s = "hello" ; return s; }
为什么? std::move(s) 是 static_cast<Widget&&>(s)——告诉编译器”我能被移走”——编译器不再 做 NRVO。
5.6 关键启示 RVO = 编译器优化 ——消除返回值的拷贝C++17 起 ——RVO 几乎强制直接 return ——帮助 RVO不要 return std::move ——阻止优化六、5 个条款的”效率(上)”全景 graph TB
A["C++ 效率(上)"] --> B["80-20 法则\n(条款 16)"]
A --> C["lazy evaluation\n(条款 17)"]
A --> D["分期摊还\n(条款 18)"]
A --> E["临时对象\n(条款 19)"]
A --> F["RVO\n(条款 20)"]
B --> B1["用 profiler\n找 20% 热点"]
C --> C1["4 大场景\n按需计算"]
D --> D1["一次大开销\n多次小开销"]
E --> E1["隐式转换\n传值参数"]
F --> F1["直接 return\n帮助 RVO"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style F fill:#FFF9C4,stroke:#F9A825,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333
style E1 fill:#FFF9C4,stroke:#F9A825,color:#333
style F1 fill:#FFF9C4,stroke:#F9A825,color:#333七、常见误区与陷阱 7.1 误区 1:凭直觉优化 7.2 误区 2:滥用 lazy 1 2 Lazy<int > x = []{ return 1 + 2 ; };
7.3 误区 3:传值大型对象 1 2 3 4 5 void f (std::vector<int > v) ; void f (const std::vector<int >& v) ;
7.4 误区 4:阻止 RVO 1 2 3 4 5 std::string make () { std::string s = "hello" ; return std::move (s); }
八、C++11/14/17 的演进 主题 C++98 时代 C++11/14/17/20 时代 80-20 法则 不变 不变 lazy 手写 概念依旧 分期摊还 不变 不变 临时对象 隐式转换 std::move / std::forwardRVO 编译器”允许” C++17 强制 RVO移动语义 无 std::move + RVO 协作表达式模板 手写 概念依旧
C++17 的”强制 RVO” :
1 2 3 4 5 Widget make () { return Widget (1 , 2 , 3 ); }
九、面试高频考点 9.1 必背题 题目 答案要点 80-20 法则? 80% 时间花在 20% 代码 怎么找 20%? Profiler(perf / gprof / callgrind) 什么是 lazy? 按需计算——4 大场景 临时对象的来源? 隐式转换 / 传值 / 返回 / 运算 什么是 RVO? 编译器省略返回值的拷贝 NRVO vs RVO? 命名 vs 匿名 C++17 强制 RVO? 是——prvalue 场景 怎么帮 RVO? 直接 return 构造对象
9.2 高频追问 追问 关键点 lazy 的 4 大场景? 引用计数 / 读写区分 / lazy fetch / 表达式缓算 分期摊还的例子? vector 扩容 / LRU cache / 连接池 临时对象怎么减少? 传 const ref / reserve+append std::move vs 直接 return? move 阻止 NRVO 移动语义 vs RVO? 移动是语言特性;RVO 是编译器优化 强制 RVO 的条件? C++17 prvalue + 直接 return
十、配套实验 10.1 实验 1:RVO vs 拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> class Widget { int x_; public : Widget (int x) : x_ (x) { std::cout << "ctor\n" ; } Widget (const Widget& w) : x_ (w.x_) { std::cout << "copy ctor\n" ; } Widget (Widget&& w) noexcept : x_ (w.x_) { w.x_ = 0 ; std::cout << "move ctor\n" ; } ~Widget () { std::cout << "dtor\n" ; } }; Widget makeBad (bool b) { Widget a (1 ) ; Widget c (2 ) ; return b ? a : c; } Widget makeGood (bool b) { if (b) { return Widget (1 ); } else { return Widget (2 ); } } int main () { std::cout << "=== Bad ===\n" ; Widget w1 = makeBad (true ); std::cout << "\n=== Good ===\n" ; Widget w2 = makeGood (true ); return 0 ; }
10.2 实验 2:临时对象来源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> #include <string> class Tracker { int id_; public : Tracker (int id) : id_ (id) { std::cout << "ctor " << id_ << "\n" ; } Tracker (const Tracker& t) : id_ (t.id_) { std::cout << "copy ctor " << id_ << "\n" ; } ~Tracker () { std::cout << "dtor " << id_ << "\n" ; } int id () const { return id_; } }; void f (Tracker t) { std::cout << "f: " << t.id () << "\n" ; } int main () { std::cout << "=== pass by value ===\n" ; Tracker t (1 ) ; f (t); std::cout << "\n=== implicit conversion ===\n" ; std::cout << "\n=== return value ===\n" ; auto makeTracker = []() { return Tracker (3 ); }; Tracker t2 = makeTracker (); return 0 ; }
10.3 实验 3:lazy evaluation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <iostream> #include <optional> #include <functional> int expensiveComputation (int n) { std::cout << "Doing expensive computation...\n" ; int sum = 0 ; for (int i = 0 ; i < n; ++i) sum += i; return sum; } class LazyInt { std::function<int ()> compute_; std::optional<int > cached_; public : explicit LazyInt (std::function<int ()> c) : compute_(std::move(c)) { } int value () { if (!cached_) { cached_ = compute_ (); } return *cached_; } }; int main () { std::cout << "=== eager ===\n" ; int x = expensiveComputation (1000 ); std::cout << "x = " << x << "\n" ; std::cout << "\n=== lazy ===\n" ; LazyInt l ([] { return expensiveComputation(1000 ); }) ; std::cout << "Created lazy\n" ; if (false ) { std::cout << l.value () << "\n" ; } return 0 ; }
十一、回到 5 条黄金法则 条款 黄金法则 16 80-20 法则——用 profiler 找 20% 热点 17 lazy = 按需计算——4 大场景:引用计数 / 读写区分 / lazy fetch / 表达式缓算 18 分期摊还——一次大开销 → 多次小开销 19 临时对象 = 隐式转换 / 传值 / 返回 / 运算 20 RVO = 编译器优化——直接 return 帮助 RVO
十二、结尾思考题 思考题 1 :用 perf / gprof 找你项目的热点函数——通常 20% 在哪?
思考题 2 :实现 copy-on-write 字符串 + 引用计数。什么时候深拷贝?
思考题 3 :用 RVO + 移动语义优化一个返回 std::vector<int> 的函数。
思考题 4 :你的项目里哪些”传值大型对象”可以改成 const T&?
思考题 5 :解释 return std::move(s); 为什么会阻止 NRVO。
十三、本篇速查表 主题 关键 API / 模式 适用场景 80-20 法则 Profiler 性能调优 lazy evaluation std::optional + lambda按需计算 分期摊还 vector 扩容 / cache 平均 O(1) 临时对象 隐式转换 / 传值 减少拷贝 RVO 直接 return C++17 强制 移动语义 std::move配合 RVO
十四、系列导航 # 文章 状态 0 系列总览 ✅ 已发布 1 基础议题 ✅ 已发布 2 操作符 ✅ 已发布 3 异常(上) ✅ 已发布 4 异常(下) ✅ 已发布 5 本文:效率(上) ✅ 已发布 6 效率(下):重载、operator new、内存池、inline 🔜 计划中 7 技术:虚拟构造、智能指针、引用计数、双重分派 🔜 计划中 8 杂项 + 总结:未来时态、标准库、命名空间 🔜 计划中
下一篇 :第 6 篇《效率(下):重载、operator new、内存池、inline》——条款 21-24 一起讲透 C++ 效率的”下半部分”:利用重载避免隐式转换、op= 优于 op、了解其他库的成本、虚函数 / 多重继承 / RTTI 的真实成本。
行动建议 :
今天 :用 profiler 找你的项目热点今天 :把”传值大型对象”改成 const T&本周 :识别你项目里的 lazy 机会(数据库查询 / 文件 IO)本周 :把所有”阻止 RVO”的地方改为”直接 return”思考 :你的项目里哪些操作可以分期摊还?