《C++ High Performance》学习总结:把性能优化从玄学变成方法论

《C++ High Performance》学习总结:把性能优化从玄学变成方法论 一句话评价:这本书不教你怎么"更快",而是教你怎么系统地发现慢在哪、为什么慢、以及怎么改。看完最大的收获不是某个技巧,而是一整套性能思维框架。 写在前面:为什么读这本书 说实话,做服务器开发这几年,性能优化基本靠"直觉 + 经验 + 试":感觉这里慢就优化这里,测一下看有没有变化。这种打法偶尔有效,但有两个致命问题: 没有量测就跑优化——经常优化了半天,发现改的根本不是热点(Amdahl 定律的教训:优化一个只占 5% 时间的函数,改出花来整体也就快 5%)。 不知道还有哪些工具——工具链一直在进化,perf、火焰图、Sanitizer、PGO,很多只知道名字没实际用过。 这本书正好把这两块补齐了。它不是零基础教程,需要你对 C++ 有一定基础(至少要懂 STL、模板、RAII),但它能把你已有的知识用性能的视角重新串一遍。 下面按书的脉络讲,但重点讲对我们服务器开发真正有用的部分,并且每条都尽量落到我们的实际场景。 这篇总结我写得比较长,因为它本质上是我重读这本书时给自己补的"教学设计":每一个之前只记结论的知识点,我都尽量补上"为什么"。读的时候你可以把它当成一条讲解链路:它是什么 → 它为什么这么设计(底层原理)→ 一个生活化的类比 → 配图或代码 → 落到我们服务器的实际场景。这样哪怕几个月后再回头看,也能靠推理而不是靠死记找回上下文。 一、先建立正确的性能思维(第 1-3 章) 1.1 值语义 vs 引用语义——很多人忽略的第一课 是什么 第 1 章从 C++ 最本质的特性讲起:C++ 默认是值语义。auto a = b 是拷贝,不是两个名字指向同一块内存。这一点和 Java、Python、C# 里"变量是引用"的直觉完全不同,也是很多新手刚从脚本语言转过来时栽的第一个跟头。 1 2 3 4 5 6 7 8 struct Player { int guid; int level; }; Player p1{10001, 60}; Player p2 = p1; // 拷贝:p2 拥有自己的内存,内容从 p1 复制 p2.level = 99; // 改 p2 不影响 p1 为什么(底层原理) ...

August 15, 2026 · 19 min · 4014 words

《深入理解计算机系统》(CSAPP)学习总结:从晶体管到网络协议的完整世界观

《深入理解计算机系统》(CSAPP)学习总结:从晶体管到网络协议的完整世界观 一句话评价:这本书解决的是"会写代码但不知道为什么"的困惑——它把从 CPU 到网络的所有中间层一次性打通。看完之后再看那些经典的 C++ 性能技巧、并发 bug、内存问题,全都"对上了号"。 写在前面:为什么服务器开发必须读这本书 做服务器开发,平时打交道最多的就是"程序为什么快/慢、为什么会崩、为什么内存涨"。这些问题你问一个纯应用层 C++ 开发者,他可能给你一堆经验;但这本书给的是底层原理——每个"经验"背后都有确切的硬件/操作系统机制支撑。 举个具体例子:我们写 std::vector 比 std::list 快,这个"经验"人尽皆知。但只有懂了缓存层次结构和 cache line(高速缓存行),你才真正明白为什么快、快多少、在什么极端情况下可能不成立。没有这个底层认知,性能优化永远是"背口诀"而不是"推导"。 这本书的三个核心理念贯穿始终: 程序员是系统的"中间层"——向上要满足应用需求,向下要理解硬件和 OS 如何配合。 抽象是管理复杂度的工具——指令集抽象 CPU,虚拟内存抽象物理内存,文件抽象 I/O,虚拟机和容器抽象整机。 “程序员的视野”——书中反复出现的经典表述:程序员看到的地址空间,是虚拟的;程序员看到的"文件",是内核在背后处理的字节流。 下面按书的 13 章讲,重点讲对服务器开发真正有用的部分,并尽量落到我们的实际场景。 一、计算机系统漫游(第 1 章) 第 1 章是全书总纲,用"hello world"程序的完整旅程串起整个系统:源文件 → 编译器 → 汇编器 → 链接器 → 可执行文件 → 加载器 → CPU 执行 → 标准输出。 1.1 一条 C 语句的一生 你写下 printf("hello, world\n"),它背后其实经历了"多国接力": 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 hello.c(文本) │ ① 预处理器:展开 #include、宏 ▼ hello.i(展开后的文本) │ ② 编译器 gcc:翻译成汇编 ▼ hello.s(汇编文本,mov/add/call 这些助记符) │ ③ 汇编器 as:把汇编翻译成机器指令(0101...) ▼ hello.o(目标文件,机器码,但地址还没定) │ ④ 链接器 ld:和库合并、分配最终地址 ▼ hello(可执行文件,躺在磁盘上) │ ⑤ 加载器:把它读进内存,从 main 开始执行 ▼ CPU 取指 → 译码 → 执行 → 访存 → 写回 ▼ 通过系统调用 write() 把字节交给操作系统 → 终端 注意一个细节:编译器的输出不是二进制,而是汇编——这是人和机器之间的一层"翻译手稿",后面第 3 章就是专门讲怎么读懂它。每一层都是下一层用"更接近机器的语言"重写一遍,直到变成 CPU 认识的 0/1。 ...

