《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

拆开 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:你的协程内存去哪里了?三层 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

从零理解 MPSC 无锁写队列:高性能网络框架的发送引擎

写在前面 你有没有想过,一个高并发网络服务器在同时给几千个客户端发消息时,底层到底在忙什么? 最直觉的做法是加把锁——谁要发消息就抢锁、写 socket、放锁。但问题来了:如果 1000 个协程同时要给同一个连接发数据,它们就得排队等这把锁。锁竞争带来的上下文切换、cache 失效,会把性能拖进泥潭。 Hical 框架的解法很酷:用一个 MPSC(多生产者单消费者)无锁队列 做缓冲,配合一个协程写循环做消费。发消息的线程只管往队列里扔节点(wait-free,永远不阻塞),写循环协程在 IO 线程上批量取出、合并发送。 这篇文章会带你从零搞懂这个设计。不需要你有无锁编程的基础,但最好知道 C++ 的 atomic、shared_ptr 和"什么是协程"大概是怎么回事。 一、先搞清楚问题:为什么普通的锁不行? 1.1 一个典型场景 假设你写了个聊天服务器。用户 A 发了条群消息,服务器要转发给群里 200 个在线用户。这意味着: 1 用户A的消息 → 广播逻辑 → 同时调用 200 个连接的 send() 如果 send() 内部用 mutex 保护写操作: 1 2 3 4 5 6 7 8 9 10 11 // 朴素实现(有严重性能问题) void Connection::send(std::string data) { std::lock_guard lock(writeMutex_); // 200个协程在这里排队 writeBuffer_.append(data); if (!writing_) { writing_ = true; doWrite(); // 触发实际的 socket 写入 } } 问题出在哪? ...

May 28, 2026 · 10 min · 2127 words

协程框架高并发翻车了?三个C++ Web框架实测,结果出乎意料

协程框架高并发翻车了?三个C++ Web框架实测,结果出乎意料 上一版测完总觉得哪里不对,于是改了配置又跑了一遍——这次结果老实多了。 前言:为什么重新测了一遍 这篇文章其实是第二版了。上一篇发出来之后我越想越不对劲——当时每个容器只给了 512MB 内存,fd 上限也没显式调高,跑 10K 并发的时候实际上根本没撑到一万个连接。wrk 报了一堆 connect 8983 错误,真正建立的有效连接也就一千出头,所谓的"高并发 10K"测试本质上测的还是千级并发。那组数据里 Hical 高并发"遥遥领先"的结论,现在看有水分。 这次我把配置拉满了:每容器 1024MB 内存、nofile 上调到 65536,VM 内核参数也做了对应调优(somaxconn、tcp_max_syn_backlog、端口范围等),确保 wrk 发起的一万个连接能真正建立起来。同时精简了文章的对比范围——压测还是 6 个框架一起跑的(数据完整保留在文章末尾),但本文只聚焦 Hical、Drogon、Cinatra 三个第一梯队选手做深入分析。这三个在上一版常规场景里已经拉开了和其余框架的差距,继续带着 91 QPS 的 cpp-httplib 同框只会让图表失真。 结果确实和上次不一样,尤其是高并发部分的排名翻了个个儿。先把结论放在前面:常规负载下三个框架打得很近,但各自有各自的强项——Hical 在 JSON 和中间件场景领先,Cinatra 在纯吞吐和 JSON Echo 上更快,Drogon 在高并发场景下表现最好。 测试环境 宿主机硬件: 项目 配置 处理器 Intel Core i7-11700K @ 3.60GHz(8核16线程) 内存 32 GB 存储 SSD(900 GB) 宿主系统 Windows 10 Enterprise LTSC 2021 虚拟化环境: 项目 配置 虚拟化平台 Oracle VM VirtualBox 7.1(半虚拟化接口: KVM) Guest OS Ubuntu 24.04.3 LTS Server VM 分配 16 GB RAM / 8 CPU Docker Engine 29.4.3 + Compose v5.1.3 每框架容器 4 CPU + 1GB RAM,nofile=65536 压测工具 wrk 4.1.0(独立容器),4 线程 默认并发 100 连接 持续时间 30 秒 编译器 GCC 14.2(Ubuntu 24.04) 优化级别 Release(-O2) 采样方式 每场景连续跑 3 轮,取算术平均值 注:所有框架运行在同一台 VM 的 Docker 容器中,wrk 也在同一 Docker 网络内发起请求(容器间通信,无宿主机网络栈介入)。VirtualBox 虚拟化会引入一定开销,绝对 QPS 数值会低于裸机,但各框架的相对排名通常稳定。 ...

