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

深入学习 Boost.Asio(三):协程进阶与实战项目

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 阅读本篇前,请确保已掌握: 入门篇:io_context、异步操作生命周期、定时器 进阶篇:协程 Echo Server、多线程模型、strand 1. 协程进阶技巧 1.1 co_spawn 的第三个参数 co_spawn 的第三个参数决定了协程完成后的行为: 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 #include <boost/asio.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/use_awaitable.hpp> using boost::asio::awaitable; using boost::asio::use_awaitable; awaitable<int> compute() { co_return 42; } awaitable<void> mayFail() { throw std::runtime_error("oops"); co_return; } void examples(boost::asio::io_context& ioCtx) { // 方式1:detached —— 忽略返回值和异常 // 适用:独立运行的协程(如连接处理) boost::asio::co_spawn(ioCtx, compute(), boost::asio::detached); // 方式2:回调 —— 协程完成时执行回调 // 适用:需要捕获协程异常或获取返回值 boost::asio::co_spawn(ioCtx, mayFail(), [](std::exception_ptr e) { if (e) { try { std::rethrow_exception(e); } catch (const std::exception& ex) { std::cerr << "协程异常: " << ex.what() << "\n"; } } }); // 方式3:use_awaitable —— 在协程中等待另一个协程 // 适用:父子协程关系 // (需要在协程内使用) } // 方式3 完整示例 awaitable<void> parent(boost::asio::io_context& ioCtx) { // 等待子协程完成并获取返回值 int result = co_await boost::asio::co_spawn( ioCtx, compute(), boost::asio::use_awaitable); std::cout << "子协程返回: " << result << "\n"; // 42 } 1.2 超时控制 生产环境中,你不能无限等待一个操作完成。Asio 提供了 awaitable_operators 实现竞争式等待: ...

May 21, 2026 · 10 min · 1926 words

深入学习 Boost.Asio(二):TCP 编程与多线程模型

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 阅读本篇前,请确保已理解 入门篇 中的以下概念: io_context 的作用和 run() 执行流程 异步操作的生命周期(发起 → 完成 → handler 执行) post/dispatch 的区别 1. TCP 编程:三步演进 我们通过构建一个 Echo Server(收到什么就回什么),从最简单的同步版本逐步演进到生产级协程版本。 1.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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // echo_server_sync.cpp // 编译: g++ -std=c++20 echo_server_sync.cpp -lboost_system -lpthread -o echo // 测试: 另开终端 nc localhost 9999,输入文字会回显 #include <boost/asio.hpp> #include <iostream> using boost::asio::ip::tcp; int main() { boost::asio::io_context ioCtx; // 创建 acceptor:监听 TCP 连接 // 参数:io_context, 绑定地址(IPv4, 端口9999) tcp::acceptor acceptor(ioCtx, tcp::endpoint(tcp::v4(), 9999)); std::cout << "同步 Echo Server 监听端口 9999\n"; while (true) { // accept() 阻塞,直到有客户端连接 tcp::socket socket(ioCtx); acceptor.accept(socket); std::cout << "客户端连接: " << socket.remote_endpoint().address().to_string() << ":" << socket.remote_endpoint().port() << "\n"; // 处理这个连接(阻塞:处理期间无法接受新连接!) boost::system::error_code ec; char buf[1024]; while (true) { // read_some:读取可用的数据(可能只有一部分) size_t n = socket.read_some(boost::asio::buffer(buf), ec); if (ec == boost::asio::error::eof) { std::cout << "客户端断开\n"; break; } if (ec) throw boost::system::system_error(ec); // 将收到的数据原样写回 boost::asio::write(socket, boost::asio::buffer(buf, n)); } } return 0; } 问题:同一时刻只能服务一个客户端。当客户端 A 连接后,客户端 B 必须等 A 断开才能被接受。 ...

May 20, 2026 · 10 min · 2026 words

深入学习 Boost.Asio(一):从原理到 io_context