August 15, 2026 · 24 min · 5028 words

任务队列 — TaskQueue、SerialTaskQueue、ConcurrentTaskQueue

第 14 课:任务队列 — TaskQueue、SerialTaskQueue、ConcurrentTaskQueue 对应源文件: trantor/utils/TaskQueue.h — 抽象基类 trantor/utils/SerialTaskQueue.h / SerialTaskQueue.cc — 串行执行队列 trantor/utils/ConcurrentTaskQueue.h / ConcurrentTaskQueue.cc — 并发线程池队列 一、为什么需要 TaskQueue? EventLoop 是单线程的,其中不能执行任何阻塞操作(数据库查询、文件 I/O、耗时计算),否则整条链路的 I/O 响应都会被拖慢。 TaskQueue 提供了一个"卸载阻塞任务"的机制: 1 2 3 4 5 6 [EventLoop 线程] [TaskQueue 线程] 收到玩家请求 │ → 投递到 TaskQueue ────────────►│ 执行 DB 查询(可阻塞) → 立即返回,处理下一个事件 │ 查询完成 │ → 回调投递回 EventLoop ◄── 收到结果,发送响应 ────────────┘ 这是异步编程的基本模式:不阻塞事件循环,把耗时操作委托给专用线程。 二、TaskQueue — 抽象基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class TaskQueue : public NonCopyable { public: // 纯虚:子类实现具体的投递方式 virtual void runTaskInQueue(const std::function<void()> &task) = 0; virtual void runTaskInQueue(std::function<void()> &&task) = 0; virtual std::string getName() const { return ""; } // 同步执行:投递任务并阻塞等待完成(基类实现,子类免费获得) void syncTaskInQueue(const std::function<void()> &task) { std::promise<int> prom; std::future<int> fut = prom.get_future(); runTaskInQueue([&]() { task(); prom.set_value(1); // 任务完成,解锁调用方 }); fut.get(); // 阻塞等待 } }; syncTaskInQueue 的精妙之处: ...

March 29, 2025 · 8 min · 1504 words

协程世界的并发编排:从需求反推 when_all 的实现

协程世界的并发编排:从需求反推 when_all 的实现 本专栏文章:拆开 cppcoro 给你看 · 第 3 篇 这篇文章我想换个写法。前两篇是"先给答案再解释",这篇反过来——从需求出发,一步步推到最终实现。 需求很简单:三个查询并发执行,全部完成后取结果。但从这个需求到最终的 when_all,踩了三个坑:顺序等待太慢 → 手动管理太繁琐 → 注册竞态太难搞。我们逐个填。 1. 场景引入:三个并发查询 1 2 3 task<User> load_user(int id); task<Order> load_orders(int userId); task<Address> load_address(int userId); 尝试 1:顺序等待(慢) 1 2 3 4 5 6 task<void> handle(int userId) { auto user = co_await load_user(userId); // ~200ms auto orders = co_await load_orders(user.id); // ~300ms auto address = co_await load_address(user.id); // ~100ms // 总耗时: ~600ms——三个没有依赖的操作却串行执行了 } 尝试 2:手动管理 counter(繁琐) 每次需要并发时都要手动写计数器逻辑——大量重复代码,容易出错。 我们需要一个泛化的并发执行工具。这就是 when_all 的由来。 2. 核心问题:怎么知道"所有任务都完成了"? 答案就是上一篇文章提到的 when_all_counter: 1 2 3 4 class when_all_counter { std::atomic<std::size_t> m_count; std::coroutine_handle<> m_awaitingCoroutine; }; 让我们从头推导它的设计。 Step 1:计数器初始化 1 when_all_counter counter(3); // 3 个任务 Step 2:每个任务完成时递减 1 2 3 4 5 6 void notify_awaitable_completed() noexcept { if (m_count.fetch_sub(1, std::memory_order_acq_rel) == 1) { // 我是最后一个完成的 → 唤醒等待者 m_awaitingCoroutine.resume(); } } Step 3:等待者注册自己 1 2 3 4 5 6 bool try_await(std::coroutine_handle<> awaitingCoroutine) noexcept { m_awaitingCoroutine = awaitingCoroutine; return m_count.fetch_sub(1, std::memory_order_acq_rel) > 1; // >1 → 还有任务没完成 → 挂起 // ==1 → 所有任务在注册前就完成了 → 不挂起 } 为什么 m_count 初始 = 任务数 + 1? 💡 这是本文第一个关键洞察——额外的那 1 票代表"注册尚未完成"。 ...

November 18, 2024 · 7 min · 1283 words