May 25, 2026 · 10 min · 2063 words

Hical 性能优化全记录

优化背景 Hical 是我写的 C++20/26 Web 框架,跑 Hello World 压测时起初只有 27K QPS,而同类框架(Cinatra 165K、Drogon 170K)差了将近一个数量级。目标很明确:追平 Cinatra/Drogon 的水平。 整个优化过程分 6 个阶段,不是拍脑袋乱改,每一步都是 perf + 火焰图定位瓶颈 → 想方案 → 写代码 → 跑压测验证 的循环。能看到数字变化才算数。 阶段 1:协程帧削减(v2.5.1-v2.5.2) 发现问题 perf 火焰图第一个大头:14.5% CPU 在 scheduler::wake_one_thread_and_unlock + pthread_cond_signal。 一开始以为是跨线程调度问题,仔细一看不是——是 Boost.Asio scheduler 每次 co_await resume 都要走的内部调度流程太重了。一个 Hello World 请求居然走了 4 个协程帧: 1 2 3 4 5 handleSession: co_await async_read → 帧 1(必需,I/O 等待) co_await router_.dispatch() → 帧 2(Router 本身是协程) co_await handler(req) → 帧 3(同步 handler 被包装成协程,不必要!) co_await async_write → 帧 4(必需,I/O 等待) 帧 1 和 4 是真正的 I/O 等待不可消除,但帧 2 和 3 完全是浪费——一个同步的 return HttpResponse("Hello") 被裹了两层协程。 ...

May 22, 2026 · 9 min · 1794 words

Hical 踩坑实录五部曲(五):Boost.MySQL 协程集成的 5 个坑

Hical 踩坑实录五部曲(五):Boost.MySQL 协程集成的 5 个坑 引言 Hical 的数据库模块(src/db/)是一个基于协程的连接池 + 中间件系统,后端使用 Boost.MySQL 的 any_connection。从 “能跑” 到 “能在生产环境跑”,中间踩了不少坑。 这篇文章记录了 Boost.MySQL 协程集成过程中遇到的 5 个真实问题,每个都附带完整的解决方案代码。 目录 Hical 踩坑实录五部曲(五):Boost.MySQL 协程集成的 5 个坑 引言 目录 坑 1:any_connection vs 强类型连接的取舍 坑 2:PreparedStatement 失效与自动重试 坑 3:SET NAMES 注入风险——validateCharset 白名单 坑 4:连接池 acquire 超时的竞争窗口 坑 5:事务忘记 commit/rollback 的自动回滚设计 第一道防线:DbMiddleware 洋葱模型 第二道防线:连接池 release 兜底回滚 附:StmtCache LRU 缓存设计 总结:Boost.MySQL 集成检查清单 坑 1:any_connection vs 强类型连接的取舍 现象:第一版连接池用 Boost.MySQL 的强类型连接(tcp_ssl_connection),结果泛型代码全部被迫模板化——编译时间爆炸,且无法在运行时根据配置切换 TCP/SSL。 强类型方式的问题: 1 2 3 4 5 6 7 8 9 10 11 // ❌ 强类型——泛型代码必须模板化 template <typename Connection> class DbPool { std::vector<std::unique_ptr<Connection>> idle_; // Connection 是 tcp_connection 还是 tcp_ssl_connection? // 中间件也要模板化、查询日志也要模板化... }; // 编译时决定,运行时无法切换 using Pool = DbPool<boost::mysql::tcp_ssl_connection>; 解决方案:any_connection——类型擦除,运行时决定传输层: ...

