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

Hical 框架开发心得:七个深刻教训

Hical 框架开发心得:七个深刻教训 引言 Hical 是一个基于 Boost.Asio 的现代 C++20/26 高性能 Web 框架,采用原生 HTTP/WebSocket 网络栈(picohttpparser + 自研 WebSocket),从第一行代码到现在的 45+ 测试文件、3 层内存池、协程化数据库中间件、自研日志系统、OpenAPI 自动生成、WsHub 广播管理器、QPS 从 27K 到 159K 的优化历程,一路走来踩了不少坑,也收获了很多。 这篇文章不讲 API 用法,也不讲架构教程——那些在其他文章里都有。这篇只聊开发过程中的真实体会:哪些决策事后证明是对的,哪些看似优雅的方案差点把自己埋了,以及最终选择背后的取舍逻辑。 目录 Hical 框架开发心得:七个深刻教训 引言 目录 一、C++20 协程是双刃剑 1.1 协程让异步代码变清晰了……吗? 1.2 co_await 后 this 可能已经死了 1.3 io_context 析构时的协程帧:成员声明顺序陷阱 1.4 co_spawn(detached) 的悬空引用陷阱 1.5 异常传播:catch 里不能 co_await 1.6 收获:协程不是银弹 二、PMR 三层内存池——收益大但陷阱多 2.1 为什么要三层 2.2 踩坑:upstream 选错导致跨线程竞争 2.3 踩坑:allocator 忘了传播 2.4 收获:PMR 的收益在高并发场景才显现 三、模板 + Concepts 比虚函数继承更适合网络框架 3.1 GenericConnection 的零成本分流 3.2 NetworkBackend concept:可替换但不多态 3.3 收获:编译期分支 > 运行时分支 四、自研日志系统的价值 4.1 为什么不用 spdlog 4.2 六层架构的设计决策 4.3 两个关键的性能优化 4.4 收获:核心框架值得自研,应用项目直接用 spdlog 五、双轨反射——为未来留后路 5.1 问题:C++26 还没来,但 API 要现在设计 5.2 宏回退层的实现策略 5.3 收获:用宏模拟未来语言特性 六、移除 Boost.Beast——火焰图驱动的依赖清退 6.1 Beast 到底慢在哪 6.2 picohttpparser + 零拷贝请求 6.3 自研 WebSocket 栈的取舍 6.4 收获:数据驱动的依赖决策 七、WsHub 广播管理器——从"能用"到"好用"的 WebSocket 架构 7.1 问题:裸连接管理的困境 7.2 WsHub 的核心设计 7.3 写串行化:被忽视的并发陷阱 7.4 收获:框架应该管理连接生命周期 后记:ThreadSanitizer CI 揪出的隐藏竞态(v2.6) Boost.Asio 对象的跨线程操作 stop() 的并发调用:门卫模式 教训 总结:七条核心原则 一、C++20 协程是双刃剑 1.1 协程让异步代码变清晰了……吗? 在引入协程之前,一个 HTTP 请求的处理链是嵌套回调: ...

May 18, 2026 · 16 min · 3291 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

Hical 生产部署实践:从编译优化到 Kubernetes 容器化

Hical 生产部署实践:从编译优化到容器化 框架开发完了,测试也通过了——然后呢?“本地跑得好好的"和"线上稳定运行"之间,隔着编译优化、进程管理、反向代理、监控告警、容器编排一整套工程实践。这篇文章把 Hical 从开发环境搬到生产环境的完整链路走一遍,每个环节都给出可直接复用的配置模板。 目录 Hical 生产部署实践:从编译优化到容器化 目录 一、编译优化:榨干最后一点性能 1.1 Release 基础参数 1.2 LTO(链接时优化) 1.3 PGO(Profile-Guided Optimization) 1.4 静态链接 vs 动态链接 二、进程管理:别让服务裸奔 2.1 systemd 服务配置 2.2 信号处理与 Graceful Shutdown 2.3 多线程与多 acceptor(SO_REUSEPORT) 三、反向代理:Nginx 挡在前面 3.1 HTTP 反向代理 3.2 WebSocket 代理 3.3 SSL 终止策略 四、监控与可观测性 4.1 Prometheus 指标暴露 4.2 日志接入 ELK / Loki 4.3 健康检查端点 五、容器化部署 5.1 多阶段 Dockerfile 5.2 docker-compose 完整示例 5.3 Kubernetes 部署参考 六、性能调优检查清单 系统级 Hical 应用级 PMR 内存池 数据库连接池 日志系统 调优流程 一、编译优化:榨干最后一点性能 1.1 Release 基础参数 开发阶段用 Debug 方便调试,上线必须切 Release。区别不只是 -O2,还有 assert 消除、NDEBUG 定义(Hical 的 HICAL_LOG_TRACE 宏在 NDEBUG 下编译期完全消除): ...

