欢迎光临
我们一直在努力

C++ 性能优化手册

C++ 性能优化手册

适用 C++17 / 20 / 23 · GCC / Clang / MSVC · 更新于 2026-08

性能优化的正确姿势:先用 profiler 找到真正的热点,再按收益从高到低选择手段,最后用基准测试验证。本手册汇总 10 个方向、40 余条可直接落地的优化方案,每条都标注了典型收益与代价。

在这里插入图片描述

三步走

  • 测量定位 —— 用 perf / VTune / Tracy 找出真正的热点,别靠直觉猜瓶颈。
  • 按收益选杠杆 —— 算法 > 内存布局 > 消除拷贝 > 编译器开关,从高到低逐个尝试。
  • 回归验证 —— 优化前后跑同一套基准测试,确认数字真的变好了再提交。’
  • 在这里插入图片描述

    目录

  • §01 测量先行:性能工作的前提
  • §02 算法与数据结构:最大的杠杆
  • §03 内存布局与缓存
  • §04 拷贝与移动:先消灭拷贝再谈别的
  • §05 容器与标准库细节
  • §06 并发与锁
  • §07 编译器与构建:免费的百分比
  • §08 向量化与 SIMD
  • §09 I/O 与其他
  • §10 速查清单与常见减速带

  • §01 测量先行:性能工作的前提 方法论

    • 先测量,再优化。 人判断热点的命中率低得惊人;Amdahl 定律决定了只有真正的热点才值得花时间。
    • 微基准用 Google Benchmark(或在线的 quick-bench),并用 DoNotOptimize 防止编译器把整个计算消掉。
    • 固定测量环境:绑核、关频率抖动、固定输入规模;报告中位数 / p99,不要单次值。
    • Debug 构建下测出的性能没有任何参考意义。
    工具用途平台
    perf 采样分析:热点函数、分支失效、缓存命中 Linux
    Intel VTune 微架构分析:前端停顿、SIMD 利用率 Windows / Linux
    Tracy 帧级实时 profiler,适合游戏与交互程序 跨平台
    Callgrind 精确到指令的调用计数(慢但精确) Linux / macOS
    Instruments 采样、时间线、内存分配分析 macOS
    Google Benchmark 微基准框架,自带防优化与统计 跨平台
    Compiler Explorer 在线查看生成汇编、对比两段代码 Web

    一个不会骗人的微基准:

    #include <benchmark/benchmark.h>

    static void BM_lookup(benchmark::State& state) {
    auto m = build_map(state.range(0)); // 构造放在计时之外
    for (auto _ : state) {
    benchmark::DoNotOptimize(m.find(42)); // 防止结果被优化掉
    }
    }
    BENCHMARK(BM_lookup)->Arg(1 << 20);


    §02 算法与数据结构:最大的杠杆 收益:高

    • 复杂度是第一杠杆:O(n²) 降到 O(n log n) 的收益,任何微观技巧都追不上。
    • 频繁查找:vector 线性扫描换成 unordered_map 索引;频繁求第 k 大用 nth_element。
    • n 很小时反转直觉:几十条数据内,有序 vector + 二分往往比哈希表更快(缓存局部性胜过渐进复杂度)。
    • 默认选连续内存容器(vector / deque);std::list 在绝大多数真实场景更慢。

    ✗ 优化前:每次查询线性扫描 O(n·m)

    bool exists(const std::vector<Order>& orders,
    int id) {
    return std::any_of(orders.begin(), orders.end(),
    [&](const Order& o) { return o.id == id; });
    }

    ✓ 优化后:建一次索引,查询 O(1)

    std::unordered_map<int, Order> index;
    index.reserve(orders.size());
    for (auto& o : orders)
    index.emplace(o.id, std::move(o));

    // 之后每次查询:index.find(id)


    §03 内存布局与缓存 收益:高

    • 现代 CPU 里一次缓存未命中约几十到上百周期;热点循环优先保证顺序、连续访问。
    • AoS → SoA:把参与计算的字段单独成列,缓存行里不装用不到的数据。
    • 伪共享:多线程写相邻变量会互相拖累,按缓存行(一般 64B)对齐隔离。
    • 小对象频繁 new/delete:改用内存池 / arena(std::pmr),整块分配、整体释放。
    • 定期用 sizeof 检查热结构体:删掉用不到的成员,注意对齐 padding 带来的虚胖。

    在这里插入图片描述

    在这里插入图片描述

    ✗ AoS:只用 pos/vel 却搬进整个结构体

    struct Particle {
    Vec3 pos, vel;
    float mass;
    char unused[48]; // 用不到的字段
    };
    std::vector<Particle> ps;

    ✓ SoA:缓存行里只装参与计算的字段

    struct Particles {
    std::vector<Vec3> pos;
    std::vector<Vec3> vel;
    std::vector<float> mass;
    };

    伪共享与内存池:

    // 伪共享:8 个计数器挤在一起,多核写入互抢同一条缓存行
    struct Counters { std::atomic<uint64_t> hit[8]; };

    // 对齐到缓存行,各自独占
    struct Counters {
    struct alignas(64) Slot { std::atomic<uint64_t> v{}; };
    std::array<Slot, 8> hit;
    };

    // arena 内存池:顺序分配、整体析构,避开逐条 new/delete
    char buf[1 << 20];
    std::pmr::monotonic_buffer_resource arena(buf, sizeof buf);
    std::pmr::vector<Node> nodes{&arena};


    §04 拷贝与移动:先消灭拷贝再谈别的 收益:高

    • 返回局部对象直接 return x;——RVO 保证零拷贝;画蛇添足的 return std::move(x) 反而可能阻止 RVO。
    • 传参:大的只读对象用 const&;要拿走所有权时用值传递 + std::move。
    • 只读字符串接口用 std::string_view,不拷贝且兼容 string / 字面量 / 子串(注意生命周期)。短字符串有 SSO,本来就不分配堆内存。
    • 字符串拼接用 += / append,别用 s = s + a + b。

    ✗ 不必要的深拷贝

    void send(std::string payload); // 按值传参

    std::string buffer = build_report();
    send(buffer); // 深拷贝整个字符串

    ✓ 转移所有权 + RVO

    send(std::move(buffer)); // 只转移内部指针

    Widget make() {
    Widget w;
    /* … */
    return w; // RVO,零拷贝
    }

    字符串拼接:

    // 每步都生成临时串,近似 O(n²) 的拷贝
    s = s + a + b + c; // ✗

    // 原地追加,摊销 O(n)
    s += a; s += b; s += c; // ✓


    §05 容器与标准库细节 收益:中 – 高

    • vector 扩容 = 翻倍重分配 + 全量搬迁:已知规模先 reserve()。
    • 删除元素用 erase–remove 一次遍历;C++20 直接 std::erase_if。
    • unordered_map:reserve 预分配桶 + 调 max_load_factor,避免中途 rehash。
    • 读多写少的键值场景:C++23 std::flat_map(有序 vector 实现,缓存友好)。
    • 容器里构造元素用 emplace_back 原地构造;只读视图传 std::span;小定长缓冲用 std::array。

    预分配与原地构造:

    std::vector<Record> out;
    out.reserve(input.size()); // 免去 log₂n 次扩容拷贝

    std::vector<UserProfile> users;
    users.emplace_back(id, name, age); // ✓ 原地构造
    users.push_back(UserProfile(id, name, age)); // 对比:先造临时对象再移动

    std::erase_if(v, pred); // ✓ C++20 一次遍历删除
    v.erase(std::remove_if(v.begin(), v.end(), pred), v.end()); // C++17 写法

    关联容器调优:

    std::unordered_map<Key, Val> m;
    m.reserve(n); // 预分配桶,避免 rehash
    m.max_load_factor(0.7f);

    std::flat_map<Key, Val> fm; // C++23:读多写少比红黑树更快


    §06 并发与锁 收益:高(并发场景)

    • 锁的成本主要来自竞争:缩小临界区、降低加锁频率,比换"更快的锁"有效得多。
    • 分片(sharding):按 key 哈希到 N 个独立锁,竞争概率降为约 1/N。
    • 读写比例悬殊用 shared_mutex;无竞争的简单状态用 atomic。
    • 生产者–消费者:有界队列 + 批量提交,把 1000 次加锁摊成 1 次;无锁队列只在 profile 证明值得时用。
    • 并行计算:std::execution::par(libstdc++ 需 TBB)/ OpenMP / TBB;注意划分粒度与伪共享(见 §03)。
    • 线程创建昂贵:用线程池,别按任务现开线程。

    分片锁示例:

    struct alignas(64) Shard { // 顺带避开伪共享
    std::mutex mu;
    std::unordered_map<Key, Val> data;
    };
    std::array<Shard, 64> shards;

    Shard& s = shards[std::hash<Key>{}(k) % shards.size()];
    {
    std::lock_guard lk(s.mu); // 临界区只包住一次读写
    s.data[k] = v;
    }


    §07 编译器与构建:免费的百分比 收益:中 – 高

    • 永远在 -O2(MSVC /O2)以上测性能;-O3 偶尔更快,需实测。
    • -march=native 解锁本机 AVX2 / AVX-512 等指令;跨平台分发时权衡。
    • LTO 跨编译单元内联与优化,通常白捡 2%–8%。
    • PGO:插桩采集真实负载再反馈编译,热路径提升常达 10%+;BOLT 可在链接后再做一层布局优化。
    • final / override 帮助去虚化与内联;彻底消除虚调用可用模板 / CRTP 静态多态。
    • [[likely]] / [[unlikely]] 标注分支倾向;__restrict 承诺无别名,帮助向量化。
    • 异常在主流 ABI 下是"不抛出即零成本"的表式实现;成本在真正 throw 的展开路径,热路径上不要 throw。
    GCC / ClangMSVC作用
    -O2 / -O3 /O2 基础优化等级(测性能的前提)
    -march=native /arch:AVX2 启用本机指令集
    -flto /GL + /LTCG 链接期优化(LTO)
    -fprofile-generate → -fprofile-use /GENPROFILE → /USEPROFILE PGO:按真实负载优化
    -fopt-info-vec-missed /Qvec-report:1 查看为什么没被自动向量化

    给编译器的提示:

    struct Renderer final : IRenderer { }; // final 帮助去虚化、内联

    if (n >= 0) [[likely]] { // C++20 分支提示
    fast_path(n);
    } else {
    slow_path(n);
    }

    void blend(const float* __restrict src, // 无别名承诺,帮助向量化
    float* __restrict dst, size_t n);


    §08 向量化与 SIMD 收益:视热点而定

    • 先让编译器自动向量化:循环要简单、连续内存、无数据依赖、无别名。
    • 用向量优化报告(见 §07 表格)看编译器为什么没向量化,逐条消除障碍。
    • 浮点循环若允许重排,可开 -ffast-math——注意它会改变 NaN / 结合律语义,数值程序要验证。
    • 手动 SIMD(intrinsics / SIMD 库)是最后手段:只在 profiler 证明值得时做。
    • alignas(32/64) 对齐 + 定长批次更利于生成向量指令。

    可自动向量化的循环:

    void axpy(float a, const float* __restrict x,
    float* __restrict y, size_t n) {
    for (size_t i = 0; i < n; ++i)
    y[i] += a * x[i]; // 连续内存、无别名、无复杂控制流
    }


    §09 I/O 与其他 收益:中

    • 大量 iostream 读写前:std::ios::sync_with_stdio(false) + cin.tie(nullptr)。
    • 换行用 '\\n',std::endl 每次都 flush。
    • 大文件一次性读入或用块读 / mmap,别逐 token 用 >> 提取;输出先攒缓冲再一次性写。
    • 格式化:std::format(C++20)明显快于 iostream;极限场景用 std::to_chars。
    • std::regex 构造昂贵:编译一次复用;热路径解析考虑手写。

    std::ios::sync_with_stdio(false); // 与 C stdio 解绑
    std::cin.tie(nullptr); // 解除 cin→cout 的隐式 flush

    std::cout << x << '\\n'; // 用 '\\n',别用 std::endl

    // 大文件一次读入
    std::ifstream f(path, std::ios::binary);
    std::string buf((std::istreambuf_iterator<char>(f)), {});


    §10 速查清单与常见减速带 速查

    手段出处典型收益注意事项
    reserve 预分配容量 §05 几乎无代价
    哈希索引替代线性扫描 §02 用内存换时间
    消除拷贝:移动 + RVO §04 理清所有权语义
    SoA / 紧凑内存布局 §03 需要重构数据结构
    分片降低锁竞争 §06 仅并发场景,复杂度上升
    PGO + LTO §07 中 – 高 构建流程变复杂
    批量 I/O、关 sync §09 几乎无代价
    string_view 传参 §04 生命周期要管好
    final / 静态多态去虚化 §07 低 – 中 放弃运行时多态
    手写 SIMD §08 最后手段 先证明值得,注意可移植性

    常见减速带 · 看到就顺手改掉

    • ✗ 热路径字符串拼接 s = s + a + b,应改 +=。
    • ✗ 大量输出时用 std::endl 代替 '\\n'。
    • ✗ std::vector<bool>:位压缩代理,行为不像正常容器。
    • ✗ 每次使用都重新构造 std::regex。
    • ✗ 热路径上 std::function + shared_ptr 的组合(间接跳转 + 原子计数)。
    • ✗ return std::move(局部变量),破坏 RVO。
    • ✗ 在 Debug 构建下做性能判断。

    “We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.”
    — Donald Knuth, 1974

    反过来说:剩下那 3% 的热点,值得按本手册从头到尾收拾一遍。先测量,再优化;优化之后,再测量。

    赞(0)
    未经允许不得转载:171主机测评 » C++ 性能优化手册
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址