May 11, 2026 · 8 min · 1605 words

Hical 踩坑实录五部曲(一):Boost.Asio 协程开发的 N 个坑

Hical 踩坑实录五部曲(一):Boost.Asio 协程开发的 N 个坑 引言 Hical 的所有异步 I/O 都基于 Boost.Asio 协程(co_await + boost::asio::use_awaitable)。路由处理器返回 Awaitable<HttpResponse>,中间件用洋葱模型 co_await next(req),连接池用 co_await timer.async_wait() 做非阻塞等待。 协程消除了回调地狱,但引入了一套全新的陷阱。这篇记录的每一个坑,都是在压测或线上环境中真实触发过的。 目录 Hical 踩坑实录五部曲(一):Boost.Asio 协程开发的 N 个坑 引言 目录 坑 1:co_await 后 this 悬挂——对象已析构 坑 2:协程异常传播——catch 里不能 co_await 坑 3:steady_timer 当协程信号量的技巧 坑 4:jthread vs thread——精准匹配停止信号 坑 5:多线程 io_context + 协程的线程安全陷阱 坑 6:detached 协程的异常黑洞 坑 7:io_context::stop() 不等于安全退出 总结:协程安全编程检查清单 坑 1:co_await 后 this 悬挂——对象已析构 现象:压测时低概率崩溃,堆栈指向 TcpServer 的 accept 循环,访问了已释放的内存。 最小复现: 1 2 3 4 5 6 7 8 9 10 11 // ❌ 危险的写法 Awaitable<void> TcpServer::acceptLoop() { while (running_) { auto socket = co_await acceptor_.async_accept(use_awaitable); // ⚠️ 如果在 co_await 期间 TcpServer 被析构, // this 已经是悬空指针! this->createConnection(std::move(socket)); // 💥 use-after-free } } 根因:协程帧通过 co_spawn(io_context, coroutine, detached) 提交到 io_context。协程帧的生命周期由 io_context 管理,与创建协程的对象完全分离。 ...

May 7, 2026 · 8 min · 1586 words

Hical 协程入门:告别回调地狱,用 co_await 写异步 C++

Hical 协程入门:告别回调地狱,用 co_await 写异步 C++ 传统 C++ 异步编程离不开回调嵌套、状态机、手动生命周期管理——代码写得像意大利面。C++20 协程从根本上改变了这一切:异步代码写起来和同步一样直观,编译器帮你管理暂停与恢复。本文从零讲解如何在 Hical 框架中使用协程,不需要你懂 Boost.Asio 底层。 什么是协程?30 秒版本 传统回调式: 1 2 3 4 5 6 7 8 9 10 11 12 // 回调嵌套——"回调地狱" socket.async_read(buffer, [&](error_code ec, size_t n) { if (!ec) { socket.async_write(buffer, [&](error_code ec2, size_t) { if (!ec2) { socket.async_read(buffer, [&](error_code ec3, size_t) { // 继续嵌套... }); } }); } }); 协程式: 1 2 3 4 // 同样的逻辑,协程版——像写同步代码一样 auto n = co_await socket.async_read(buffer, use_awaitable); co_await socket.async_write(buffer, use_awaitable); auto n2 = co_await socket.async_read(buffer, use_awaitable); co_await 会暂停当前函数,等 I/O 完成后自动恢复执行。没有回调,没有嵌套,错误用 try/catch 处理。 Hical 对协程做了什么封装? Hical 在 Coroutine.h 中提供了三个核心工具: ...

May 5, 2026 · 4 min · 762 words