May 17, 2026 · 15 min · 2992 words

Heaptrack:找出 C++ 程序中的无效内存分配

Heaptrack:找出 C++ 程序中的无效内存分配 你的火焰图上 malloc/free 占了 8% CPU。你知道分配太频繁了,但——是哪个函数在疯狂 new?每次 new 了多少字节?有没有更好的办法? 故事:每秒 17000 次 malloc,但只有 41 次是浪费的 对我的 C++20/26 Web 框架(Hical)做 Heaptrack 分析时发现:136K QPS 下每秒 17457 次堆分配,但临时分配(分配后很快释放)只有 41 次/秒——说明 PMR 内存池策略生效了。 但第一版代码没有 PMR 时,临时分配高达 13 万次/秒。Heaptrack 精确告诉了我哪些 std::string 和 std::vector 是罪魁祸首,逐个消灭后内存分配开销从 8% 降到 < 0.1%。 这篇教你用 Heaptrack 做同样的事——精确定位哪个函数在做无效分配,然后干掉它。 一、Heaptrack 是什么 Heaptrack 是一个堆内存分配追踪器,记录程序运行期间的每一次 malloc/new/free/delete,告诉你: 总共分配了多少次?多少字节? 哪个函数分配最多?(完整调用栈) 峰值内存使用在哪个时间点? 有没有泄漏(分配了但从未释放)? 临时分配有多少?(分配后很快释放——这是优化首要目标) 对比 Valgrind Massif Heaptrack Valgrind –tool=massif 性能开销 2~5x 减速 20~50x 减速 数据粒度 每次分配的完整调用栈 定期快照 GUI heaptrack_gui(丰富) ms_print(文本) 适用场景 日常分析(推荐) 极精确内存画像 一句话:Heaptrack 是 Valgrind Massif 的现代替代品,快 10 倍,信息更全。 ...

May 15, 2026 · 5 min · 873 words

Linux 性能分析与优化实战指南:perf / 火焰图 / Heaptrack 全流程

Linux 性能分析与优化实战指南 基于 Hical 项目的 Ubuntu 24.04 VM 环境(VirtualBox,8 CPU / 16GB RAM)。 前置条件:已完成 Hical-Linux开发环境 和 VM编译运行Hical-Benchmark流程 的环境搭建。 目录 零、工具安装 一、perf stat:硬件计数器分析 二、perf record + 火焰图:CPU 热点定位 三、Heaptrack:内存分配分析 四、缓存层次与 cache line 五、实战:Hical 性能分析全流程 六、速查卡 零、工具安装 0.1 一键安装所有性能工具 1 2 3 4 5 6 7 8 9 10 11 # perf(必须匹配内核版本) sudo apt install -y linux-tools-$(uname -r) linux-tools-generic # heaptrack(内存分配分析) sudo apt install -y heaptrack heaptrack-gui # FlameGraph(火焰图生成脚本) git clone --depth 1 https://github.com/brendangregg/FlameGraph.git ~/FlameGraph # 辅助工具 sudo apt install -y valgrind strace sysstat hwloc 0.2 内核参数调整(perf / heaptrack 权限) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # ── perf 权限 ── # 查看当前值(默认通常是 4,限制很严) cat /proc/sys/kernel/perf_event_paranoid # 临时放开(重启失效) sudo sysctl -w kernel.perf_event_paranoid=-1 sudo sysctl -w kernel.kptr_restrict=0 # ── ptrace 权限(heaptrack --pid 运行时附着需要) ── # 查看当前值(默认 1,禁止非父进程 ptrace) cat /proc/sys/kernel/yama/ptrace_scope # 临时放开(重启失效) sudo sysctl -w kernel.yama.ptrace_scope=0 # ── 永久生效(写入配置文件) ── cat << 'EOF' | sudo tee /etc/sysctl.d/99-perf.conf kernel.perf_event_paranoid = -1 kernel.kptr_restrict = 0 kernel.yama.ptrace_scope = 0 EOF sudo sysctl --system 各级别含义: ...