系列导航:入门篇 | 进阶篇 | 实战篇 引言:为什么需要异步 I/O? 假设你在写一个聊天服务器,同时连接 1000 个用户。如果用传统的"一个线程处理一个连接"模型: 1 2 3 4 线程1: read(socket_1) ← 阻塞等待用户1输入... 线程2: read(socket_2) ← 阻塞等待用户2输入... ... 线程1000: read(socket_1000) ← 阻塞等待用户1000输入... 问题:1000 个线程各自阻塞在 read() 上,每个线程占用 ~1MB 栈内存(合计 ~1GB),还有大量的上下文切换开销。这就是经典的 C10K 问题。 异步 I/O 的解决思路:用 1 个线程(或少量线程)管理所有连接,操作系统在数据就绪时通知我们: 1 2 3 4 5 6 单线程事件循环: ┌→ 等待事件(epoll/IOCP) │ ├─ socket_7 可读 → 处理用户7的消息 │ ├─ socket_42 可读 → 处理用户42的消息 │ └─ socket_100 可写 → 继续发送给用户100 └─ 回到等待 Boost.Asio 就是 C++ 中实现这一模型的工业级库。本篇将带你理解它的底层原理和核心组件。 ...

May 18, 2026 · 10 min · 1990 words

连接级 Atomic 时间戳超时的实现决策

起因 最初 Hical 的空闲超时实现就是传统做法:每个 HTTP 请求/每次 keep-alive 读等待都注册一个 steady_timer,读完成后取消,超时则关闭连接。实现上用的是 shared_ptr<function> 自引用环做回调链续期——每连接 2 次堆分配(shared_ptr 控制块 + function 对象),且每次续期都要重新构造回调。 v2.5.2 压测到 132K QPS 时,做热路径Review代码发现这个 timer 机制的问题: 每请求 2 次 epoll_ctl(注册 + 取消 timer) shared_ptr<function> 自引用环本身就有堆分配开销 140K QPS 下整体约产生 100 万次 epoll_ctl/sec,38% CPU 花在内核 _raw_spin_unlock_irqrestore(TCP spin_lock),而用户态框架代码只占不到 5% 瓶颈已经从用户态转移到内核态,减少进内核的次数成为核心策略。空闲超时的 timer 是明确可以砍掉的——30-60s 的超时精度要求本来就极低。 改良过程 分两步走: 第一步:先把 shared_ptr<function> 回调链改为独立协程 idleTimerLoop,消除自引用环和 2 次堆分配。这一步还是 per-connection 一个 timer 协程,只是实现更干净了。 第二步:发现即便使用协程,per-connection timer 仍然意味着每次 timer 到期时要走 scheduler 调度 + epoll_ctl。最终演化为"TcpServer 统一扫描"的设计——整个 server 只需要一个扫描协程,连接侧只写一个 atomic 值。 ...

May 12, 2026 · 2 min · 314 words

Hical v2.5.2 性能优化实战:SO_REUSEPORT + 连接级 Timer 实现 3 倍 QPS 提升

Hical v2.5.2 性能优化实战:SO_REUSEPORT + 连接级 Timer 实现 3 倍 QPS 提升 在 火焰图分析中,我们定位到 Hical 的 QPS 瓶颈在 Boost.Asio 的 epoll 交互模型——跨线程调度(14.5%)和 timer 相关 epoll_ctl(12.5%)合计吃掉了 27% 的 CPU。[P1 优化](Router 同步快速路径)无实质提升后,本文记录 P2/P3 两项优化的设计思路、实现细节和实测结果。 目录 1. 背景回顾 2. 优化方案 A:SO_REUSEPORT 多 Acceptor 3. 优化方案 B:连接级 Timer + Atomic 时间戳 4. 实测结果 5. 剩余差距与后续方向 6. 复现指南 1. 背景回顾 1.1 P1 优化无效的原因 v2.5.2 实现了 Router::dispatchSync() 同步快速路径,在无中间件场景下跳过协程帧分配。三轮 Docker 压测结果: 轮次 QPS 变化 v2.5.1(基线) 27,493 — v2.5.1(静态链接) 19,381 系统波动 v2.5.2(dispatchSync) 20,940 无实质提升 原因:Router::dispatch 在火焰图中仅占 0.24% CPU,同步快速路径省掉的协程帧(~40-130ns)被 Asio 调度层(27%)完全淹没。 ...

