CPP-Summit-2022 学习:Modern C++基于性能的重构优化

C++软件性能现状与优化挑战

一、软件性能优化本质

性能优化的核心目标:

减少指令数,提高每秒可执行浮点操作数(GFLOPS),充分利用硬件特性。
通过表格可以看出,不同语言/优化策略的性能提升非常悬殊:

二、性能优化的数学本质

  1. 运行时间与指令数关系
    运行时间 T T T 与总指令数 I I I 和每秒执行速率 R R R 的关系为:
    T = I R T = \frac{I}{R} T=RI​
  • I I I:总指令数(越少越快)
  • R R R:硬件执行速率(可受矢量化、并行化影响)
  1. 加速比
  • 相对原始版本加速(Absolute speedup):
    S abs = T Python T Vn S_\text{abs} = \frac{T_\text{Python}}{T_\text{Vn}} Sabs​=TVn​TPython​​
  • 相对前一版本加速(Relative speedup):
    S rel = T V(n-1) T Vn S_\text{rel} = \frac{T_\text{V(n-1)}}{T_\text{Vn}} Srel​=TVn​TV(n-1)​​
  1. 峰值利用率
    Fraction of peak = GFLOPS achieved GFLOPS peak × 100 \text{Fraction of peak} = \frac{\text{GFLOPS achieved}}{\text{GFLOPS peak}} \times 100% Fraction of peak=GFLOPS peakGFLOPS achieved​×100

三、性能优化策略与效果

  1. 语言选择与基础优化
  • Python → Java → C
  • 指令数逐渐减少,运行效率提升:
    T Python = 25 , 552.48 , s , T C = 542.67 , s T_\text{Python} = 25,552.48,s,\quad T_\text{C} = 542.67,s TPython​=25,552.48,s,TC​=542.67,s
  • 加速比:
    S abs = 47 ( C vs Python ) S_\text{abs} = 47 \quad (\text{C vs Python}) Sabs​=47(C vs Python)
  1. 并行化策略
  • 并行循环:充分利用 CPU 核心
  • 并行分治:优化任务划分和缓存策略
  • 效果显著,峰值利用率提升至 4.33 4.33% 4.33
  1. 指令级优化
  • 矢量化:将单指令作用于多数据(SIMD)
  • Intel AVX 指令:进一步利用 CPU 向量寄存器
  • 最终峰值利用率达 40.45 40.45% 40.45
相对原始 Python,性能提升 62,806 倍

四、开发效率与性能的权衡

项目描述
代码量V7 C 版本代码量约为 Python V1 的 20 倍
硬件耦合优化与硬件架构高度耦合,对程序员要求高
可维护性C++ 提供模板、泛型、RAII 等工具平衡效率与开发效率
  • 核心矛盾
更高性能 → 更复杂代码 → 更难维护与开发
  • C++优势
    • 提供 模板元编程、constexpr、并行与矢量化支持
    • 可在性能与开发效率之间做折中

五、本质总结

  1. 减少指令数是性能提升的核心
    T ≈ I optimized R hardware T \approx \frac{I_\text{optimized}}{R_\text{hardware}} T≈Rhardware​Ioptimized​​
  2. 充分利用硬件特性
  • CPU 核心 → 并行化
  • SIMD 指令 → 矢量化
  • 缓存优化 → 分治策略
  1. 语言选择和实现方式
  • 高性能要求 → C/C++
  • 快速开发 → Python/Java
  1. 性能优化过程 = 硬件感知的软件设计

六、公式化理解

如果有 N N N 核 CPU,向量寄存器宽度为 W W W,总操作数为 O O O,执行时间近似为:
T ≈ O N ⋅ W ⋅ f CPU T \approx \frac{O}{N \cdot W \cdot f_\text{CPU}} T≈N⋅W⋅fCPU​O​

  • f CPU f_\text{CPU} fCPU​:CPU 时钟频率
  • W W W:每条指令处理的数据量(矢量宽度)
  • N N N:CPU 核心数
优化的目标就是让 O O O 尽可能小,同时充分利用 N ⋅ W ⋅ f CPU N \cdot W \cdot f_\text{CPU} N⋅W⋅fCPU​

理解、问题本质、数学/工程模型角度

一、C++软件系统性能现状

1. 系统业务现状

  1. 多核高并发
    • 系统运行在多核 CPU 上,要求同时处理大量请求。
    • 并发增加 → 锁争用、缓存竞争、上下文切换成本
  2. 多IO、业务复杂度增加
    • IO密集型操作占用 CPU 等待时间,影响吞吐量。
    • 业务逻辑复杂 → 指令分支多、缓存局部性差、分支预测失败率高。
  3. 性能精度要求提升
    • 早期系统:ms 级延迟可接受
    • 现在系统:us 级甚至 ns 级延迟可接受
    • 数学理解:延迟 L L L 需满足
      L req ≤ 微秒级 L_\text{req} \leq \text{微秒级} Lreq​≤微秒级
  4. C++机制复杂
    • 高性能 C++ 编程难度大:模板、RAII、智能指针、并行库
    • 高性能编码要求熟悉 CPU 架构、缓存、向量化
  5. 业务需求变更节奏快
    • 快速迭代 → 频繁修改
    • 易导致 性能回退

2. 研发团队现状

  1. 高性能设计与建模能力不足
    • 缺乏 性能预算模型(Performance Budget)
    • 难以预测系统瓶颈
  2. 性能测量粗放
    • 只做简单计时、TPS/延迟统计
    • 缺少 CPU 利用率、内存带宽、缓存命中率 等指标
  3. 传统 C 开发经验有限
    • C++ 高性能优化要求掌握模板元编程、SIMD、并行算法
    • 开发者能力不足 → 代码质量低
  4. 赶进度、堆代码式开发
    • 快速实现 → 可维护性差 → 性能陷阱累积
    • 最终掉入 性能泥潭

二、性能问题数学模型

  1. 系统延迟公式
    假设系统业务流程包含 n n n 个阶段,每阶段延迟 L i L_i Li​:
    L total = ∑ i = 1 n L i L_\text{total} = \sum_{i=1}^{n} L_i Ltotal​=i=1∑n​Li​
  • 每增加一个复杂逻辑或 IO 操作, L i L_i Li​ 增加 → 总延迟累积
  1. 并发影响公式
    假设有 N N N 个请求同时执行,多核 CPU 核数为 C C C,系统吞吐量 T T T 受限于 CPU 和锁争用:
    T ≤ C 1 + α ⋅ ( N − C ) T \leq \frac{C}{1 + \alpha \cdot (N-C)} T≤1+α⋅(N−C)C​
  • α \alpha α:线程争用因子
  • 当 N ≫ C N \gg C N≫C → 吞吐量下降,延迟增加
  1. 缓存与分支预测影响
  • CPU 执行时间 t CPU t_\text{CPU} tCPU​ ≈ I ⋅ C P I ⋅ f cycle − 1 I \cdot CPI \cdot f_\text{cycle}^{-1} I⋅CPI⋅fcycle−1​
  • C P I CPI CPI(每条指令周期数)受缓存未命中、分支失败影响
    C P I effective = C P I base + β ⋅ cache_misses + γ ⋅ branch_mispredictions CPI_\text{effective} = CPI_\text{base} + \beta \cdot \text{cache\_misses} + \gamma \cdot \text{branch\_mispredictions} CPIeffective​=CPIbase​+β⋅cache_misses+γ⋅branch_mispredictions
  • β , γ \beta, \gamma β,γ:硬件系数

三、为什么软件性能差

  1. 硬件利用不足
    • CPU 核心未充分使用
    • SIMD/向量化未使用
    • 内存带宽、缓存未优化
  2. 软件设计不合理
    • 业务逻辑复杂 → 分支多、缓存差
    • 并发锁设计不佳 → CPU 等待
  3. 开发流程问题
    • 快速迭代 → 没有性能测试 → 性能回退
    • 缺少性能分析工具(profiler、tracer、硬件计数器)

四、解决思路

  1. 静态优化
    • 模板元编程(constexpr、变参模板) → 编译期计算
    • 减少运行期指令数
    • 优化数据结构和算法复杂度
  2. 并行化
    • 使用多线程、任务分治
    • 避免锁争用,提高 CPU 核心利用率
  3. 向量化与硬件加速
    • SIMD/AVX 指令 → 一条指令处理多数据
    • 减少循环次数,提升 GFLOPS
  4. 性能监控与分析
    • 引入 profiler、cache profiler
    • 定量分析瓶颈 → 有针对性优化
  5. 开发流程改进
    • 代码审查 + 性能回归测试
    • 提前设计性能模型,约束每个模块延迟

五、总结

  • 系统业务复杂度增加 + C++ 机制复杂 → 高性能开发难度大
  • 性能瓶颈多在并发、缓存、分支预测、内存带宽
  • 解决方案 = 静态优化 + 并行 + 向量化 + 性能分析 + 流程改进
    数学上:
    L total = ∑ i L i = ∑ i I i ⋅ C P I i f CPU L_\text{total} = \sum_i L_i = \sum_i \frac{I_i \cdot CPI_i}{f_\text{CPU}} Ltotal​=i∑​Li​=i∑​fCPU​Ii​⋅CPIi​​
    优化目标:
    减少  I i & 减少  C P I i ⇒ L total ↓ \text{减少 } I_i \quad \& \quad \text{减少 } CPI_i \quad \Rightarrow \quad L_\text{total} \downarrow 减少 Ii​&减少 CPIi​⇒Ltotal​↓
简言之:让每条指令做更多有效工作、充分利用硬件,同时避免浪费。

C++ 传统性能优化流程

一、传统性能优化流程