May 15, 2026 · 25 min · 5176 words

perf + 火焰图:5 分钟定位 C++ 程序的 CPU 瓶颈

perf + 火焰图:5 分钟定位 C++ 程序的 CPU 瓶颈 你的服务器 CPU 跑满了,QPS 却上不去。top 告诉你"忙",但不告诉你忙在哪。怎么办? 故事:从 27K 到 136K QPS 我开发了一个 C++20/26 Web 框架(Hical),第一次压测只有 27K QPS,而同场景下 Drogon 和 Cinatra 都在 160K+。CPU 使用率 100%,top 没用,gdb 打断点太慢。 最终靠 perf record + 火焰图,5 分钟定位到瓶颈不在我的框架代码(仅占 2% CPU),而在 Boost.Asio 的调度层——跨线程 epoll_ctl 和 per-request timer 合计吃了 27% CPU。 优化后 QPS 从 27K → 136K。 这篇文章把我整套分析流程分享出来。不需要你用过 Hical,任何 C++ 服务器程序都适用。 一、工具安装(2 分钟搞定) 1 2 3 4 5 6 7 8 9 # perf(必须匹配内核版本) sudo apt install -y linux-tools-$(uname -r) linux-tools-generic # FlameGraph 脚本(Brendan Gregg 出品) git clone --depth 1 https://github.com/brendangregg/FlameGraph.git ~/FlameGraph # 放开 perf 权限(否则只能看到自己的进程) sudo sysctl -w kernel.perf_event_paranoid=-1 sudo sysctl -w kernel.kptr_restrict=0 验证: ...

May 15, 2026 · 5 min · 971 words

缓存行对 C++ 性能的影响有多大?实测告诉你

缓存行对 C++ 性能的影响有多大?实测告诉你 面试题:“遍历 vector 比遍历 list 快多少倍?"——答案不是 2 倍,是 10~100 倍。原因只有一个字:缓存。 故事:为什么 vector 存 20 个 HTTP 头比 unordered_map 还快 开发 Hical Web 框架时,我面临一个选择:HTTP 请求头用什么容器存? 直觉说 unordered_map<string, string> 查找 O(1),肯定比 vector<pair<string, string>> 的 O(n) 快。但实测结果打脸——vector 线性扫描 20 个头部,比 unordered_map 哈希查找还快 40%。 原因就是 cache line。这篇文章讲清楚这件事。 一、CPU 缓存:被忽视的性能悬崖 1.1 速度鸿沟 你的程序跑在 CPU 上,但数据存在内存里。两者之间有一道巨大的速度鸿沟: 1 2 3 4 5 6 7 8 9 10 11 ┌──────────┐ │ CPU 寄存器│ ~0.3 ns (1 cycle) ├──────────┤ │ L1 Cache │ ~1 ns (3-4 cycles) 32-48 KB / 核 ├──────────┤ │ L2 Cache │ ~4 ns (10-12 cycles) 256 KB-1 MB / 核 ├──────────┤ │ L3 Cache │ ~12 ns (30-40 cycles) 8-32 MB / 共享 ├──────────┤ │ 主内存 │ ~60-100 ns (150-300 cycles) └──────────┘ 关键数字:L1 和主内存的延迟差 100 倍。 ...

May 15, 2026 · 6 min · 1203 words