《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

拆开 Hical:Router 是怎么做到 O(1) 路由匹配的?

[Hical] Router 是怎么做到 O(1) 路由匹配的?兼谈 ~40ns 的 dispatchSync 快速路径 本专栏文章:拆开 Hical · 第 2 篇 上一篇我们跟踪了一个 HTTP 请求从 socket 字节到响应序列化的全过程。走到 Router.dispatch() 这一步时我们跳过去了——现在把它展开。 一个 HTTP 框架的 Router 基本只有一件事要做:给定 method + path,找到对应的 handler。听起来简单,但"怎么找"的差异可以把延迟拉开一个数量级。 1. 三种路由、三种策略 Hical 的 Router 支持三种路由: 1 2 3 静态路由: GET /api/users → hash map O(1) 参数路由: GET /api/users/{id} → per-method vector 线性匹配 通配符路由: GET /static/*path → 优先级最低,兜底匹配 匹配优先级:静态 > 参数 > 通配符。为什么是这个顺序?静态路由一 hash 命中就返回,参数路由需要逐个匹配,通配符是兜底的——先试最快的。 2. 静态路由:透明哈希消除 string_view→string 转换 2.1 问题 静态路由的 key 是 method + path 的组合。如果存在 unordered_map 里: ...

August 10, 2026 · 5 min · 937 words

拆开 Hical:Vyukov MPSC 无锁队列在 HTTP 服务器上的实战——GenericConnection 写路径

[Hical] Vyukov MPSC 无锁队列在 HTTP 服务器上的实战:GenericConnection 的写路径 本专栏文章:拆开 Hical · 第 4 篇 前面三篇都在 HTTP 层面打转——请求怎么解析、路由怎么匹配、中间件怎么执行。这一篇沉到网络层,看一个具体的问题:多个协程想往同一个 socket 写数据时,怎么不加锁? 答案藏在 GenericConnection 的 Vyukov MPSC 无锁队列里。 1. 问题:多个协程同时往一个连接上写 先搞清楚为什么会有这个问题。HTTP/2 和 WebSocket 都允许在一个 TCP 连接上并发地处理多个"流": 1 2 3 线程 A(协程处理 WebSocket frame)──→ 想往 socket 写数据 线程 B(协程处理心跳 ping) ──→ 也想往 socket 写数据 线程 C(IO 线程正在写上一批数据) ──→ socket 只能同时一个写操作 传统的做法是 std::mutex + std::queue。但这里有三个痛点: 生产者多、消费者一个:多个协程往队列里塞数据,只有一个写协程取出来发给 socket mutex 竞争:每秒几十万次 send → 几十万次 mutex lock/unlock → 内核态的 futex 开销 队列长度短:大多数时候队列深度 < 5,争锁的开销比实际写数据还大 2. Vyukov MPSC 队列:核心原理 Dmitry Vyukov 的 MPSC 队列专门为"多生产者、单消费者"场景设计。先直观理解——想象排队买票: ...

August 10, 2026 · 5 min · 984 words

拆开 Hical:一个 HTTP 请求从 socket 字节到路由分发的完整旅程

[Hical] 一个 HTTP 请求在 Hical 里经历了什么?从 socket 字节到路由分发 本专栏文章:拆开 Hical · 第 1 篇 用过 Web 框架的人很多,知道"一个请求怎么从 socket 字节变成 handler 参数"的人很少。大部分框架把这层封装得严严实实,你只要写 app.get("/", handler) 就行。 但这篇文章要干相反的事——把 Hical 的整个请求处理链路扒开看。读完你能回答:请求头为什么零堆分配、响应头怎么一次 async_write 发出去、ReadBufferPool 为什么是请求级 borrow 而不是连接级持有。 1. 大图:一个请求的完整旅程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 socket 上来了字节 │ ▼ readBuf = ReadBufferPool::acquire() ← 借一块 8KB 缓冲 │ ▼ picohttpparser 解析 → phr_header[64] ← 请求头解析到栈数组 │ ▼ 构造 NativeRequest (string_view) ← 零拷贝,指针指到 readBuf │ ▼ Router::dispatch(req) ──→ handler 协程 ← 你的业务代码 │ ▼ NativeResponse::serializeHeadTo(buf) ← 序列化到栈上 512 字节 │ ▼ async_write(socket, buf) ← 一次异步写 │ ▼ readBuf 还给 ReadBufferPool::return_() ← 归还 8KB 缓冲 下面每一步都拆开讲"为什么这样设计"。 ...

August 10, 2026 · 6 min · 1273 words

拆开 Hical:一套 API 两套实现——C++26 反射双轨是怎么设计的

[Hical] 一套 API,两套实现:C++26 反射双轨是怎么设计的 本专栏文章:拆开 Hical · 第 7 篇(完结篇) 这是系列的最后一篇,也是最"前沿"的一篇——C++26 反射在中文技术社区几乎没有实战文章,因为它太新了。Hical 是目前全网唯二(或许唯一)在 open-source 项目中用 C++26 反射的生产级框架。 但 Hical 等不到所有编译器都支持 P2996——它必须兼容 GCC 14、Clang 20、MSVC 2022。所以它做了一套双轨架构:C++26 原生反射和 C++20 宏 fallback 提供相同的用户 API。 1. 问题:你要 JSON 序列化,但不想每个 struct 手写 to_json/from_json 假设你定义了一个 DTO: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct User { std::string name; int age; std::string email; }; // 你想要的: auto json = hical::meta::toJson(user); // → {"name":"张三","age":25,"email":"z3@example.com"} auto user2 = hical::meta::fromJson<User>(json); // 你不想要的: boost::json::object to_json(const User& u) { boost::json::object obj; obj["name"] = u.name; obj["age"] = u.age; obj["email"] = u.email; return obj; } // ↑ 每个 DTO 手写一遍,字段增删时忘了更新序列化 → 静默 bug 传统方案(nlohmann/json)靠宏来自动生成,但需要每个类型手动注册。Hical 的做法是让框架自动发现结构体的字段。 ...

August 10, 2026 · 5 min · 963 words

拆开 Hical:中间件洋葱模型怎么做到连续 N 个同步中间件只分配一次协程帧?

[Hical] 中间件洋葱模型怎么做到"连续 N 个同步中间件只分配一次协程帧"? 本专栏文章:拆开 Hical · 第 3 篇 上一篇文章讲了 Router 的 dispatchSync——当 handler 是同步的,跳过协程帧分配。同样的思路也应用在中间件上。 大多数框架的中间件是"一层一个协程"——3 个中间件 = 3 个协程帧 = 3 次堆分配。但 Hical 问了一个问题:如果 N 个中间件都是同步的(不用 co_await),为什么不能把它们合并成一个协程帧? 答案是:可以。而且 Hical 实现了。 1. 先理解洋葱模型 1.1 标准中间件的三个角色 一个 HTTP 请求经过中间件管道时,有三种类型的操作: 1 2 3 请求进来 ──→ before1 ──→ before2 ──→ handler ──→ after2 ──→ after1 ──→ 响应出去 ↑ ↑ ↑ ↑ 前置拦截 前置处理 后置修改 后置处理 Before:在 handler 之前执行。可以拦截请求、直接返回响应(如认证失败返回 401) Handler:核心业务逻辑 After:在 handler 之后执行。可以修改响应头(如加安全头、压缩 body) 1.2 传统方案:一层一个协程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 全异步中间件链:N 个中间件 = N 个协程 lambda MiddlewareNext makeChain(asyncHandler1, asyncHandler2, asyncHandler3, finalHandler) { // 从内到外构建 auto inner = [h3, finalHandler](req) -> Awaitable<HttpResponse> { co_return co_await h3(req, finalHandler); }; auto middle = [h2, inner](req) -> Awaitable<HttpResponse> { co_return co_await h2(req, inner); }; auto outer = [h1, middle](req) -> Awaitable<HttpResponse> { co_return co_await h1(req, middle); }; return outer; } 3 个协程 lambda → 3 个协程帧 → 3 次堆分配。 ...

August 10, 2026 · 6 min · 1099 words

拆开 Hical:为什么不用每个连接一个 timer 协程?IdleScanner 集中式空闲扫描设计

[Hical] 为什么不用每个连接一个 timer 协程?IdleScanner 的集中式空闲扫描设计 本专栏文章:拆开 Hical · 第 5 篇 本篇讲一个大多数 HTTP 框架都有的、但很少被拿出来单独聊的功能:空闲连接超时断开。 功能本身不复杂——如果一个 keep-alive 连接 60 秒没有新请求,就关掉它。但"怎么实现"有两种完全不同的路线,方案选择直接影响内存和 CPU 成本。 1. 方案 A:每连接一个 timer 协程(大多数框架的做法) 1 2 3 4 5 6 7 8 9 为每个连接创建一个 steady_timer,然后 co_await 在它上面: co_spawn(io_context, [self]() -> Awaitable<void> { steady_timer timer(60s); auto result = co_await (timer.async_wait() || socket.async_read(...)); // 如果 timer 先到 → 超时关闭 // 如果 socket 先到 → 取消 timer,处理请求 }); 问题在哪? ...

August 10, 2026 · 4 min · 750 words

拆开 Hical:你的协程内存去哪里了?三层 PMR 内存池设计

[Hical] 你的协程内存去哪里了?三层 PMR 内存池设计 本专栏文章:拆开 Hical · 第 6 篇 前几篇我们都在聊"怎么做",这篇聊"内存去哪了"。 HTTP 服务器处理一个请求时,至少有这些内存分配:读缓冲(8KB)、请求头解析、URL decode、body 字符串、JSON 对象、响应序列化缓冲、中间件属性注入。每秒 10 万请求 → 每秒数百万次的 malloc/free。 C++17 的 PMR(多态内存资源)给了我们一个机会——换掉默认的 new/delete,用分层的分配器策略。Hical 把这个机会用到了极致。 1. PMR 的最简入门 如果你没用过 PMR,这里 30 秒快速理解: 1 2 3 4 5 6 7 8 // 传统方式:全局 new/delete(不知道这内存在哪、持续多久) std::string s = "hello"; // malloc → free // PMR 方式:你指定分配器的"上游" char buf[1024]; // 栈上的一块内存 std::pmr::monotonic_buffer_resource pool(buf, sizeof(buf)); std::pmr::string s("hello", &pool); // 分配在栈上的 buf 里! // pool 析构 → buf 回收 → 不需要 free! PMR 的核心思想:分离"我要内存"和"从哪拿内存"。 不同的分配器适合不同的场景: ...

August 10, 2026 · 4 min · 825 words

性能优化思路:从测量到代码的五层递进方法论

性能优化思路:从测量到代码的五层递进方法论 优化不靠直觉,靠数据。这篇文章是我在给 Hical 做性能优化时沉淀下来的一套方法论:从"用 profiler 找到真正的瓶颈",到架构选型、系统间交互、数据结构、再到最后的代码级微优化,一共五层,层层递进。每一层都有对应的工具、实战案例和"什么时候别优化"的提醒。看完你会发现,大部分性能问题根本轮不到写代码,先想清楚在哪一层出手,比埋头优化重要得多。 目录 第零层:测量——找到真正的瓶颈 性能优化五层模型 架构级性能优化 (子)系统间性能优化 数据结构与算法性能优化 代码级性能优化 性能回归检测:优化完了怎么守住 第零层:测量——找到真正的瓶颈 优化不是凭直觉猜,是数据驱动的工程决策。没有 profiling 数据就动手,相当于在黑屋子里开枪——打中算运气,打不中浪费子弹。我在 Hical 上踩过的第一个坑就是猜错了方向:光凭代码感觉觉得是协程帧太多,结果 perf 一跑,14.5% 的 CPU 其实耗在调度器上。所以,先量,再动。 工具链:每种工具回答一个问题 工具 回答什么问题 Hical 实战 perf record + 火焰图 CPU 时间花在哪了 perf record -g -F 99 → docker 内 10s wrk strace -c 系统调用频次和时间占比 epoll_ctl 31351 次占 25.92% GTest 性能用例 函数级性能断言 test_router_perf.cpp,100K 次 dispatch 计时 wrk / wrk2 端到端吞吐和延迟分布 wrk -c100 -t4 -d30s heaptrack / valgrind massif 内存分配热点 找意外的大分配 perf stat -d IPC、分支预测率、cache miss 看整体健康度 火焰图怎么看 拿 Hical 实际火焰图数据来说(docker 内 wrk -c10000 -d10s,perf record -g -F 99): ...

August 1, 2026 · 13 min · 2748 words