May 10, 2026 · 6 min · 1271 words

Hical 性能剖析实战:perf + 火焰图定位 QPS 瓶颈

Hical 性能剖析实战:perf + 火焰图定位 QPS 瓶颈 在 C++ 框架性能实测中,Hical 的 Hello World QPS(~27K)远低于 Cinatra(165K)和 Drogon(161K)。静态链接 + strip 验证后确认瓶颈不在链接方式。本文记录用 perf record + 火焰图精确定位 CPU 热点的全过程。 目录 1. 背景与动机 2. Profiling 环境搭建 3. 数据采集 4. 火焰图分析 5. 优化方向 6. 复现指南 1. 背景与动机 1.1 已排除的因素 在本次 profiling 之前,已经通过对照实验排除了以下因素: 假设 验证方式 结论 动态链接 Boost 有性能损耗 改为 Boost 静态链接,重跑压测 QPS 无显著变化(27K → 27K) strip 影响性能 strip vs 不 strip 对比 无影响(符号表不参与运行时) 二进制体积(icache 压力) 7.8M(strip) vs 9.3M(不strip) QPS 在噪声范围内,非瓶颈 排除结论:性能瓶颈在框架运行时架构,需要 profiling 定位具体热点函数。 ...

May 9, 2026 · 4 min · 687 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

Boost.Asio 学习课程:异步 I/O 与协程

课程导航:学习路径 | Boost.System | Boost.Asio | Boost.Beast | Boost.JSON | Boost.MySQL 前置知识 课程 1: Boost.System(error_code、system_error) C++ 基础:模板、lambda、智能指针 C++20 协程语法(co_await、co_return)——本课程会从零讲解 学习目标 完成本课程后,你将能够: 理解 io_context 的工作原理和生命周期管理 掌握 C++20 协程式异步编程(co_await + use_awaitable) 编写协程式 TCP 服务器和客户端 使用 steady_timer 实现定时任务 理解多线程模型的选型和 strand 序列化 读懂 Hical 的 EventLoop、连接管理和 SSL 集成 目录 前置知识 学习目标 目录 1. 核心概念 1.1 Asio 的设计哲学 1.2 io_context:事件循环的心脏 1.3 Executor 模型:post vs dispatch 1.4 三种异步完成方式 2. 基础用法 2.1 最小 io_context 示例 2.2 TCP 基础:同步与异步 2.3 协程式异步 I/O 2.4 steady_timer 定时器 2.5 buffer 操作 3. 进阶主题 3.1 多线程模型 3.2 strand 序列化执行 3.3 SSL/TLS 支持 3.4 signal_set 信号处理 4. Hical 实战解读 4.1 AsioEventLoop:io_context 的框架封装 4.2 dispatch vs post 实战 4.3 EventLoopPool:多线程池模型 4.4 AsioTimer:定时器的生产级封装 4.5 TcpServer:协程式 accept 循环 4.6 Coroutine.h:协程工具函数 4.7 SSL 集成 5. 练习题 练习 1:协程式 Echo Server 练习 2:周期性日志 练习 3:多 io_context 模型 练习 4:SSL Echo Server 练习 5:协程式 HTTP 客户端 参考答案 练习 1 参考答案:协程式 Echo Server 练习 2 参考答案:周期性日志 练习 3 参考答案:多 io_context 模型 练习 4 参考答案:SSL Echo Server 练习 5 参考答案:协程式 HTTP 客户端 6. 总结与拓展阅读 核心 API 速查表 三种异步模式对比 拓展阅读 下一步 1. 核心概念 1.1 Asio 的设计哲学 Boost.Asio 采用 Proactor 模式——应用程序发起异步操作,操作系统完成后通知应用。 ...

April 15, 2026 · 20 min · 4250 words