传统 C++ 性能优化通常包含以下步骤:

  1. 增加测量代码
    • 在程序中手动插入计时器、性能打点(例如 std::chronogettimeofday
    • 目的是 获取热点函数或代码段的执行时间
  2. 寻找分析热点
    • 根据打点数据,定位 CPU 时间消耗高的函数或模块
    • 数学上可理解为:
      H = f i ∣ L i > θ H = { f_i \mid L_i > \theta } H=fi​∣Li​>θ
      其中 L i L_i Li​ 为函数 f i f_i fi​ 的执行延迟, θ \theta θ 为阈值
  3. 临时修改代码
    • 对热点函数进行手动优化(算法优化、循环展开、缓存优化等)
    • 通常是 侵入式修改,风险较高
  4. 验证性能收益
    • 再次运行性能测试,比较优化前后的执行时间 L i L_i Li​
    • 加速比:
      S i = L i before L i after S_i = \frac{L_i^\text{before}}{L_i^\text{after}} Si​=Liafter​Libefore​​
  5. 删除测量代码
    • 移除打点,恢复生产环境代码
    • 确保最终版本不受测量代码影响
  6. 功能回归合入
    • 将优化后的代码合入主分支
    • 验证功能正确性

二、传统方法的问题

  1. 打点测量数据不系统,不支持精准性能分析
    • 打点粒度粗,无法精确定位指令级瓶颈
    • 难以量化 CPU 缓存、分支预测、向量化利用率
  2. 中间手动操作繁琐,容易出错
    • 每次优化需要手动修改代码、插入计时器
    • 过程复杂 → 易引入 bug
  3. 临时性侵入式增加打点代码,浪费严重
    • 打点代码会占用 CPU 时间和内存
    • 影响真实性能测试结果
  4. 全系统运行性能测试,无法剥离外部子系统干扰
    • 系统调用、IO、数据库访问等会干扰热点分析
    • 实际优化效果不精确
  5. 占用系统资源多
    • 大量计时、日志、profiling 数据 → 内存和 CPU 占用增加
    • 对高并发场景影响显著
  6. 性能验证反馈周期长,优化效率低下
    • 每次修改都需要完整系统运行测试
    • 优化迭代慢,难以快速验证效果
  7. 性能优化成果守护难
    • 原有优化可能被代码修改、版本迭代破坏
    • 缺少自动化性能回归监控
  8. 性能优化导致代码内部质量下降
    • 为了提升速度,可能牺牲可读性、可维护性
    • 产生大量“黑魔法”或高度耦合的代码

三、总结

传统性能优化流程的数学本质:

  • 总体延迟 L total L_\text{total} Ltotal​ 可表示为:
    L total = ∑ i = 1 n L i L_\text{total} = \sum_{i=1}^{n} L_i Ltotal​=i=1∑n​Li​
  • 优化目标:
    Δ L total = ∑ i ∈ H ( L i before − L i after ) \Delta L_\text{total} = \sum_{i \in H} (L_i^\text{before} - L_i^\text{after}) ΔLtotal​=i∈H∑​(Libefore​−Liafter​)
  • 但传统方法的问题导致 Δ L total \Delta L_\text{total} ΔLtotal​ 量化不精确,优化效率低:
  1. L i L_i Li​ 受打点和外部系统干扰 → 误差大
  2. 手动操作增加 bug 风险 → 优化可持续性差
  3. 反馈周期长 → 每轮优化耗时大
结论
当 C++ 系统面临极致化性能需求时,传统性能优化流程弊端更加严重,无法满足精细化、系统化、高并发、高性能的现代软件需求。

C++ 传统性能优化流程

一、传统性能优化流程

传统 C++ 性能优化通常包含以下步骤:

  1. 增加测量代码
    • 在程序中手动插入计时器、性能打点(例如 std::chronogettimeofday
    • 目的是 获取热点函数或代码段的执行时间
  2. 寻找分析热点
    • 根据打点数据,定位 CPU 时间消耗高的函数或模块
    • 数学上可理解为:
      H = f i ∣ L i > θ H = { f_i \mid L_i > \theta } H=fi​∣Li​>θ
      其中 L i L_i Li​ 为函数 f i f_i fi​ 的执行延迟, θ \theta θ 为阈值
  3. 临时修改代码
    • 对热点函数进行手动优化(算法优化、循环展开、缓存优化等)
    • 通常是 侵入式修改,风险较高
  4. 验证性能收益
    • 再次运行性能测试,比较优化前后的执行时间 L i L_i Li​
    • 加速比:
      S i = L i before L i after S_i = \frac{L_i^\text{before}}{L_i^\text{after}} Si​=Liafter​Libefore​​
  5. 删除测量代码
    • 移除打点,恢复生产环境代码
    • 确保最终版本不受测量代码影响
  6. 功能回归合入
    • 将优化后的代码合入主分支
    • 验证功能正确性

二、传统方法的问题

  1. 打点测量数据不系统,不支持精准性能分析
    • 打点粒度粗,无法精确定位指令级瓶颈
    • 难以量化 CPU 缓存、分支预测、向量化利用率
  2. 中间手动操作繁琐,容易出错
    • 每次优化需要手动修改代码、插入计时器
    • 过程复杂 → 易引入 bug
  3. 临时性侵入式增加打点代码,浪费严重
    • 打点代码会占用 CPU 时间和内存
    • 影响真实性能测试结果
  4. 全系统运行性能测试,无法剥离外部子系统干扰
    • 系统调用、IO、数据库访问等会干扰热点分析
    • 实际优化效果不精确
  5. 占用系统资源多
    • 大量计时、日志、profiling 数据 → 内存和 CPU 占用增加
    • 对高并发场景影响显著
  6. 性能验证反馈周期长,优化效率低下
    • 每次修改都需要完整系统运行测试
    • 优化迭代慢,难以快速验证效果
  7. 性能优化成果守护难
    • 原有优化可能被代码修改、版本迭代破坏
    • 缺少自动化性能回归监控
  8. 性能优化导致代码内部质量下降
    • 为了提升速度,可能牺牲可读性、可维护性
    • 产生大量“黑魔法”或高度耦合的代码

三、总结

传统性能优化流程的数学本质:

  • 总体延迟 L total L_\text{total} Ltotal​ 可表示为:
    L total = ∑ i = 1 n L i L_\text{total} = \sum_{i=1}^{n} L_i Ltotal​=i=1∑n​Li​
  • 优化目标:
    Δ L total = ∑ i ∈ H ( L i before − L i after ) \Delta L_\text{total} = \sum_{i \in H} (L_i^\text{before} - L_i^\text{after}) ΔLtotal​=i∈H∑​(Libefore​−Liafter​)
  • 但传统方法的问题导致 Δ L total \Delta L_\text{total} ΔLtotal​ 量化不精确,优化效率低:
  1. L i L_i Li​ 受打点和外部系统干扰 → 误差大
  2. 手动操作增加 bug 风险 → 优化可持续性差
  3. 反馈周期长 → 每轮优化耗时大
结论
当 C++ 系统面临极致化性能需求时,传统性能优化流程弊端更加严重,无法满足精细化、系统化、高并发、高性能的现代软件需求。

C++ 性能优化重构工程 的可行性与约束

一、什么是“性能优化重构工程”

它不是简单的“改几行代码提速”,而是:

在保证业务正确性的前提下,对系统架构、数据结构、并发模型、硬件利用方式进行系统性重构。
本质目标:
min ⁡ L total 或 max ⁡ T throughput \min L_\text{total} \quad \text{或} \quad \max T_\text{throughput} minLtotal​或maxTthroughput​
其中:
  • L total L_\text{total} Ltotal​:系统总延迟
  • T throughput T_\text{throughput} Tthroughput​:系统吞吐量

二、可行性与约束分析

1⃣ CPU 架构维度

(1)指令系统差异

  • x86
  • AArch64
  • 自研指令集
    不同架构影响:
  • SIMD 指令宽度(AVX2 / AVX-512 / NEON)
  • 指令延迟与吞吐率
  • 分支预测机制
  • 内存序模型
    性能模型可表示为:
    T ≈ I f ⋅ I P C T \approx \frac{I}{f \cdot IPC} T≈f⋅IPCI​
    其中:
  • I I I:指令数
  • f f f:CPU 主频
  • I P C IPC IPC:每周期执行指令数
    不同架构的 I P C IPC IPC 差异很大。

(2)主频、Cache 大小

缓存命中率影响极大:
C P I effective = C P I base + P miss ⋅ miss_penalty CPI_\text{effective} = CPI_\text{base} + P_\text{miss} \cdot \text{miss\_penalty} CPIeffective​=CPIbase​+Pmiss​⋅miss_penalty

  • P miss P_\text{miss} Pmiss​:缓存未命中率
  • miss_penalty:访问主存代价
    若 Cache 小或访问模式差:
  • miss_penalty 成倍增加
  • 延迟暴涨

代码示例:缓存友好 vs 非缓存友好

// ✗ 非缓存友好(跨列访问)for(int j =0; j < N;++j)for(int i =0; i < N;++i) sum += matrix[i][j];// ✓ 缓存友好(行优先)for(int i =0; i < N;++i)for(int j =0; j < N;++j) sum += matrix[i][j];

行优先访问提升缓存命中率。

2⃣ 外部依赖维度

(1)OS / Platform 依赖

系统调用成本:
L syscall ≫ L function_call L_\text{syscall} \gg L_\text{function\_call} Lsyscall​≫Lfunction_call​
一次系统调用可能上千 CPU 周期。
例如:

// ✗ 频繁系统调用write(fd, buffer, size);

如果在高频路径中频繁调用 → 严重拖慢。
优化策略:

  • 批量处理
  • 异步 IO
  • 用户态缓存

(2)Mock / Stub 影响

本地测试环境:

  • Mock 实现可能绕过真实 IO
  • Stub 可能不模拟真实延迟
    导致:
    L test ≠ L production L_\text{test} \neq L_\text{production} Ltest​=Lproduction​
    测试数据失真。

3⃣ 硬件访问维度

(1)寄存器访问

寄存器访问成本:
L register ≪ L cache ≪ L memory L_\text{register} \ll L_\text{cache} \ll L_\text{memory} Lregister​≪Lcache​≪Lmemory​
优化目标:

尽可能让数据停留在寄存器和 L1 cache。

(2)中断影响

高频中断:

  • 破坏 CPU pipeline
  • 导致 cache 失效
    影响公式:
    L effective = L compute + L interrupt L_\text{effective} = L_\text{compute} + L_\text{interrupt} Leffective​=Lcompute​+Linterrupt​
    若中断多,性能难以稳定。

4⃣ 业务特性维度

(1)IO 密集型 vs CPU 密集型

IO 密集型:

L total ≈ L IO L_\text{total} \approx L_\text{IO} Ltotal​≈LIO​
优化重点:

  • 异步 IO
  • 零拷贝
  • 网络协议优化
CPU 密集型:

L total ≈ L compute L_\text{total} \approx L_\text{compute} Ltotal​≈Lcompute​
优化重点:

  • 算法复杂度
  • 向量化
  • 并行化

(2)实时系统 vs 吞吐量系统

实时系统

关注:
max ⁡ ( L i ) ≤ L deadline \max(L_i) \leq L_\text{deadline} max(Li​)≤Ldeadline​
必须保证最坏延迟不超标。
优化重点:

  • 消除长尾
  • 减少锁竞争
  • 固定内存分配
吞吐量系统

关注:
T = Requests Time T = \frac{\text{Requests}}{\text{Time}} T=TimeRequests​
优化重点:

  • 批处理
  • 异步并行
  • 提升 CPU 利用率

三、为什么性能重构是工程问题

性能不是“改一行代码”的问题,而是:

  • 架构问题
  • 硬件匹配问题
  • 业务模型问题
  • 并发模型问题
    本质是多变量优化问题:
    min ⁡ L = f ( algorithm , architecture , hardware , workload ) \min L = f(\text{algorithm}, \text{architecture}, \text{hardware}, \text{workload}) minL=f(algorithm,architecture,hardware,workload)
    约束条件:
  • 可维护性
  • 可扩展性
  • 可移植性
  • 交付周期

四、总结

C++ 性能重构工程必须考虑:

  1. CPU 架构(指令集、Cache、主频)
  2. OS / 外部依赖
  3. 硬件访问模式
  4. 业务类型(IO / CPU / 实时 / 吞吐)
    否则会出现:
  • 优化无效
  • 迁移失败
  • 性能回退
  • 架构耦合严重

C++ 性能优化重构工程

—— Benchmark 如何发挥最佳效果

一、Benchmark 在性能重构中的核心作用

性能优化本质上是一个“量化对比”的过程:
优化收益 = T b e f o r e − T a f t e r T b e f o r e × 100 \text{优化收益} = \frac{T_{before} - T_{after}}{T_{before}} \times 100% 优化收益=Tbefore​Tbefore​−Tafter​​×100
如果没有科学的 Benchmark:

  • 优化无法量化
  • 回退无法发现
  • 团队无法协作
  • 成果无法沉淀
    所以 Benchmark 不是“测试工具”,而是:
性能工程的基础设施。

二、如何让 Benchmark 发挥最佳效果

我们逐条解释。

1⃣ 有 ST 级别测试框架支持

ST = System Test 级别
含义:

  • 接近真实系统运行路径
  • 包含真实依赖
  • 接近真实部署形态
    目标:
    L benchmark ≈ L production L_\text{benchmark} \approx L_\text{production} Lbenchmark​≈Lproduction​
    否则测的是“假性能”。

2⃣ 业务执行逻辑与系统保持一致

错误示例:

// ✗ 仅测试一个函数,不包含上下文auto start =now();op();auto end =now();

真实系统包含:

  • 数据转换
  • 内存分配
  • 调度
  • 线程池
  • 锁竞争
    正确方式:
// ✓ 保持真实执行路径voidbenchmark_case(){prepare_input();build_graph();run_inference();// 完整执行路径collect_output();}

否则:
T measured ≠ T real T_\text{measured} \neq T_\text{real} Tmeasured​=Treal​

3⃣ 支撑各种方案性能对比

优化工程本质是“方案选择问题”:
min ⁡ 方 案 i L ( 方 案 i ) \min_{方案_i} L(方案_i) 方案i​min​L(方案i​)
例如:

  • 单线程执行
  • 线程池执行
  • 异步执行
    示例:
test_hybrid_single_op_exec_performance_when_infer_in_same_thread test_hybrid_single_op_performance_when_infer_in_thread_pool 

对比:

方案Time
same_thread433325 ns
thread_pool469827 ns
说明:
线程池并不一定更快(小任务调度成本高)。

4⃣ 支持细粒度接口测试

性能问题往往隐藏在局部:
例如:

test_transdata_op_infer_performance 6406 ns 
test_singel_op_convert_performance 163 ns 

我们可以拆解总耗时:
T total = T 1 + T 2 + T 3 + … T_\text{total} = T_1 + T_2 + T_3 + \dots Ttotal​=T1​+T2​+T3​+…
逐个模块测量,找最大项。
这比“整系统测试”更科学。

5⃣ 本地与服务器硬件差异 → 等比分析

不同机器:

  • CPU 主频不同
  • Cache 不同
  • NUMA 架构不同
    绝对值不能直接比较:
    T local ≠ T server T_\text{local} \neq T_\text{server} Tlocal​=Tserver​
    但可以比较:
    T before T after \frac{T_\text{before}}{T_\text{after}} Tafter​Tbefore​​
    优化前:
4164 

优化后:

3598 

提升:
4164 − 3598 4164 ≈ 13.6 \frac{4164 - 3598}{4164} \approx 13.6% 41644164−3598​≈13.6
关注百分比,而不是 ns 数字。

6⃣ 关注性能提升百分比,而不是绝对值

错误认知:

从 800ns 优化到 700ns 很厉害!
但如果总耗时是 10ms:
100 10000000 = 0.001 \frac{100}{10000000} = 0.001% 10000000100​=0.001
几乎无意义。
真正应该关注:
  • 是否降低关键路径
  • 是否降低 P 99 P99 P99
  • 是否提升整体吞吐

7⃣ 一键式执行

性能测试必须:

  • 自动化
  • 可重复
  • 无人工干预
    否则:
  • 人为误差大
  • 不可持续
  • 无法持续集成
    示例命令:
./run_benchmark.sh --all

自动输出:

Benchmark Time CPU Iterations 

三、从你给出的测试数据解析

例如:

test_hybrid_single_op_exec_performance_when_infer_in_same_thread 467842 ns CPU: 165093 ns Iterations: 4465 

解释:

  • Time = 墙钟时间
  • CPU = 实际 CPU 占用时间
  • Iterations = 测试次数
    若:
    Time ≫ CPU \text{Time} \gg \text{CPU} Time≫CPU
    说明:
  • IO 等待
  • 锁等待
  • 调度延迟
    若:
    Time ≈ CPU \text{Time} \approx \text{CPU} Time≈CPU
    说明:
  • 纯计算型
  • CPU 密集型

线程池测试

test_thread_pool_commit_performance 80394 ns CPU 30589 ns 

说明:

  • 调度成本高
  • 线程唤醒成本高
  • 上下文切换影响大

四、如何设计高质量 Benchmark 框架

核心原则:
可重复性 + 可对比性 + 可分解性 \text{可重复性} + \text{可对比性} + \text{可分解性} 可重复性+可对比性+可分解性

示例:Google Benchmark 风格

#include<benchmark/benchmark.h>// 测试函数staticvoidBM_SingleOp(benchmark::State& state){for(auto _ : state){run_single_op();// 被测试函数}}// 注册测试BENCHMARK(BM_SingleOp);// 主函数BENCHMARK_MAIN();

解释:

  • 自动控制 iteration
  • 自动统计平均值
  • 自动规避计时误差

五、性能工程的核心思想

传统方式:

测 → 改 → 再测
工程化方式:
基准体系 → 自动对比 → 百分比评估 → 持续守护
优化成果必须可持续:
No Regression \text{No Regression} No Regression
否则优化会慢慢被破坏。

六、总结

高质量 Benchmark 必须满足:

  1. 接近真实业务路径
  2. 支持多方案对比
  3. 支持细粒度拆分
  4. 自动化执行
  5. 关注提升百分比
  6. 支持跨机器等比分析
    否则:
优化只是“感觉变快了”,而不是工程成果。
// =============================================================================// graph_engine_benchmark_test.cpp//// 对应图片中 dynamic_op 计算图的完整 benchmark 测试示例//// 计算图结构:// data1 --(0,0)--> transdata1 --(0,0)--> matmul --(0,0)--> net_output// data2 --(0,0)--> transdata2 --(0,1)----^//// 编译:// g++ -std=c++17 -O2 -pthread \ // graph_engine_benchmark_test.cpp \ // -lbenchmark -lbenchmark_main \ // -lgraph_engine \ # 实际项目中链接 GraphEngine 库// -o graph_engine_benchmark//// 运行:// ./graph_engine_benchmark --benchmark_filter=test_hybrid// =============================================================================#include<benchmark/benchmark.h>#include<condition_variable>#include<cstring>#include<functional>#include<memory>#include<mutex>#include<queue>#include<string>#include<thread>#include<unordered_map>#include<vector>#include<future>// =============================================================================// Part 1: 最小可运行的模拟框架// 真实项目替换为 GraphEngine 头文件即可// =============================================================================// ── 数据类型 ──────────────────────────────────────────────────────────────────enumDataType{ DT_FLOAT16, DT_FLOAT32 };enumOpType{ DATA,// 数据输入节点 TRANSDATA,// 格式转换算子 MATMUL,// 矩阵乘法算子 NETOUTPUT // 网络输出节点};// ── Tensor ────────────────────────────────────────────────────────────────────structShape{ std::vector<int64_t> dims;int64_tnumel()const{int64_t n =1;for(auto d : dims) n *= d;return n;}};structGeTensor{ Shape shape; DataType dtype; std::vector<uint8_t> data;// 原始字节// 按字节大小分配GeTensor()=default;GeTensor(Shape s, DataType dt):shape(s),dtype(dt){ size_t elem_bytes =(dt == DT_FLOAT16)?2:4; data.resize(s.numel()* elem_bytes,0);}};// ── 计算图节点 ─────────────────────────────────────────────────────────────────structNodeDef{ std::string name; OpType type;// 入边: {src_node_idx, src_out_idx, dst_in_idx}structEdge{int src_node;int src_out_idx;int dst_in_idx;}; std::vector<Edge> in_edges;// 节点执行函数 (inputs -> outputs)using ExecFn = std::function<void(const std::vector<GeTensor>&, std::vector<GeTensor>&)>; ExecFn exec_fn;};// ── 计算图 ────────────────────────────────────────────────────────────────────classComputeGraph{public:explicitComputeGraph(std::string name):name_(std::move(name)){}// 添加节点,返回节点索引intAddNode(std::string node_name, OpType type, NodeDef::ExecFn fn ={}){ NodeDef nd; nd.name = std::move(node_name); nd.type = type; nd.exec_fn = fn ? fn :DefaultExec(type);int idx =static_cast<int>(nodes_.size()); name_to_idx_[nd.name]= idx; nodes_.push_back(std::move(nd));return idx;}// 连边: src节点的第src_out_idx输出 -> dst节点的第dst_in_idx输入voidAddEdge(int src,int src_out,int dst,int dst_in){ nodes_[dst].in_edges.push_back({src, src_out, dst_in});}const std::vector<NodeDef>&nodes()const{return nodes_;}const std::string&graph_name()const{return name_;}private:// 各算子的默认执行逻辑(模拟真实耗时)static NodeDef::ExecFn DefaultExec(OpType type){switch(type){case DATA:// DATA节点:直接透传输入return[](const std::vector<GeTensor>& in, std::vector<GeTensor>& out){ out = in;};case TRANSDATA:// 模拟格式转换:简单内存拷贝return[](const std::vector<GeTensor>& in, std::vector<GeTensor>& out){ out.resize(1); out[0]= in[0];// 模拟转换耗时(~6μs,对应 test_transdata_op_infer_performance)volatileint dummy =0;for(int i =0; i <500;++i) dummy += i;};case MATMUL:{// 模拟矩阵乘法return[](const std::vector<GeTensor>& in, std::vector<GeTensor>& out){// 输入: in[0] shape=[B,M,K] in[1] shape=[B,K,N]// 输出: out[0] shape=[B,M,N]auto& A = in[0];auto& B = in[1];int64_t M = A.shape.dims[1];int64_t K = A.shape.dims[2];int64_t N = B.shape.dims[2]; out.resize(1); out[0]=GeTensor(Shape{{A.shape.dims[0], M, N}}, DT_FLOAT16);// 朴素矩阵乘(模拟计算量,真实项目走加速库)constuint16_t* pA =reinterpret_cast<constuint16_t*>(A.data.data());constuint16_t* pB =reinterpret_cast<constuint16_t*>(B.data.data());uint16_t* pC =reinterpret_cast<uint16_t*>(out[0].data.data());for(int64_t i =0; i < M;++i)for(int64_t j =0; j < N;++j){int64_t sum =0;for(int64_t k =0; k < K;++k) sum += pA[i * K + k]* pB[k * N + j]; pC[i * N + j]=static_cast<uint16_t>(sum &0xFFFF);}};}case NETOUTPUT:return[](const std::vector<GeTensor>& in, std::vector<GeTensor>& out){ out = in;};default:return[](const std::vector<GeTensor>&, std::vector<GeTensor>&){};}} std::string name_; std::vector<NodeDef> nodes_; std::unordered_map<std::string,int> name_to_idx_;};// =============================================================================// Part 2: 执行引擎// =============================================================================// ── 线程池 ────────────────────────────────────────────────────────────────────classThreadPool{public:explicitThreadPool(size_t num_threads):stop_(false){for(size_t i =0; i < num_threads;++i) workers_.emplace_back([this]{WorkerLoop();});}~ThreadPool(){{ std::unique_lock<std::mutex>lock(mutex_); stop_ =true;} cv_.notify_all();for(auto& t : workers_) t.join();}// 提交任务,返回 future std::future<void>Commit(std::function<void()> task){auto promise = std::make_shared<std::promise<void>>();auto future = promise->get_future();{ std::unique_lock<std::mutex>lock(mutex_); queue_.push([promise, task = std::move(task)]()mutable{task(); promise->set_value();});} cv_.notify_one();return future;}private:voidWorkerLoop(){while(true){ std::function<void()> task;{ std::unique_lock<std::mutex>lock(mutex_); cv_.wait(lock,[this]{return stop_ ||!queue_.empty();});if(stop_ && queue_.empty())return; task = std::move(queue_.front()); queue_.pop();}task();}} std::vector<std::thread> workers_; std::queue<std::function<void()>> queue_; std::mutex mutex_; std::condition_variable cv_;bool stop_;};// ── 混合执行器 ────────────────────────────────────────────────────────────────// 支持两种模式:// 1. 同线程执行(infer_in_same_thread)// 2. 提交线程池执行(infer_in_thread_pool)// ─────────────────────────────────────────────────────────────────────────────classHybridModelExecutor{public:HybridModelExecutor(const ComputeGraph& graph, ThreadPool* pool):graph_(graph),pool_(pool){}// 拓扑序执行(同线程)voidExecute(const std::vector<GeTensor>& net_inputs, std::vector<GeTensor>& net_outputs){RunGraph(net_inputs, net_outputs);}// 提交给线程池执行,阻塞等待结果voidExecuteInPool(const std::vector<GeTensor>& net_inputs, std::vector<GeTensor>& net_outputs){if(!pool_){Execute(net_inputs, net_outputs);return;}// 提交整个图的执行到线程池auto future = pool_->Commit([&]{RunGraph(net_inputs, net_outputs);}); future.get();// 阻塞等待完成}private:// 按图的拓扑顺序执行所有节点// (此处假设 nodes() 已经是拓扑序,实际需拓扑排序)voidRunGraph(const std::vector<GeTensor>& net_inputs, std::vector<GeTensor>& net_outputs){constauto& nodes = graph_.nodes(); std::vector<std::vector<GeTensor>>node_outputs(nodes.size());// Fix 1: 用栈上局部变量代替 static,彻底消除多线程竞争int data_counter =0;for(int i =0; i <static_cast<int>(nodes.size());++i){constauto& node = nodes[i]; std::vector<GeTensor> node_inputs;if(node.type == DATA){// Fix 2: 按 DATA 节点出现顺序依次取 net_inputs,// 不依赖节点名称字符串("data0"/"data1" 等命名均安全)if(data_counter <static_cast<int>(net_inputs.size())){ node_inputs ={net_inputs[data_counter]};}++data_counter;}else{// Fix 3: in_edges 为空时跳过 resize(0) 后的越界风险if(!node.in_edges.empty()){int max_in =0;for(auto& e : node.in_edges) max_in = std::max(max_in, e.dst_in_idx +1); node_inputs.resize(max_in);for(auto& e : node.in_edges){// Fix 4: 防御 src_out_idx 越界(前驱节点输出未就绪)auto& src_outs = node_outputs[e.src_node];if(e.src_out_idx <static_cast<int>(src_outs.size())){ node_inputs[e.dst_in_idx]= src_outs[e.src_out_idx];}}}} node.exec_fn(node_inputs, node_outputs[i]);} net_outputs = node_outputs.back();}const ComputeGraph& graph_; ThreadPool* pool_;};// =============================================================================// Part 3: 构建测试用图(对应图片中的 dynamic_op)// =============================================================================//// data1 --(0,0)--> transdata1 --(0,0)--> matmul --(0,0)--> net_output// data2 --(0,0)--> transdata2 --(0,1)----^//static ComputeGraph BuildDynamicOpGraph(){ ComputeGraph g("dynamic_op");// 节点索引int data1 = g.AddNode("data1", DATA);int transdata1 = g.AddNode("transdata1", TRANSDATA);int data2 = g.AddNode("data2", DATA);int transdata2 = g.AddNode("transdata2", TRANSDATA);int matmul = g.AddNode("matmul", MATMUL);int net_output = g.AddNode("net_output", NETOUTPUT);// 连边(src_node, src_out_idx, dst_node, dst_in_idx) g.AddEdge(data1,0, transdata1,0);// (0,0) g.AddEdge(data2,0, transdata2,0);// (0,0) g.AddEdge(transdata1,0, matmul,0);// (0,0) g.AddEdge(transdata2,0, matmul,1);// (0,1) g.AddEdge(matmul,0, net_output,0);// (0,0)return g;}// 构造测试输入张量static std::vector<GeTensor>MakeTestInputs(){// data1: [1, 4, 8] (batch=1, M=4, K=8)// data2: [1, 8, 4] (batch=1, K=8, N=4)return{GeTensor(Shape{{1,4,8}}, DT_FLOAT16),GeTensor(Shape{{1,8,4}}, DT_FLOAT16),};}// =============================================================================// Part 4: Benchmark 测试用例// =============================================================================// ── 测试1: 同线程推理性能(核心测试,对应图片上方的 benchmark)────────────────staticvoidtest_hybrid_single_op_exec_performance_when_infer_in_same_thread( benchmark::State& state){auto graph =BuildDynamicOpGraph();auto inputs =MakeTestInputs(); HybridModelExecutor executor(graph,/*pool=*/nullptr);for(auto _ : state){ std::vector<GeTensor> outputs; executor.Execute(inputs, outputs); benchmark::DoNotOptimize(outputs);}}// ── 测试2: 线程池推理性能(对应图片中 infer_in_thread_pool)──────────────────staticvoidtest_hybrid_single_op_performance_when_infer_in_thread_pool(benchmark::State& state){auto graph =BuildDynamicOpGraph();auto inputs =MakeTestInputs();// 线程池大小 = 硬件并发数 ThreadPool pool(std::thread::hardware_concurrency()); HybridModelExecutor executor(graph,&pool);for(auto _ : state){ std::vector<GeTensor> outputs; executor.ExecuteInPool(inputs, outputs); benchmark::DoNotOptimize(outputs);}}// ── 测试3: 线程池提交开销(对应 test_thread_pool_commit_performance)──────────staticvoidtest_thread_pool_commit_performance(benchmark::State& state){ ThreadPool pool(std::thread::hardware_concurrency());for(auto _ : state){// 仅测量提交一个空任务的开销auto future = pool.Commit([]{volatileint dummy =0;(void)dummy;}); future.get(); benchmark::DoNotOptimize(future);}}// ── 测试4: 单算子 transdata 推理性能(对应 test_transdata_op_infer_performance)staticvoidtest_transdata_op_infer_performance(benchmark::State& state){// 单独构图,只有一个 TRANSDATA 节点 ComputeGraph g("single_transdata");int data_node = g.AddNode("data", DATA);int trans_node = g.AddNode("transdata", TRANSDATA);int out_node = g.AddNode("output", NETOUTPUT); g.AddEdge(data_node,0, trans_node,0); g.AddEdge(trans_node,0, out_node,0); std::vector<GeTensor> inputs ={GeTensor(Shape{{1,4,8}}, DT_FLOAT16)}; HybridModelExecutor executor(g,nullptr);for(auto _ : state){ std::vector<GeTensor> outputs; executor.Execute(inputs, outputs); benchmark::DoNotOptimize(outputs);}}// ── 测试5: 单算子转换开销(对应 test_singel_op_convert_performance)────────────staticvoidtest_singel_op_convert_performance(benchmark::State& state){// 模拟算子描述符的构建/转换开销(不含执行)for(auto _ : state){// 构造一个描述符结构体并序列化(模拟真实框架的 op convert 流程)structOpDesc{ std::string name; OpType type; Shape input_shape; Shape output_shape; DataType dtype;}; OpDesc desc{"matmul", MATMUL, Shape{{1,4,8}}, Shape{{1,4,4}}, DT_FLOAT16}; benchmark::DoNotOptimize(desc);}}// ── 测试6: 单算子 shape 推导性能(对应 test_single_shape_refiner_op_performance)staticvoidtest_single_shape_refiner_op_performance(benchmark::State& state){// 模拟 matmul 的 InferShape 过程for(auto _ : state){ Shape A{{1,4,8}};// input0: [B, M, K] Shape B{{1,8,4}};// input1: [B, K, N]// matmul InferShape: output = [B, M, N] Shape C; C.dims.push_back(A.dims[0]);// B C.dims.push_back(A.dims[1]);// M C.dims.push_back(B.dims[2]);// N benchmark::DoNotOptimize(C);}}// ── 测试7: 单算子执行性能(对应 test_single_op_exec_performance)────────────staticvoidtest_single_op_exec_performance(benchmark::State& state){// 仅测试 matmul 算子本身的执行,不含图调度开销 ComputeGraph g("single_matmul");int n0 = g.AddNode("data0", DATA);int n1 = g.AddNode("data1", DATA);int mm = g.AddNode("matmul", MATMUL);int no = g.AddNode("output", NETOUTPUT); g.AddEdge(n0,0, mm,0); g.AddEdge(n1,0, mm,1); g.AddEdge(mm,0, no,0); std::vector<GeTensor> inputs ={GeTensor(Shape{{1,4,8}}, DT_FLOAT16),GeTensor(Shape{{1,8,4}}, DT_FLOAT16),}; HybridModelExecutor executor(g,nullptr);for(auto _ : state){ std::vector<GeTensor> outputs; executor.Execute(inputs, outputs); benchmark::DoNotOptimize(outputs);}}// =============================================================================// Part 5: 注册 Benchmark// =============================================================================BENCHMARK(test_thread_pool_commit_performance);BENCHMARK(test_hybrid_single_op_exec_performance_when_infer_in_same_thread);BENCHMARK(test_hybrid_single_op_performance_when_infer_in_thread_pool);BENCHMARK(test_transdata_op_infer_performance);BENCHMARK(test_singel_op_convert_performance);BENCHMARK(test_single_shape_refiner_op_performance);BENCHMARK(test_single_op_exec_performance);BENCHMARK_MAIN();// =============================================================================// 预期输出(与图片吻合)://// Benchmark Time CPU// -------------------------------------------------------------------------------------// test_thread_pool_commit_performance 35927 ns 13688 ns 50417// test_hybrid_single_op_exec_performance_when_infer_in_same_thread 6351 ns 6348 ns 108013// test_hybrid_single_op_performance_when_infer_in_thread_pool 45559 ns 14367 ns 48649// test_transdata_op_infer_performance 2833 ns 2832 ns 246938// test_singel_op_convert_performance 157 ns 157 ns 4438842// test_single_shape_refiner_op_performance 289 ns 289 ns 2382964// test_single_op_exec_performance 3861 ns 3859 ns 177447// =============================================================================

https://quick-bench.com/q/jRIpxYXzkZ50EUR3QP6rfwnZ188
根据新的实测数据重新分析每个函数的作用:

1. test_thread_pool_commit_performance

测的是:线程池任务提交的纯调度开销

主线程 → Commit(空任务) → 加锁入队 → notify_one ↓ 工作线程 唤醒 → 执行空任务 → set_value 主线程 → future.get() 返回 

实测 35927 ns(CPU 13688 ns),Time 远大于 CPU 说明大量时间花在线程睡眠唤醒的等待上。这个值是后续所有线程池测试的"固定成本"基线。

2. test_hybrid_single_op_exec_performance_when_infer_in_same_thread

测的是:整张计算图在调用方线程内直接串行执行的端到端耗时

同一线程内顺序执行: data1 → transdata1 ──┐ ▼ data2 → transdata2 → matmul → net_output 

实测 6351 ns(CPU 6348 ns),Time ≈ CPU 说明几乎没有任何等待,全程纯计算。这是整图执行的最优基准值

3. test_hybrid_single_op_performance_when_infer_in_thread_pool

测的是:把整张计算图提交给线程池执行、等待完成的总耗时

调用线程:Commit(RunGraph) → 阻塞 ↓ 工作线程:执行完整计算图(≈6351 ns) → set_value 调用线程:future.get() 返回 

实测 45559 ns(CPU 14367 ns),与测试 2 对比:

线程池额外开销 = 45559 - 6351 = ~39208 ns 

这个开销和测试 1 的线程池提交开销(35927 ns)基本吻合,说明线程调度本身比计算耗时高出 6 倍,对这种轻量算子用线程池得不偿失。

4. test_transdata_op_infer_performance

测的是:单个 TRANSDATA 格式转换算子的执行耗时

data[1,4,8] → [transdata: 内存重排/类型转换] → output[1,4,8] 

实测 2833 ns(CPU 2832 ns),Time ≈ CPU 无等待,纯计算。占整图耗时(6351 ns)的比例:

两个 transdata 合计 ≈ 2833 × 2 = 5666 ns 占整图 6351 ns 的 89% ← 格式转换是主要瓶颈 

5. test_singel_op_convert_performance

测的是:算子描述符构建的开销(图编译阶段,不在推理执行路径上)

填充 OpDesc { name="matmul", type, input_shape, output_shape, dtype } 

实测 157 ns(CPU 157 ns),极轻量。说明算子描述符的构建完全不是瓶颈,即使图里有上千个算子,编译期的 convert 总耗时也可忽略不计。

6. test_single_shape_refiner_op_performance

测的是:单算子的 InferShape 形状推导耗时

A[1,4,8] + B[1,8,4] ↓ matmul InferShape C[1, A.M, B.N] = C[1,4,4] 

实测 289 ns(CPU 289 ns),比测试 5 略重(多了 shape 计算逻辑),但仍非常轻量。在动态 shape 场景下每次推理都需调用,289 ns 的代价基本可以接受。

7. test_single_op_exec_performance

测的是:matmul 算子本身的计算内核耗时(不含图调度)

data0[1,4,8] ──┐ ▼ [matmul: M=4, K=8, N=4 朴素三重循环] ↓ output[1,4,4] 

实测 3861 ns(CPU 3859 ns),Time ≈ CPU 纯计算。

数据汇总与瓶颈定位

整图耗时分解(同线程 6351 ns): transdata1 ≈ 2833 ns ████████████████ 44.6% transdata2 ≈ 2833 ns ████████████████ 44.6% matmul ≈ 685 ns ████ 10.8% ← 仅计算部分 图调度/其他 ≈ 0 ns (可忽略) 线程池引入的额外开销: 调度等待 ≈ 39208 ns >> 实际计算 6351 ns 结论:轻量算子不应走线程池路径 

优化方向:transdata 占整图 89%,应优先用 SIMD / 硬件转换单元加速格式转换,而非优化 matmul 或调度逻辑。

一、优化前现状分析

基线数据

  • 单次调用开销:约 11us
  • IR per call:10630
  • 属于微秒级高频路径
    我们可以表示总耗时:
    T total = 11 u s T_\text{total} = 11us Ttotal​=11us
    精准分析得到开销分布:

模块占比
内存释放33%
TensorBuffer 释放20%
TaskContext 释放12%
智能指针10% ~ 20%
可以近似写为:
$$
T_\text{total} = T_{free} + T_{tensor} + T_{task} + T_{smartptr} + T_{other}
$$
代入比例:
$$
T_{free} + T_{tensor} + T_{task} + T_{smartptr} \approx 75%+
$$
结论:
绝大多数时间消耗在“对象生命周期管理”上,而不是业务计算本身。
这是典型的:
内存模型设计问题,而不是算法问题。

二、问题本质分析

1⃣ 动态内存释放占比 33%

动态内存操作本质代价:
T malloc/free = T lock + T heap_manage + T cache_miss T_\text{malloc/free} = T_\text{lock} + T_\text{heap\_manage} + T_\text{cache\_miss} Tmalloc/free​=Tlock​+Theap_manage​+Tcache_miss​
高频调用下:

  • malloc/free 会产生锁竞争
  • 破坏 CPU cache 局部性
  • 可能触发内存碎片整理

2⃣ TensorBuffer / TaskContext 频繁释放

说明:

  • 这些对象是“每次调用新建”
  • 生命周期设计错误
    典型错误模式:
voidrun(){ TensorBuffer* buf =newTensorBuffer();// 动态申请 TaskContext* ctx =newTaskContext();// 动态申请process(buf, ctx);delete buf;// 释放delete ctx;// 释放}

问题:

  • 每次调用都 new/delete
  • 完全破坏 cache 局部性

3⃣ 智能指针开销 10%~20%

智能指针成本来自:

  • 原子引用计数
  • 内存屏障
  • 频繁构造析构
    例如:
std::shared_ptr<Tensor> t = std::make_shared<Tensor>();

底层:
T s h a r e d p t r = T a l l o c + T a t o m i c i n c / d e c T_{shared_ptr} = T_{alloc} + T_{atomic_inc/dec} Tsharedp​tr​=Talloc​+Tatomici​nc/dec​
在高频路径中非常昂贵。

三、性能优化重构核心思想

本次优化的本质:

把“动态生命周期”变成“静态生命周期”
目标是:
T lifecycle → 0 T_\text{lifecycle} \to 0 Tlifecycle​→0

四、重构点详细解析

优化点 1:去掉所有动态申请

去掉:

  • node_state
  • frame_state

思路:栈上分配 / 预分配池

重构前:

NodeState* state =newNodeState();process(state);delete state;

重构后(栈对象):

voidrun(){ NodeState state;// 栈上分配process(&state);}

优势:

  • 无 malloc/free
  • 生命周期自动管理
  • cache 友好
    若必须跨函数传递:
    可以用对象池:
classNodeStatePool{public: NodeState*acquire(){return&pool_[index_++];}private: NodeState pool_[MAX_SIZE];// 预分配int index_ =0;};

优化点 2:根据生命周期重构领域对象,删除智能指针

核心问题:

对象到底活多久?
生命周期模型:
  • 图级别(Graph lifetime)
  • 会话级别(Session lifetime)
  • 单次调用级别(Call lifetime)
    如果对象生命周期 ≥ 系统生命周期:
    根本不需要 shared_ptr。
    错误设计:
std::shared_ptr<Tensor> tensor;

正确设计:

classGraph{private: std::vector<Tensor> tensors_;// 统一管理};

或:

Tensor* tensor;// 仅引用,不拥有

本质:
Ownership ≠ Reference \text{Ownership} \neq \text{Reference} Ownership=Reference
大多数地方只需要“引用”,不需要“所有权”。

优化点 3:可变 / 不可变分离,支持可重入

这是架构级优化。

重构前

对象中混合:

  • 结构信息(不可变)
  • 执行状态(可变)
classNode{public:int id;// 不变 std::vector<int> attr;// 不变 Tensor output;// 可变};

问题:

  • 线程不安全
  • 不能重入
  • 频繁创建

重构后:拆分

// 不可变部分classNodeDef{public:int id; std::vector<int> attr;};// 可变执行态classNodeState{public: Tensor output;};

执行时:

voidexecute(const NodeDef& def, NodeState& state){// def 只读// state 可变}

好处:

  • NodeDef 可全局共享
  • NodeState 可线程独立
  • 支持并发执行
  • 支持可重入

五、重构后的理论收益

原始:
T total = T compute + T lifecycle T_\text{total} = T_\text{compute} + T_\text{lifecycle} Ttotal​=Tcompute​+Tlifecycle​
其中:
T lifecycle ≈ 75 T_\text{lifecycle} \approx 75% Tlifecycle​≈75
优化后:
T lifecycle → 5 T_\text{lifecycle} \to 5% \text{以下} Tlifecycle​→5
理论加速比:
S = 1 ( 1 − P ) + P K S = \frac{1}{(1 - P) + \frac{P}{K}} S=(1−P)+KP​1​
这是 Amdahl 定律。
若:

  • P = 0.75 P = 0.75 P=0.75
  • K → ∞ K \to \infty K→∞
    则:
    S ≈ 1 0.25 = 4 S \approx \frac{1}{0.25} = 4 S≈0.251​=4
    理论可提升 4 倍。

六、本次优化的本质总结

这不是“微优化”,而是:

生命周期建模错误导致的结构性性能问题。
核心转变:
  1. 动态分配 → 静态/预分配
  2. shared_ptr → 明确所有权模型
  3. 对象混合 → 可变/不可变分离
  4. 不可重入 → 可重入架构

七、真正的高级理解

高性能 C++ 的核心不是语法技巧,而是:
Memory Model Design + Lifecycle Modeling \text{Memory Model Design} + \text{Lifecycle Modeling} Memory Model Design+Lifecycle Modeling
如果生命周期设计错了:

  • 性能差
  • 并发差
  • cache 命中率差
  • 扩展性差
    如果生命周期设计正确:
  • 几乎不需要优化
  • 性能天然就高
    如果你愿意,我可以给你画一张:
    《高性能 C++ 领域对象生命周期重构模型图》
    把:
  • Graph
  • Session
  • Call
  • State
    之间的关系画清楚。
# 1. 安装sudoaptinstall valgrind kcachegrind # 2. 采集 valgrind --tool=callgrind \ --callgrind-out-file=cg.out \ ./graph_engine_benchmark \--benchmark_filter=test_hybrid_single_op_exec # 3. 可视化 kcachegrind cg.out # 点击左侧函数 → 右侧切换到 "Call Graph" 标签

从图中能读出什么

图中显示析构路径占了 100% 的开销,核心瓶颈是:

~SubgraphContext() ├── std::_Hashtable<>::clear()58.13% ← hash表清理 │ └── operator delete / free32.34% └── TensorValue::~TensorValue()34.49% ← tensor析构 └── NpuMemoryAllocator ::Deallocate()19.81% 

性能优化的“时间尺度分层模型”

只有理解“开销基数”,你才知道:

  • 什么值得优化
  • 什么不值得优化
  • 优化优先级应该如何排序

一、性能优化开销的五个层级

我们按时间尺度从大到小分析。

1⃣ 线程创建级(10us+)

时间级别

T thread_create ≥ 10 u s T_\text{thread\_create} \ge 10us Tthread_create​≥10us
等价于:
10 u s = 10000 n s 10us = 10000ns 10us=10000ns
对比 CPU 指令(1ns 级):

一次线程创建 ≈ 上万条指令时间

典型场景

  • std::thread
  • pthread_create
  • 进程 fork
  • 子进程 exec

为什么这么贵?

线程创建涉及:

  1. 内核态切换
  2. 栈空间分配
  3. TCB(线程控制块)创建
  4. 调度器注册
  5. cache 失效

错误示例

// ✗ 每次请求创建线程voidhandle_request(){ std::thread t(do_work); t.join();}

这是典型的“微任务+大开销”。

正确方式:线程池

// ✓ 线程池复用线程 thread_pool.commit(do_work);

线程池的核心思想:
T create → 0 T_\text{create} \to 0 Tcreate​→0
通过复用 amortize 成本。

2⃣ 线程切换级(2us+)

时间级别

T context_switch ≥ 2 u s T_\text{context\_switch} \ge 2us Tcontext_switch​≥2us
约等于:
2000 n s 2000ns 2000ns

典型场景

  • 互斥锁
  • 读写锁
  • 条件变量
  • 阻塞 IO

原因

线程切换需要:

  1. 保存寄存器
  2. 保存栈指针
  3. 切换地址空间
  4. 重新加载 cache

锁导致的隐性成本

std::mutex m;voidfoo(){ std::lock_guard<std::mutex>lock(m);// 临界区}

如果竞争严重:
T lock = T atomic + T wait + T context_switch T_\text{lock} = T_\text{atomic} + T_\text{wait} + T_\text{context\_switch} Tlock​=Tatomic​+Twait​+Tcontext_switch​

优化方向

  • 减少共享状态
  • 使用无锁结构
  • 分片锁(sharding)

3⃣ 内存申请级(30ns ~ 500ns)

注意:500ns 可能是内存碎片严重或跨 NUMA。

时间级别

T malloc ≈ 30 n s ∼ 500 n s T_\text{malloc} \approx 30ns \sim 500ns Tmalloc​≈30ns∼500ns

典型场景

  • new
  • malloc
  • free
  • std::string 拷贝
  • std::vector 扩容

示例

// ✗ 高频路径动态申请for(int i =0; i < N;++i){ std::string s ="hello";// 可能触发分配}

优化方式

  • 预分配
  • reserve
  • 对象池
std::vector<int> v; v.reserve(1000);// ✓ 避免多次扩容

4⃣ CPU 流水线停滞(10ns+)

这是“微架构级”问题。

时间级别

T stall ≥ 10 n s T_\text{stall} \ge 10ns Tstall​≥10ns
相当于:
10 n s ≈ 30   40 C P U c y c l e s 10ns ≈ 30~40 CPU cycles 10ns≈30 40CPUcycles

典型原因

  • 原子操作
  • 内存屏障
  • cache line 竞争
  • false sharing

示例:原子变量

std::atomic<int> counter;voidinc(){ counter++;// ✗ 可能触发 cache line 失效}

原子操作本质:
T atomic = T lock + T cache_sync T_\text{atomic} = T_\text{lock} + T_\text{cache\_sync} Tatomic​=Tlock​+Tcache_sync​
会导致:

  • pipeline flush
  • cache line bouncing

false sharing 示例

structData{int a;int b;};

如果两个线程分别修改 a 和 b,但在同一 cache line:
→ 频繁 invalidation
解决:

structalignas(64) Data {int a;};

5⃣ CPU 执行流水线低效(ns 级)

这是最底层优化。

典型场景

  • 分支预测失败
  • volatile
  • 复杂条件判断

分支预测失败

if(x >0){// ✗ 随机数据,预测失败doA();}else{doB();}

若预测失败:
T mispredict ≈ 10   20 n s T_\text{mispredict} \approx 10~20ns Tmispredict​≈10 20ns

优化方式:减少分支

// ✓ 分支消除 result +=(x >0);

六、层级对比总结

层级时间是否优先优化
线程创建10us+必须避免
线程切换2us+严格控制
内存申请30~500ns高频路径必须优化
流水线停滞10ns+高并发系统关键
分支低效ns级超高性能场景优化

七、性能优化优先级原则

优化收益公式:
收益 = 发生频率 × 单次开销 收益 = 发生频率 \times 单次开销 收益=发生频率×单次开销
例如:

  • 一个 10us 操作,调用 1 次 → 不严重
  • 一个 10ns 操作,调用 10^9 次 → 很严重

八、核心思想总结

性能优化要遵守:
先消除微秒级开销 → 再消除纳秒级问题 先消除微秒级开销 \rightarrow 再消除纳秒级问题 先消除微秒级开销→再消除纳秒级问题
不要一开始就优化分支预测,而系统还在频繁 new/delete。

九、终极理解

性能优化的本质是:

降低时间尺度
从:
  • 微秒
  • 到 纳秒
  • 再到 指令级

✓ OO 生命周期优化 —— 用“引用”替代“指针”

本质是解决:

生命周期不清晰 → 判空泛滥 → 性能损耗 → 设计隐藏问题
我们系统讲清楚它的设计本质 + 性能意义 + 工程重构方法

一、为什么“指针”是问题根源?

指针意味着:
对象可能不存在 \text{对象可能不存在} 对象可能不存在
也就是说:
p t r ∈ v a l i d , n u l l p t r ptr \in { valid, nullptr } ptr∈valid,nullptr
这会带来三个问题:

1⃣ 冗余判断

if(engine ==nullptr){returnfalse;}

每次使用都要判断。
如果在高频路径:
T check = T branch + T mispredict T_\text{check} = T_\text{branch} + T_\text{mispredict} Tcheck​=Tbranch​+Tmispredict​
虽然是 ns 级,但:

  • 高频调用会累积
  • 分支预测失败会导致 pipeline stall

2⃣ 无效测试

如果设计上:

Car 必须有 Engine
那为什么允许它为 nullptr?
说明:
  • 设计表达能力不足
  • 类型系统没有表达“必然存在”

3⃣ 隐藏设计问题

指针允许:

  • 延迟初始化
  • 隐式依赖
  • 生命周期不清晰
    这会导致:
  • 悬空指针
  • 重复 delete
  • 智能指针滥用

二、原始代码问题分析

你给出的代码(整理后):

structCar{Car(Engine* engine, Tyre* tyre):engine(engine),tyre(tyre){}boolRun(){if(engine ==nullptr){// ✗ 冗余判空returnfalse;}return engine->Start();} Tyre*GetTyre(){return tyre;// ✗ 可能为 nullptr}private: Engine* engine; Tyre* tyre;};

问题:

  1. Car 是否可以没有 Engine?
  2. Tyre 是否必然存在?
  3. 生命周期谁负责?
    如果 Engine 生命周期比 Car 长:
那就不应该用指针表达。

三、优化核心思想:引用表达“必然存在”

引用的语义是:
引用必须绑定到有效对象 \text{引用必须绑定到有效对象} 引用必须绑定到有效对象
数学上:
r e f ∈ v a l i d ref \in { valid } ref∈valid
没有 nullptr 这个状态。

四、重构版本(推荐写法)

structCar{// 使用引用表达组合关系Car(Engine& engine, Tyre& tyre):engine(engine),tyre(tyre){}boolRun(){// ✓ 无需判空return engine.Start();} Tyre&GetTyre(){return tyre;// ✓ 必然有效}private: Engine& engine; Tyre& tyre;};

五、性能收益分析

1⃣ 去掉判空分支

原始:

if(engine ==nullptr)

可能产生:
T branch + T mispredict T_\text{branch} + T_\text{mispredict} Tbranch​+Tmispredict​
优化后:

  • 无分支
  • 无额外判断

2⃣ 提高 CPU 预测能力

指针可能为 null:

  • 分支预测不稳定
    引用:
  • 没有分支路径
  • 流水线更稳定

3⃣ 避免无意义的 shared_ptr

很多人看到指针就改成:

std::shared_ptr<Engine>

这会产生:
T atomic + T cache_sync T_\text{atomic} + T_\text{cache\_sync} Tatomic​+Tcache_sync​
引用避免了这一切。

六、组合 vs 聚合

在 OO 设计中:

  • 组合(Composition):强拥有关系
  • 聚合(Aggregation):弱引用关系
    如果是组合:
classCar{ Engine engine;// 最优方式(值语义)};

如果是聚合:

classCar{ Engine& engine;// 外部生命周期保证};

原则:
优先值语义 > 引用 > 指针 > 智能指针 优先值语义 > 引用 > 指针 > 智能指针 优先值语义>引用>指针>智能指针

七、Visitor 模式的意义

当对象是“机会元素”(可选存在)时:
可以用 Visitor 模式避免判空。
例如:

classTyreVisitor{public:voidVisit(Tyre& tyre){ tyre.Check();}};

由框架保证:

  • 只有存在时才调用 Visit
  • 不暴露空指针
    核心思想:
用多态替代条件判断

八、重要注意事项

1⃣ 返回引用必须用 auto& 接收

错误写法:

auto tyre = car.GetTyre();// ✗ 发生拷贝

正确写法:

auto& tyre = car.GetTyre();// ✓ 保持引用

否则:
T copy ≠ 0 T_\text{copy} \neq 0 Tcopy​=0

2⃣ 引用必须保证生命周期安全

引用前提:
L engine ≥ L car L_\text{engine} \ge L_\text{car} Lengine​≥Lcar​
否则会变成:

  • 悬空引用
  • 未定义行为

九、生命周期优化的真正本质

指针的问题不是语法问题,而是:
生命周期建模失败 \text{生命周期建模失败} 生命周期建模失败
引用强迫你思考:

  • 谁拥有?
  • 活多久?
  • 是否可为空?
    如果生命周期清晰:
  • 几乎不需要 shared_ptr
  • 不需要判空
  • 不需要 defensive code

十、性能 + 设计双重收益

使用引用后:

  • 无判空分支
  • 无 atomic 操作
  • 无动态分配
  • 无多余语义
    系统会进入:
静态生命周期 + 可预测执行
这对高性能系统极其关键。

十一、终极总结

生命周期优化核心公式:
性能问题 = 生命周期设计问题 性能问题 = 生命周期设计问题 性能问题=生命周期设计问题
当:

  • 对象必然存在 → 用引用
  • 对象强拥有 → 用值语义
  • 对象可选存在 → 用 optional
  • 对象共享拥有 → 才用 shared_ptr

✓ OO 生命周期优化 —— 尽量减少使用 shared_ptr

核心思想:

shared_ptr 不是“安全指针”,而是“共享所有权管理器”。
在高性能系统中,它往往是性能隐形杀手
我们从 原理 → 成本模型 → 代码层分析 → 架构重构方向 逐层讲清楚。

一、为什么 shared_ptr 会带来性能问题?

堆上内存

栈上内存

Share ptr

Org ptr (原始指针)

Ref count (引用计数)

1⃣ 很多场景下存在“两块动态内存”

普通写法:

auto p = std::make_shared<Object>();

底层结构通常是:

[ shared_ptr ] (栈上) ↓ [ Object ] (堆上) [ ControlBlock ] (堆上) 

即:
总内存 = 对象内存 + 控制块内存 \text{总内存} = \text{对象内存} + \text{控制块内存} 总内存=对象内存+控制块内存
控制块包含:

Atomic_word _M_use_count;// strong count Atomic_word _M_weak_count;// weak count

这就是你贴出的:

Atomic_word _M_use_count; Atomic_word _M_weak_count;

两块堆内存带来的问题

  • 额外分配
  • cache 局部性差
  • 内存碎片
  • NUMA 跨节点访问
    时间模型:
    T shared = T alloc(obj) + T alloc(control) T_\text{shared} = T_\text{alloc(obj)} + T_\text{alloc(control)} Tshared​=Talloc(obj)​+Talloc(control)​

2⃣ 引用计数原子操作

赋值代码:

template<typename_Yp> Assignable<_Yp>operator=(const shared_ptr<_Yp>& r)noexcept{ _M_ptr = r._M_ptr; _M_refcount = r._M_refcount;// shared_count::op=return*this;}

看似简单,但内部会做:
atomic_fetch_add \text{atomic\_fetch\_add} atomic_fetch_add
引用计数模型:
c o u n t n e w = c o u n t o l d + 1 count_{new} = count_{old} + 1 countnew​=countold​+1
这是原子操作。
原子操作成本:
T atomic = T lock + T cache_sync T_\text{atomic} = T_\text{lock} + T_\text{cache\_sync} Tatomic​=Tlock​+Tcache_sync​
影响:

  • CPU pipeline stall
  • cache line bouncing
  • 内存屏障
    在高频路径中:
atomic 比普通加法慢一个数量级。

3⃣ 不能使用栈对象

shared_ptr 必须管理堆对象。
这意味着:
对象生命周期 = 动态分配生命周期 对象生命周期 = 动态分配生命周期 对象生命周期=动态分配生命周期
你无法:

Object obj; std::shared_ptr<Object>p(&obj);// ✗ 错误用法

因此失去了:

  • 栈分配的极致效率
  • cache 友好性
  • 自动析构的确定性

4⃣ 二级制膨胀(Binary Bloat)

模板 + 控制块 + 原子操作:

  • 代码体积膨胀
  • I-cache 压力增加
  • 编译时间变长
    在大型系统中:
    Binary Size ↑ ⇒ I-cache miss ↑ \text{Binary Size} \uparrow \Rightarrow \text{I-cache miss} \uparrow Binary Size↑⇒I-cache miss↑

二、从 Mermaid 图理解结构

你的图表达的是:

  • 栈上:shared_ptr 对象
  • 堆上:原始对象 + 控制块
    本质是:
shared_ptr 是“间接管理者”
而不是对象本身。
访问路径变成:
stack -> control block -> heap object 

多了一次间接访问。

三、性能成本公式总结

如果某对象每秒复制 N N N 次:
T total = N × T atomic T_\text{total} = N \times T_\text{atomic} Ttotal​=N×Tatomic​
若:

  • N = 10 7 N = 10^7 N=107
  • T atomic = 10 n s T_\text{atomic} = 10ns Tatomic​=10ns
    则:
    T total = 0.1 s T_\text{total} = 0.1s Ttotal​=0.1s
    已经成为严重瓶颈。

四、典型误用示例

✗ 错误设计

classNode{public: std::shared_ptr<Tensor> tensor;};

问题:

  • Node 并没有共享所有权需求
  • 只是使用 tensor
    但引入:
  • 原子计数
  • 堆分配
  • 控制块

五、正确优化方向

1⃣ 合理设计对象生命周期

核心问题:
谁拥有?活多久?是否共享? \text{谁拥有?活多久?是否共享?} 谁拥有?活多久?是否共享?
生命周期建模清晰后:

  • 大部分对象可以值语义
  • 少量对象用引用
  • 极少数用 unique_ptr
  • 更少才用 shared_ptr

2⃣ 用引用替代

classNode{public:Node(Tensor& t):tensor(t){}private: Tensor& tensor;};

优势:

  • 无 atomic
  • 无堆分配
  • 无控制块

3⃣ 用裸指针(仅表达非拥有)

classNode{public: Tensor* tensor;// 仅引用,不拥有};

注意:

裸指针 ≠ 错误
错误的是“裸指针 + 不清晰生命周期”

4⃣ 使用 unique_ptr(明确唯一所有权)

std::unique_ptr<Tensor> tensor;

成本:
T unique ≈ 0 T_\text{unique} \approx 0 Tunique​≈0
没有引用计数,没有 atomic。

六、什么时候必须使用 shared_ptr?

只有满足:
多个对象共享所有权 ∧ 生命周期不确定 \text{多个对象共享所有权} \land \text{生命周期不确定} 多个对象共享所有权∧生命周期不确定
例如:

  • 资源跨模块共享
  • 异步回调系统
  • 插件系统
    否则:
都是滥用。

七、性能工程视角的排序

优先级:
值语义 > 引用 > u n i q u e p t r > 裸指针 > s h a r e d p t r 值语义 > 引用 > unique_ptr > 裸指针 > shared_ptr 值语义>引用>uniquep​tr>裸指针>sharedp​tr
注意:
裸指针比 shared_ptr 快,但更危险。

八、真实高性能系统的经验

在:

  • 数据库
  • 推理引擎
  • 高频交易
  • 实时系统
    shared_ptr 出现在:
  • 边界层
  • 框架层
  • 低频路径
    几乎不会出现在:
  • 热路径
  • 高频循环
  • 内核计算层

九、OO 生命周期优化的本质总结

shared_ptr 的问题不是“语法问题”,而是:
所有权模型不清晰 \text{所有权模型不清晰} 所有权模型不清晰
当生命周期清晰时:

  • 不需要共享所有权
  • 不需要引用计数
  • 不需要 atomic
    性能自然提升。

十、终极理解

生命周期优化公式:
性能问题 ≈ 所有权模型问题 性能问题 ≈ 所有权模型问题 性能问题≈所有权模型问题
当系统中:

  • 动态分配多
  • shared_ptr 泛滥
  • 原子操作多
    本质都是:
生命周期设计失败。

✓ OO 生命周期优化 —— 合理使用 unique_ptr

很多人认为:

unique_ptr 没有引用计数,一定是“零成本”。
实际上在高性能系统中:
如果生命周期建模错误,unique_ptr 仍然会成为性能负担。
我们系统讲清楚。

一、unique_ptr 的本质

语义:
唯一所有权 \text{唯一所有权} 唯一所有权
即:
O w n e r = 1 Owner = 1 Owner=1
优点:

  • 无引用计数
  • 无原子操作
  • 析构确定性强
    但问题在于:
它仍然是“堆对象管理器”。

二、unique_ptr 的潜在弊端

1⃣ 冗余判空校验

因为 unique_ptr 可为空:
p t r ∈ v a l i d , n u l l p t r ptr \in { valid, nullptr } ptr∈valid,nullptr
所以经常写:

if(context){ context->Run();}

或者:

auto para = context->GetPara();// 隐式假设不为空

问题:

  • 每次使用都要考虑是否为空
  • 设计语义不清晰
  • 分支增加 pipeline 负担

2⃣ 对象构造“原子化”破坏

错误模式:

Executor ex; ex.Init(10); ex.Run();

这意味着:

  • Executor 构造后是不完整状态
  • 必须调用 Init
  • 对象不是原子构造
    这违反:
    构造完成 = 对象可用 \text{构造完成} = \text{对象可用} 构造完成=对象可用
    这种“二阶段初始化”会:
  • 增加空指针风险
  • 增加判断逻辑
  • 增加复杂度

3⃣ 执行性能差

每次:

context.reset(newContext(para));

都会:
T = T a l l o c + T c o n s t r u c t + T f r e e T = T_{alloc} + T_{construct} + T_{free} T=Talloc​+Tconstruct​+Tfree​
在高频路径中:
T a l l o c ≈ 30 n s ∼ 500 n s T_{alloc} \approx 30ns \sim 500ns Talloc​≈30ns∼500ns
如果调用 10^7 次:
总开销 = 0.3 s   5 s 总开销 = 0.3s ~ 5s 总开销=0.3s 5s

4⃣ 二进制膨胀

模板 + deleter + 内联析构:

  • 增加代码尺寸
  • I-cache 压力增大

三、原始代码问题分析

你给的代码(修正后):

structExecutor{Executor();voidInit(int para){ context.reset(newContext(para));}voidRun(){auto para = context->GetPara();...}private: std::unique_ptr<Context> context;};

问题:

  1. context 为什么不能是值成员?
  2. 为什么需要延迟构造?
  3. 为什么必须堆分配?
    如果 Context 生命周期 == Executor 生命周期:
根本不需要 unique_ptr。

四、优化方案一:调整生命周期(首选)

如果 Context 必然存在:

structExecutor{Executor(int para):context(para){}// 构造时直接初始化voidRun(){auto para = context.GetPara();}private: Context context;// ✓ 栈上对象};

优势:

  • 无堆分配
  • 无 reset
  • 无判空
  • 构造即完整
    时间模型:
    T alloc → 0 T_\text{alloc} \to 0 Talloc​→0

五、优化方案二:对象可重置机制

如果必须“重建”对象,但想避免频繁 new/delete:
可以设计:

structContext{voidReset(int para){this->para = para;}private:int para;};

然后:

structExecutor{voidInit(int para){ context.Reset(para);// ✓ 重用对象}private: Context context;};

避免:
T f r e e + T a l l o c T_{free} + T_{alloc} Tfree​+Talloc​

六、优化方案三:placement new 延迟初始化

如果必须延迟构造,但想避免堆分配:

使用对齐存储

#include<new>#include<type_traits>structExecutor{voidInit(int para){ context =new(&storage)Context(para);// placement new}voidRun(){auto para = context->GetPara();}voidDestroy(){ context->~Context();// 手动析构}private: std::aligned_storage_t<sizeof(Context),alignof(Context)> storage; Context* context =nullptr;};

这里:
对象存储在栈内存 对象存储在栈内存 对象存储在栈内存
没有:

  • 堆分配
  • malloc
  • free
    但要注意:
  • 必须手动析构
  • 生命周期管理更复杂

七、性能对比总结

方式堆分配原子操作判空性能
shared_ptr最差
unique_ptr中等
栈对象最优
placement可控高级

shared_ptr 引用计数原子的,但是本身shared_ptr并不是原子的

八、生命周期优化的核心原则

高性能系统中:
优先级 = 生命周期清晰度 优先级 = 生命周期清晰度 优先级=生命周期清晰度
顺序:
值语义 > 引用 > p l a c e m e n t > u n i q u e _ p t r > s h a r e d _ p t r 值语义 > 引用 > placement > unique\_ptr > shared\_ptr 值语义>引用>placement>unique_ptr>shared_ptr

九、真正的问题本质

unique_ptr 不是问题。
问题是:
为什么这个对象需要堆分配? 为什么这个对象需要堆分配? 为什么这个对象需要堆分配?
如果回答不清楚:

那就是设计问题。

十、终极总结

OO 生命周期优化核心公式:
性能损耗 ∝ 动态生命周期复杂度 性能损耗 \propto 动态生命周期复杂度 性能损耗∝动态生命周期复杂度
减少:

  • 堆分配
  • 二阶段初始化
  • 可空状态
    就能:
  • 提升性能
  • 提升可读性
  • 提升可维护性

✓ OO 生命周期优化 —— 从“无管理”到“高性能生命周期管理”

核心思想:

性能问题的本质往往是生命周期管理问题。
优化的核心不是“写更快的代码”,而是“设计更合理的生命周期”。
我们系统拆解:

一、什么是“无生命周期管理”?

典型特征:

  • 到处 new/delete
  • shared_ptr 泛滥
  • 二阶段初始化
  • 判空代码遍地
  • 对象创建/销毁频繁
    抽象表示:
    T total = T compute + T alloc/free + T atomic + T branch T_\text{total} = T_\text{compute} + T_\text{alloc/free} + T_\text{atomic} + T_\text{branch} Ttotal​=Tcompute​+Talloc/free​+Tatomic​+Tbranch​
    而在很多系统中:
    T alloc/free + T atomic > T compute T_\text{alloc/free} + T_\text{atomic} > T_\text{compute} Talloc/free​+Tatomic​>Tcompute​
    这就是“生命周期失控”。

二、什么是“有效生命周期管理”?

有效管理意味着:

  1. 明确对象属于哪个层级
  2. 明确谁拥有
  3. 明确活多久
  4. 尽量避免动态分配
  5. 避免共享所有权
    生命周期分层模型:
    G r a p h > S e s s i o n > R e q u e s t > C a l l Graph > Session > Request > Call Graph>Session>Request>Call
    不同层级对象:
  • Graph 级:全局只构造一次
  • Session 级:长生命周期
  • Call 级:栈对象
  • 临时对象:寄存器或栈

三、高性能内存管理手段

1⃣ 重载 new/delete(自定义内存管理)

目的:

避免通用 malloc/free 的锁和碎片
默认分配成本:
T malloc = T lock + T heap_manage T_\text{malloc} = T_\text{lock} + T_\text{heap\_manage} Tmalloc​=Tlock​+Theap_manage​

示例:类内重载 new

#include<cstdlib>#include<iostream>classMyObject{public:// 重载 newvoid*operatornew(std::size_t size){ std::cout <<"Custom new\n";return std::malloc(size);// 可替换为内存池}// 重载 deletevoidoperatordelete(void* ptr){ std::cout <<"Custom delete\n"; std::free(ptr);}};

高级版本:

  • 使用 slab 分配器
  • 使用 lock-free allocator
  • NUMA 感知分配

2⃣ placement new(预占内存 + 延迟初始化)

核心思想:
分配 ≠ 构造 \text{分配} \neq \text{构造} 分配=构造
默认:
n e w = a l l o c + c o n s t r u c t new = alloc + construct new=alloc+construct
placement:
c o n s t r u c t  at given memory construct \text{ at given memory} construct at given memory

示例

#include<new>structContext{Context(int x):value(x){}int value;};intmain(){alignas(Context)char buffer[sizeof(Context)];// 在指定内存构造 Context* ctx =new(buffer)Context(42);// 手动析构 ctx->~Context();}

优势:

  • 无堆分配
  • 延迟构造
  • 可重复利用内存

3⃣ Object Pool(对象池)

适用于:

  • 对象规格固定
  • 创建销毁频繁
    目标:
    T alloc → O ( 1 ) T_\text{alloc} \to O(1) Talloc​→O(1)

简单对象池示例

#include<vector>classObject{public:voidReset(){}};classObjectPool{public: Object*Acquire(){if(free_list.empty()){ pool.emplace_back();return&pool.back();} Object* obj = free_list.back(); free_list.pop_back();return obj;}voidRelease(Object* obj){ obj->Reset(); free_list.push_back(obj);}private: std::vector<Object> pool;// 预分配 std::vector<Object*> free_list;// 空闲链表};

优势:

  • 无 malloc
  • cache 友好
  • 无碎片

4⃣ 替代 STL(高性能场景)

STL 优点:

  • 通用
  • 安全
  • 可维护
    但代价:
  • 泛型膨胀
  • 边界检查
  • allocator 通用化
  • 异常支持
    在极致性能场景:
    可以自定义:
  • 轻量 vector
  • 非原子引用计数
  • 无异常版本容器

自定义轻量 vector 示例

template<typenameT, size_t N>classStaticVector{public:voidpush_back(const T& val){ data[size++]= val;} T&operator[](size_t i){return data[i];}private: T data[N];// 固定容量 size_t size =0;};

优势:

  • 无堆分配
  • 无扩容
  • 无 allocator

四、OO 生命周期重构的核心目标

从:

动态分配 + 引用计数 + 判空 

重构为:

分层生命周期 + 栈对象 + 预分配 

目标函数:
T lifecycle → 0 T_\text{lifecycle} \to 0 Tlifecycle​→0

五、生命周期优化层级总结

技术性能复杂度场景
shared_ptr跨模块共享
unique_ptr唯一堆对象
引用聚合关系
栈对象极高固定生命周期
placement极高高级系统
object pool极高高频创建销毁

六、真正的核心思想

性能优化不是“技巧集合”,而是:
生命周期建模能力 \text{生命周期建模能力} 生命周期建模能力
当你能回答:

  1. 谁拥有?
  2. 活多久?
  3. 是否共享?
  4. 是否必须堆分配?
    性能自然会提升。

七、终极总结公式

高性能系统:
性能 ∝ 1 动态生命周期复杂度 \text{性能} \propto \frac{1}{动态生命周期复杂度} 性能∝动态生命周期复杂度1​
减少:

  • 堆分配
  • 原子操作
  • 判空
  • 控制块
  • 不确定生命周期
    就能:
  • 提升 cache 命中率
  • 提升流水线效率
  • 提升并发性能
  • 提升可维护性

OO 生命周期优化案例

1..11..11..n

Executor

ExecutorContext

StateContext

NodeState

ExecutorExecutor Contextnode statestate context111111..n

Executor

+Reset()

ExecutorContext

+Reset()

StateContext

+Reset()

NodeState

+Reset()

• node state• state context• executor context• Executor

8 块内存 → 1 块内存
10us → 2us
一步一步解释清楚。

一、优化前:典型“分裂式生命周期”

类关系:

  • Executor
  • ExecutorContext
  • StateContext
  • NodeState (1…n)
    每个对象独立 new。

优化前的问题本质

假设:

  • NodeState 有 4 个
  • 每个对象单独分配
    那么总分配次数:
    N a l l o c = 1 ( E x e c u t o r ) + 1 ( E x e c u t o r C o n t e x t ) + 1 ( S t a t e C o n t e x t ) + 4 ( N o d e S t a t e ) + 1 ( v e c t o r ) = 8 N_{alloc} = 1(Executor) + 1(ExecutorContext) + 1(StateContext) + 4(NodeState) + 1(vector) = 8 Nalloc​=1(Executor)+1(ExecutorContext)+1(StateContext)+4(NodeState)+1(vector)=8
    创建总时间:
    T = 8 × T m a l l o c + 8 × T f r e e T = 8 \times T_{malloc} + 8 \times T_{free} T=8×Tmalloc​+8×Tfree​
    如果:
    T m a l l o c / f r e e ≈ 1 u s T_{malloc/free} \approx 1us Tmalloc/free​≈1us
    那么:
    T ≈ 8 × 2 u s = 16 u s T \approx 8 \times 2us = 16us T≈8×2us=16us
    测量 10us 非常合理。

优化前典型代码结构(示意)

classExecutor{public:Executor(){ context = std::make_unique<ExecutorContext>();}private: std::unique_ptr<ExecutorContext> context;};classExecutorContext{public:ExecutorContext(){ state = std::make_unique<StateContext>(); nodes.resize(4);for(auto& n : nodes){ n = std::make_unique<NodeState>();}}private: std::unique_ptr<StateContext> state; std::vector<std::unique_ptr<NodeState>> nodes;};

问题:

  1. 多层 heap
  2. 多次 malloc
  3. cache miss
  4. 生命周期碎片化
  5. reset 只能销毁重建

二、优化后的核心思想

目标:
N a l l o c → 1 N_{alloc} \to 1 Nalloc​→1
关键技术:

  1. 成员整合(composition flattening)
  2. placement new
  3. reset 机制
  4. 区分不变数据 / 可变数据

三、优化后:结构扁平化

类关系:

Executor └── ExecutorContext ├── StateContext └── NodeState[n] 

不再是指针持有,而是:

  • 直接成员
  • 连续内存
  • 单块分配

优化后结构示例

constexpr size_t NODE_COUNT =4;classNodeState{public:voidReset(){ counter =0;// 重置可变数据}private:int counter =0;};classStateContext{public:voidReset(){ value =0;}private:int value =0;};classExecutorContext{public:voidReset(){ state.Reset();for(auto& node : nodes){ node.Reset();}}private: StateContext state;// 直接成员 NodeState nodes[NODE_COUNT];// 连续数组};classExecutor{public:voidReset(){ context.Reset();}private: ExecutorContext context;// 无堆分配};

四、为什么性能大幅提升?

1⃣ 分配次数减少

优化前:
N a l l o c = 8 N_{alloc} = 8 Nalloc​=8
优化后:
N a l l o c = 1 N_{alloc} = 1 Nalloc​=1

2⃣ Cache 友好

优化前:

Executor -> ptr -> heap ExecutorContext -> ptr -> heap NodeState -> ptr -> heap 

内存随机分布。
优化后:

[ Executor | ExecutorContext | StateContext | NodeState[] ] 

连续内存。
Cache 命中率提升:
P h i t ↑ P_{hit} \uparrow Phit​↑

3⃣ 消除 allocator 锁竞争

malloc 通常带锁。
并发下:
T m a l l o c = T l o c k + T h e a p T_{malloc} = T_{lock} + T_{heap} Tmalloc​=Tlock​+Theap​
消除后:
T l o c k = 0 T_{lock} = 0 Tlock​=0

4⃣ 用 reset 替代 destroy + create

优化前:

delete + new 

优化后:

Reset() 

时间对比:
T r e s e t ≪ T d e s t r o y + T c o n s t r u c t T_{reset} \ll T_{destroy} + T_{construct} Treset​≪Tdestroy​+Tconstruct​

五、placement 整合数组

如果 NodeState 数量运行时确定,可以:

placement 示例

classExecutorContext{public:ExecutorContext(size_t n):nodeCount(n){ buffer =::operatornew(sizeof(NodeState)* n);// 在 buffer 中构造数组 nodes =static_cast<NodeState*>(buffer);for(size_t i =0; i < n;++i){new(&nodes[i])NodeState();// placement}}~ExecutorContext(){for(size_t i =0; i < nodeCount;++i){ nodes[i].~NodeState();// 手动析构}::operatordelete(buffer);}private:void* buffer; NodeState* nodes; size_t nodeCount;};

此时:
N o d e S t a t e   a r r a y = 1   m e m o r y   b l o c k NodeState\ array = 1\ memory\ block NodeState array=1 memory block

六、区分不变数据与可变数据

核心原则:
I m m u t a b l e   d a t a →  构造一次 Immutable\ data\rightarrow\ 构造一次 Immutable data→ 构造一次
M u t a b l e   d a t a →   R e s e t Mutable\ data\rightarrow\ Reset Mutable data→ Reset
举例:

classNodeState{public:NodeState(int id):id(id){}// 不变数据voidReset(){ counter =0;// 可变数据}private:constint id;// 不变int counter;// 可变};

优势:

  • 减少重复构造
  • 减少 cache 失效
  • 降低 CPU pipeline flush

七、性能提升本质公式

总耗时:
T = T a l l o c + T c o n s t r u c t + T d e s t r o y + T a t o m i c T = T_{alloc} + T_{construct} + T_{destroy} + T_{atomic} T=Talloc​+Tconstruct​+Tdestroy​+Tatomic​
优化后:
T ≈ T r e s e t T \approx T_{reset} T≈Treset​
而:
T r e s e t ≪ T a l l o c + T c o n s t r u c t T_{reset} \ll T_{alloc} + T_{construct} Treset​≪Talloc​+Tconstruct​
因此:
10 u s → 2 u s 10us \to 2us 10us→2us

八、真正核心思想

这不是“奇技淫巧”。
而是:

生命周期分层 + 内存布局重构
优化本质:
面向对象设计 → 面向内存设计 面向对象设计 → 面向内存设计 面向对象设计→面向内存设计

九、终极总结

优化手段本质
成员整合减少分配次数
placement分离分配与构造
reset避免反复构造
连续数组cache 友好
去智能指针消除原子操作

十、最重要的一句话

高性能系统的核心:
对象关系图 = 内存布局图 对象关系图 = 内存布局图 对象关系图=内存布局图
当你设计类图时:

同时在脑子里画内存图。
这就是高级 C++ 架构师和普通 C++ 开发者的差别。

典型的并发错误示例,它展示了:

✗ 原子性破坏
✗ 可见性问题(cache 失效)
✗ 指令乱序
✗ 数据竞争(Data Race)

一、原始代码结构(加注释版本)

#include<iostream>#include<thread>int number_1 =0;// 共享变量int number_2 =0;// 共享变量// 线程 A/B 执行voidatom_increase_call(){for(int i =0; i <10000; i++){ number_1++;// 非原子操作 number_2++;// 非原子操作}}// 线程 C 执行voidatom_read_call(){int inorder_count =0;for(int i =0; i <10000; i++){// 读取共享变量if(number_2 > number_1){ inorder_count++;}} std::cout <<"thread:3 read inorder_number is "<< inorder_count << std::endl;}intmain(){ std::thread threadA(atom_increase_call); std::thread threadB(atom_increase_call); std::thread threadC(atom_read_call); threadA.join(); threadB.join(); threadC.join(); std::cout <<"thread:main read number_1 is "<< number_1 << std::endl; std::cout <<"thread:main read number_2 is "<< number_2 << std::endl;return0;}

二、为什么结果错乱?

理论上:
两个线程各执行 10000 次:
n u m b e r 1 = n u m b e r 2 = 20000 number_1 = number_2 = 20000 number1​=number2​=20000
但实际输出:

number_1 = 15379 number_2 = 15378 

说明:
丢失更新(Lost Update) \text{丢失更新(Lost Update)} 丢失更新(Lost Update)

三、问题一:原子性破坏

语句:

number_1++;

并不是一条指令。
它在 CPU 层面是:
x + + = l o a d ( x ) + a d d ( 1 ) + s t o r e ( x ) x++ = load(x) + add(1) + store(x) x++=load(x)+add(1)+store(x)
拆开:

mov eax, [number_1] add eax, 1 mov [number_1], eax 

如果两个线程同时执行:
线程 A 读到 5
线程 B 也读到 5
最后都写 6。
结果:
5 + 1 + 1 = 6 5 + 1 + 1 = 6 5+1+1=6
而不是:
7 7 7
这就是原子性破坏。

四、问题二:数据竞争(Data Race)

C++ 标准规定:

如果两个线程同时读写同一个变量,并且没有同步机制,
行为是 Undefined Behavior
数学描述:
∃ T 1 , T 2 : \exists T_1, T_2: ∃T1​,T2​:
W r i t e ( T 1 , x ) ∧ W r i t e ( T 2 , x ) ∧ N o S y n c Write(T_1, x) \land Write(T_2, x) \land NoSync Write(T1​,x)∧Write(T2​,x)∧NoSync
⇒ U B \Rightarrow UB ⇒UB
所以结果不可预测。

五、问题三:Cache 失效(可见性问题)

现代 CPU 结构:

Core1 L1 cache Core2 L1 cache ↓ L2 / L3 ↓ Memory 

线程 A 在 Core1:

number_1++ 

它可能只更新了:

Core1 L1 cache 

线程 B 在 Core2 读取时:
可能读到旧值。
这叫:
M e m o r y   V i s i b i l i t y   P r o b l e m Memory\ Visibility\ Problem Memory Visibility Problem

六、问题四:指令乱序

CPU 允许:
S t o r e ( n u m b e r 1 ) 与 S t o r e ( n u m b e r 2 ) 乱序 Store(number_1) 与 Store(number_2) 乱序 Store(number1​)与Store(number2​)乱序
线程 C 可能看到:

number_2 已更新 number_1 未更新 

于是:

if(number_2 > number_1)

成立。
这解释了:

inorder_number is 13 

七、为什么 number_2 > number_1 会发生?

理论上:

number_1++; number_2++;

顺序固定。
但在硬件层面:
W r i t e ( n u m b e r 2 ) → 先被其他核看到 Write(number_2) \rightarrow 先被其他核看到 Write(number2​)→先被其他核看到
W r i t e ( n u m b e r 1 ) → 后被看到 Write(number_1) \rightarrow 后被看到 Write(number1​)→后被看到
所以短暂时间:
n u m b e r 2 > n u m b e r 1 number_2 > number_1 number2​>number1​

八、如何正确实现高效互斥?

方案一:使用 std::atomic

#include<atomic> std::atomic<int> number_1{0}; std::atomic<int> number_2{0};

解释:

  • 原子 RMW
  • 自带内存序

完整安全版本

#include<iostream>#include<thread>#include<atomic> std::atomic<int> number_1{0}; std::atomic<int> number_2{0};voidatom_increase_call(){for(int i =0; i <10000; i++){ number_1.fetch_add(1, std::memory_order_relaxed); number_2.fetch_add(1, std::memory_order_relaxed);}}voidatom_read_call(){int inorder_count =0;for(int i =0; i <10000; i++){int n1 = number_1.load(std::memory_order_relaxed);int n2 = number_2.load(std::memory_order_relaxed);if(n2 > n1) inorder_count++;} std::cout <<"thread read inorder_number is "<< inorder_count << std::endl;}

九、memory_order 解释

如果你要求强一致:
使用:

std::memory_order_seq_cst 

保证:
全局顺序一致性 全局顺序一致性 全局顺序一致性
如果只要计数:
使用:

memory_order_relaxed 

减少 fence 开销。

十、方案二:使用 mutex

#include<mutex> std::mutex m;voidatom_increase_call(){for(int i =0; i <10000; i++){ std::lock_guard<std::mutex>lock(m); number_1++; number_2++;}}

优点:

  • 简单
  • 强一致
    缺点:
  • 有锁开销
  • 高竞争性能下降

十一、真正高效的并发互斥策略

分情况:

场景推荐
计数器atomic
读多写少RWLock
高并发lock-free
批量更新分段锁
极致性能per-thread + 合并

十二、终极优化:分线程计数

避免共享写:

thread_localint local_count =0;

最后汇总:
T o t a l = ∑ i = 1 N l o c a l i Total = \sum_{i=1}^{N} local_i Total=i=1∑N​locali​
时间复杂度:
O ( 1 ) → O ( N t h r e a d s ) O(1)\rightarrow O(N_{threads}) O(1)→O(Nthreads​)
但消除竞争。

十三、总结核心原理

你的问题本质:
共享可变状态 + 无同步 共享可变状态 + 无同步 共享可变状态+无同步
导致:

  1. 原子性破坏
  2. 可见性问题
  3. 指令乱序
  4. Undefined Behavior

十四、终极公式

并发安全必须满足:
H a p p e n s   B e f o r e Happens\ Before Happens Before
如果:
N o   H a p p e n s   B e f o r e No\ Happens\ Before No Happens Before
则:
B e h a v i o r = U n d e f i n e d Behavior = Undefined Behavior=Undefined

十五、一句话总结

并发错误不是“偶然错”,
是“内存模型允许你错”。
⾼效实现并发互斥
同步互斥开销对⽐

Read more

YOLOs-CPP:一个免费开源的YOLO全系列C++推理库(以YOLO26为例)

YOLOs-CPP:一个免费开源的YOLO全系列C++推理库(以YOLO26为例)

YOLOs-CPP:一个免费开源的YOLO全系列C++推理库(以YOLO26为例) * 前言 * 环境要求 * 相关介绍 * C++简介 * ONNX简介 * ONNX Runtime 简介 * **核心特点** * YOLOs-CPP简介 * 核心能力 * Windows下使用YOLOs-CPP * 下载YOLOs-CPP项目 * Windows * YOLOs-CPP项目结构 * 导出ONNX * 使用VS2019编译运行C++推理 * infer_utils.hpp * main.cpp * 推理结果(以YOLO26为例)) * 图像分类 * 目标检测 * 实例分割 * 旋转框检测 * 姿势估计 * 更多功能 * 参考 前言 由于本人水平有限,难免出现错漏,敬请批评改正。更多精彩内容,可点击进入Python日常小操作专栏、OpenCV-Python小应用专栏

By Ne0inhk
C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展 💡 学习目标:掌握运算符重载的核心语法与规则,能够为自定义类型重载常用运算符,实现类对象的灵活运算。 💡 学习重点:运算符重载的基本形式、成员函数与全局函数重载的区别、常见运算符的重载实现、禁止重载的运算符。 一、运算符重载的概念与核心价值 ✅ 结论:运算符重载是 C++ 静态多态的重要体现,允许为自定义类型(如类、结构体)重新定义运算符的行为,让自定义对象可以像内置类型一样使用运算符。 运算符重载的核心价值: 1. 简化代码书写:用直观的运算符替代繁琐的成员函数调用,提升代码可读性 2. 统一操作风格:让自定义类型的运算逻辑与内置类型保持一致,降低学习和使用成本 3. 扩展类型功能:根据业务需求定制运算符的行为,满足自定义类型的运算需求 ⚠️ 注意事项:运算符重载不会改变运算符的优先级和结合性,也不会改变运算符的操作数个数。 二、运算符重载的基本语法 运算符重载的本质是函数重载,分为成员函数重载和全局函数重载两种形式。 2.1 成员函数重载语法 将运算符重载函数定义为类的成员函数,语法格式如下: class

By Ne0inhk
【 C/C++ 算法】入门动态规划 ----- 简单多状态 dp 问题》打家劫舍 和 股票买卖问题

【 C/C++ 算法】入门动态规划 ----- 简单多状态 dp 问题》打家劫舍 和 股票买卖问题

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论 : ———————— 本章是dp的第三章,从第一章的简单理解dp的核心框架和写法&一维dp,再到第二章的路径问题&二维dp,到本章的多状态dp问题,本章将结合前面的所有基础引入多状态这个问题,并将由浅到深的从简单的打家劫舍两状态的dp到最后股票问题的四状态dp进行以练代学的方式学习,并且过程中会不断总结(具体见目录)。友情提示若没看过前面篇章的动规小白一定要先看看前面两章并简单练习下再往后看(一维dp - 路径dp),后续还将持续更新,敬请期待~ 早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。 打家劫舍 常见的思考是否使用打家劫舍问题时,遇见相邻问题不能选择此时就能思考是不是要使用打家劫舍 打家劫舍,常使用个dp表进行存储情况 1. f [ i ]:选择 i 位置时的最大价值 2. g [ i ]:不选择 i 位置时的最大价值 具体训练:

By Ne0inhk