拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++高性能算法

C++高性能算法
  1. 多线程编程:C++11/14/17

TL;DR

  • 多线程优先任务并行,线程数约等于核数
  • SIMD 先自动向量化,热点再用 intrinsics
  • 内存池适合高频小对象分配场景
  • 并发结构优先使用成熟库
  • 性能优化必须测量先行,再优化

  1. 多线程编程:C++11/14/17

核心工具
• C++11:std::thread、std::mutex、std::condition_variable、std::atomic、std::future/promise/async、std::chrono
• C++14:泛型 lambda、std::shared_timed_mutex
• C++17:std::shared_mutex、std::scoped_lock、并行算法 std::execution::par、std::hardware_destructive_interference_size
关键原则

  1. 优先任务并行/数据并行,而不是手动创建大量线程:线程数通常约等于物理核数。

  2. 减少共享:线程私有数据、TLS、分块处理。

  3. 避免 false sharing:不同线程写的变量不要落在同一 cache line。

  4. 锁粒度适中:锁太粗并发低,锁太细开销大。

  5. 无锁不一定更快:竞争激烈时自旋、ABA、内存回收都很复杂。
    线程池骨架
    cpp
    class ThreadPool {
    public:
    explicit ThreadPool(size_t n) {
    for (size_t i = 0; i < n; ++i) {
    workers_.emplace_back([this] {
    while (true) {
    std::function<void()> task;
    {
    std::unique_lockstd::mutex lk(mtx_);
    cv_.wait(lk, [this] { return stop_ || !tasks_.empty(); });
    if (stop_ && tasks_.empty()) return;
    task = std::move(tasks_.front());
    tasks_.pop();
    }
    task();
    }
    });
    }
    }

    template
    void enqueue(F&& f) {
    {
    std::lock_guardstd::mutex lk(mtx_);
    tasks_.emplace(std::forward(f));
    }
    cv_.notify_one();
    }

    ~ThreadPool() {
    {
    std::lock_guardstd::mutex lk(mtx_);
    stop_ = true;
    }
    cv_.notify_all();
    for (auto& t : workers_) t.join();
    }

private:
std::vectorstd::thread workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mtx_;
std::condition_variable cv_;
bool stop_ = false;
};
false sharing 示例
cpp
struct alignas(std::hardware_destructive_interference_size) PaddedCounter {
std::atomic value{0};
};
C++17 并行算法
cpp
#include
#include

std::for_each(std::execution::par, v.begin(), v.end(), [](auto& x) {
x = heavy_compute(x);
});
通常需要 TBB 作为后端。


  1. SIMD 指令优化
    优化路径
  2. 先让编译器自动向量化
    o 连续内存访问
    o 循环次数可计算
    o 无函数调用、少分支
    o 使用 __restrict /restrict消除别名
    o 编译选项:-O3 -march=native -ftree-vectorize,MSVC /arch:AVX2
  3. 热点再用 intrinsics
    o x86:SSE、AVX、AVX2、AVX-512
    o ARM:NEON、SVE
  4. 数据布局
    o SoA 通常比 AoS 更适合 SIMD
    o 对齐:alignas(32)、std::aligned_alloc
  5. 注意
    o AVX-512 可能降频
    o 跨平台需运行时检测 CPU 特性
    o 优先使用 xsimd、std::experimental::simd 或未来 std::simd
    AVX2 数组加法
    cpp
    #include <immintrin.h>
    #include

void add_avx2(const float* __restrict a,
const float* __restrict b,
float* __restrict c,
size_t n) {
size_t i = 0;
for (; i + 8 <= n; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
_mm256_storeu_ps(c + i, _mm256_add_ps(va, vb));
}
for (; i < n; ++i) c[i] = a[i] + b[i];
}
编译:
bash
g++ -O3 -mavx2 -mfma main.cpp
自动向量化提示
cpp
void add(float* __restrict c,
const float* __restrict a,
const float* __restrict b,
size_t n) {
#pragma omp simd
for (size_t i = 0; i < n; ++i) {
c[i] = a[i] + b[i];
}
}
查看向量化报告:
bash
g++ -O3 -march=native -fopt-info-vec
clang++ -O3 -march=native -Rpass=vectorize


  1. 内存池实现
    为什么需要
    • malloc/free 通用但开销大、碎片多、缓存局部性差。
    • 高频小对象分配适合对象池/固定大小池。
    • 多线程下可用 thread-local pool + 中心池。
    常见类型
    • 固定大小内存池:free list,O(1) 分配释放
    • Slab / Chunk:批量申请大块,切分小块
    • Arena / Monotonic:只分配不单独释放,整体释放
    • 对象池:配合 placement new 管理对象生命周期
    • C++17 std::pmr:
    o std::pmr::monotonic_buffer_resource
    o std::pmr::unsynchronized_pool_resource
    o std::pmr::synchronized_pool_resource
    固定大小内存池简化版
    cpp
    class FixedPool {
    public:
    FixedPool(size_t block_size, size_t blocks_per_chunk)
    : block_size_(std::max(block_size, sizeof(Node))),
    blocks_per_chunk_(blocks_per_chunk) {
    grow();
    }

    ~FixedPool() {
    for (auto p : chunks_) ::operator delete§;
    }

    void* allocate() {
    if (!free_) grow();
    Node* n = free_;
    free_ = free_->next;
    return n;
    }

    void deallocate(void* p) {
    Node* n = static_cast<Node*>§;
    n->next = free_;
    free_ = n;
    }

private:
struct Node { Node* next; };

void grow() { std::byte* chunk = static_cast<std::byte*>( ::operator new(block_size_ * blocks_per_chunk_)); chunks_.push_back(chunk); for (size_t i = 0; i < blocks_per_chunk_; ++i) { Node* n = reinterpret_cast<Node*>(chunk + i * block_size_); n->next = free_; free_ = n; } } size_t block_size_; size_t blocks_per_chunk_; Node* free_ = nullptr; std::vector<void*> chunks_;

};
多线程版本:每线程一个池,或给 free list 加锁,或用原子栈 + 批量迁移。
C++17 PMR 示例
cpp
#include <memory_resource>
#include

std::pmr::monotonic_buffer_resource pool;
std::pmr::vector v{&pool};
v.push_back(1);
// 适合生命周期短、批量释放的场景
工程建议
• 先尝试替换全局分配器:jemalloc、tcmalloc、mimalloc
• 特殊热点对象再用自定义池
• 注意对齐、构造/析构、线程安全、生命周期


  1. 并发数据结构
    分类
    • 互斥锁保护:简单可靠,适合低竞争
    • 细粒度锁:分段哈希表、锁条带化
    • 无锁 lock-free:CAS 循环,至少一个线程能推进
    • wait-free:更强,但实现极难
    常见结构
    • SPSC 环形队列:单生产者单消费者,通常最快
    • MPMC 队列:Michael-Scott 队列,或直接用成熟库
    • 无锁栈:简单但 ABA 问题严重
    • 并发哈希表:分段锁、开放寻址、RCU
    • RCU / Hazard Pointer / Epoch Reclamation:安全内存回收
    无锁栈示意(不完整,仅教学)
    cpp
    template
    class LockFreeStack {
    struct Node {
    T value;
    Node* next;
    };

    std::atomic<Node*> head_{nullptr};

public:
void push(T v) {
Node* n = new Node{std::move(v), head_.load(std::memory_order_relaxed)};
while (!head_.compare_exchange_weak(
n->next, n,
std::memory_order_release,
std::memory_order_relaxed)) {}
}

bool pop(T& out) { Node* old = head_.load(std::memory_order_acquire); while (old && !head_.compare_exchange_weak( old, old->next, std::memory_order_acq_rel, std::memory_order_acquire)) {} if (!old) return false; out = std::move(old->value); delete old; // 危险:真实场景需要 hazard pointer / epoch / RCU return true; }

};
内存序速查
• relaxed:只保证原子性,适合计数
• acquire:读操作,后面代码不能上移
• release:写操作,前面代码不能下移
• acq_rel:读改写
• seq_cst:默认,最安全但最慢
工程建议
优先使用成熟库:
• Intel TBB:并发容器、并行算法
• Boost.Lockfree:无锁队列/栈
• Folly:高性能并发结构
• moodycamel::ConcurrentQueue:优秀 MPMC 队列
• libcds:无锁数据结构集合
自研无锁结构必须解决:
• ABA
• 内存回收
• 虚假唤醒/自旋
• 内存序正确性
• 可移植性和测试


  1. 综合优化路线
  2. 测量优先:Google Benchmark、perf、VTune、cachegrind、heaptrack
  3. 并行化:线程池 + 分块,避免 false sharing
  4. 向量化:先自动向量化,热点用 intrinsics
  5. 内存:替换分配器,热点对象池,SoA 布局
  6. 并发结构:优先成熟库,SPSC 环形队列常优于通用 MPMC
  7. NUMA 与亲和性:绑定线程,减少跨节点访问
  8. 编译器:-O3 -march=native -flto -fprofile-use

  1. 性能分析与调优实战

测量先行
• 用 Google Benchmark 写微基准,避免编译器优化掉被测代码
• perf stat 看 IPC、cache miss、branch miss
• VTune / perf record 定位热点函数
• heaptrack / valgrind massif 检查内存分配热点

Google Benchmark 示例
cpp
#include <benchmark/benchmark.h>

static void BM_Add(benchmark::State& state) {
std::vector a(1 << 20), b(1 << 20), c(1 << 20);
for (auto _ : state) {
for (size_t i = 0; i < a.size(); ++i) c[i] = a[i] + b[i];
}
}
BENCHMARK(BM_Add)->Arg(1 << 20);
BENCHMARK_MAIN();

perf 常用命令
bash
perf stat ./app # 汇总统计
perf record -g ./app # 采样调用栈
perf report # 查看热点
perf annotate # 查看汇编级热点

优化验证流程

  1. 先建立基线:记录优化前的耗时、吞吐、内存占用
  2. 每次只改一个变量,避免混淆
  3. 用同一硬件、同一编译器、同一优化选项对比
  4. 关注 P95/P99 而非平均值,避免被长尾掩盖
  5. 回归测试:性能优化不能破坏正确性

常见陷阱
• 微基准被编译器优化掉:用 benchmark::DoNotOptimize
• 冷启动 vs 热运行:预热后再计时
• CPU 频率波动:绑定频率或多次取中位数
• false sharing 在基准中不明显,真实负载才暴露
• 过早优化:先测量,再优化,再测量


  1. 综合实战:图像处理流水线优化

本节用一个完整的图像处理案例,把前面几节的多线程、SIMD、内存池、性能测量串起来:从单线程基线出发,逐步叠加并行化、向量化和内存优化,并给出每一步的性能数据与对比。

场景设定
对一张 4096×4096 的灰度图做三步处理:

  1. 高斯模糊(3×3 卷积)
  2. 阈值二值化
  3. 直方图统计

输入为 float 数组,输出为 uint8_t 数组。我们以「处理整张图的总耗时」为指标,硬件为 8 核 x86-64,编译器 g++ -O3 -march=native。

第一步:单线程基线
cpp
#include
#include
#include
#include

void gaussian_blur(const float* __restrict in,
float* __restrict out,
int w, int h) {
for (int y = 1; y < h - 1; ++y) {
for (int x = 1; x < w - 1; ++x) {
float sum = 0.0f;
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx)
sum += in[(y + dy) * w + (x + dx)];
out[y * w + x] = sum / 9.0f;
}
}
}

void threshold(const float* __restrict in,
uint8_t* __restrict out,
int w, int h, float t) {
for (int i = 0; i < w * h; ++i)
out[i] = in[i] > t ? 255 : 0;
}

void histogram(const uint8_t* __restrict in,
uint32_t* __restrict hist, int n) {
for (int i = 0; i < n; ++i)
++hist[in[i]];
}

int main() {
const int W = 4096, H = 4096, N = W * H;
std::vector in(N, 128.0f), blur(N);
std::vector<uint8_t> bin(N);
std::vector<uint32_t> hist(256, 0);

auto t0 = std::chrono::steady_clock::now(); gaussian_blur(in.data(), blur.data(), W, H); threshold(blur.data(), bin.data(), W, H, 128.0f); histogram(bin.data(), hist.data(), N); auto t1 = std::chrono::steady_clock::now(); double ms = std::chrono::duration<double, std::milli>(t1 - t0).count(); std::printf("baseline: %.2f ms\n", ms); return 0;

}
基线耗时:约 42.0 ms。

第二步:多线程 + 分块
把图像按行切成 8 块,每块交给一个线程处理。三个步骤都按行分块,避免跨线程共享 cache line。
cpp
#include
#include

void run_parallel(const float* in, float* blur, uint8_t* bin,
uint32_t* hist, int w, int h, int nthreads) {
std::vectorstd::thread threads;
int rows_per_thread = h / nthreads;

for (int t = 0; t < nthreads; ++t) { int y0 = t * rows_per_thread; int y1 = (t == nthreads - 1) ? h : y0 + rows_per_thread; threads.emplace_back([=] { // 每线程私有直方图,避免原子竞争 std::vector<uint32_t> local_hist(256, 0); for (int y = y0; y < y1; ++y) { for (int x = 1; x < w - 1; ++x) { float sum = 0.0f; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) sum += in[(y + dy) * w + (x + dx)]; blur[y * w + x] = sum / 9.0f; } } for (int i = y0 * w; i < y1 * w; ++i) bin[i] = blur[i] > 128.0f ? 255 : 0; for (int i = y0 * w; i < y1 * w; ++i) ++local_hist[bin[i]]; // 合并到全局直方图 for (int k = 0; k < 256; ++k) hist[k] += local_hist[k]; }); } for (auto& th : threads) th.join();

}
耗时:约 7.8 ms,加速比约 5.4×。未达到 8× 是因为内存带宽和线程调度开销。

第三步:叠加 SIMD 向量化
对高斯模糊和阈值做 AVX2 优化。模糊的 3×3 卷积可拆成水平与垂直两次一维滤波,便于向量化。
cpp
#include <immintrin.h>

// 水平 3 点均值:out[x] = (in[x-1] + in[x] + in[x+1]) / 3
void blur_h_avx2(const float* __restrict in,
float* __restrict out, int w, int h) {
for (int y = 0; y < h; ++y) {
const float* row = in + y * w;
float* orow = out + y * w;
int x = 1;
for (; x + 7 < w - 1; x += 8) {
__m256 a = _mm256_loadu_ps(row + x - 1);
__m256 b = _mm256_loadu_ps(row + x);
__m256 c = _mm256_loadu_ps(row + x + 1);
__m256 s = _mm256_add_ps(_mm256_add_ps(a, b), c);
_mm256_storeu_ps(orow + x, _mm256_mul_ps(s, _mm256_set1_ps(1.0f / 3.0f)));
}
for (; x < w - 1; ++x)
orow[x] = (row[x - 1] + row[x] + row[x + 1]) / 3.0f;
}
}

// 垂直 3 点均值:out[y][x] = (in[y-1][x] + in[y][x] + in[y+1][x]) / 3
void blur_v_avx2(const float* __restrict in,
float* __restrict out, int w, int h) {
for (int y = 1; y < h - 1; ++y) {
const float* r0 = in + (y - 1) * w;
const float* r1 = in + y * w;
const float* r2 = in + (y + 1) * w;
float* orow = out + y * w;
int x = 0;
for (; x + 7 < w; x += 8) {
__m256 a = _mm256_loadu_ps(r0 + x);
__m256 b = _mm256_loadu_ps(r1 + x);
__m256 c = _mm256_loadu_ps(r2 + x);
__m256 s = _mm256_add_ps(_mm256_add_ps(a, b), c);
_mm256_storeu_ps(orow + x, _mm256_mul_ps(s, _mm256_set1_ps(1.0f / 3.0f)));
}
for (; x < w; ++x)
orow[x] = (r0[x] + r1[x] + r2[x]) / 3.0f;
}
}

// 阈值 + 直方图:一次遍历完成
void threshold_hist_avx2(const float* __restrict in,
uint8_t* __restrict bin,
uint32_t* __restrict hist, int n) {
__m256 th = _mm256_set1_ps(128.0f);
int i = 0;
for (; i + 7 < n; i += 8) {
__m256 v = _mm256_loadu_ps(in + i);
__m256 mask = _mm256_cmp_ps(v, th, _CMP_GT_OS);
int m = _mm256_movemask_ps(mask); // 8 bit
for (int k = 0; k < 8; ++k) {
uint8_t val = (m >> k) & 1 ? 255 : 0;
bin[i + k] = val;
++hist[val];
}
}
for (; i < n; ++i) {
bin[i] = in[i] > 128.0f ? 255 : 0;
++hist[bin[i]];
}
}
把第二步的并行框架换成 AVX2 内核后,耗时约 2.1 ms,相比基线加速约 20×。

第四步:叠加内存池
图像处理中 blur 中间缓冲反复分配。用固定大小内存池或 std::pmr 避免每次 malloc/free。
cpp
#include <memory_resource>

// 用单调内存池管理中间缓冲,整批释放
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector blur_buf{&pool};
blur_buf.resize(N);
std::pmr::vector<uint8_t> bin_buf{&pool};
bin_buf.resize(N);
// 处理完成后 pool.release() 一次性回收
pool.release();
配合内存池后,耗时约 1.9 ms。提升主要来自减少分配开销和改善缓存局部性。

优化前后对比

版本耗时相对基线加速
单线程基线42.0 ms1×
多线程(8 线程)7.8 ms5.4×
多线程 + AVX22.1 ms20×
多线程 + AVX2 + 内存池1.9 ms22×

小结

  1. 先测量,再优化:基线 42 ms,明确瓶颈在模糊卷积。
  2. 并行化收益最大:8 线程带来 5.4×,接近核数上限。
  3. 向量化进一步压榨 ALU:AVX2 让卷积和阈值提速约 3.7×。
  4. 内存池锦上添花:减少分配开销,适合高频小缓冲场景。
  5. 每一步都要回归验证正确性:直方图总和必须等于像素总数。

  1. 总结与展望

核心要点回顾

  1. 多线程编程:优先任务并行/数据并行,线程数约等于物理核数;减少共享、避免 false sharing、锁粒度适中;无锁不一定更快。
  2. SIMD 优化:先让编译器自动向量化(连续内存、可计算循环、__restrict、-O3 -march=native),热点再用 intrinsics;SoA 布局通常优于 AoS。
  3. 内存池:malloc/free 通用但开销大、碎片多;高频小对象用固定大小池/对象池,生命周期短用 Arena/PMR;多线程用 thread-local pool + 中心池。
  4. 并发数据结构:优先成熟库(TBB、Boost.Lockfree、Folly、moodycamel、libcds);自研无锁必须解决 ABA、内存回收、内存序正确性。
  5. 性能分析:测量先行,用 Google Benchmark、perf、VTune 定位热点;关注 P95/P99 而非平均值;每次只改一个变量并回归验证。

性能优化的一般性流程

  1. 建立基线:记录优化前的耗时、吞吐、内存占用。
  2. 定位瓶颈:用 perf/VTune 找热点函数,用 heaptrack 查内存分配热点。
  3. 并行化:线程池 + 分块,避免 false sharing,收益通常最大。
  4. 向量化:先自动向量化,热点用 intrinsics,注意数据布局与对齐。
  5. 内存优化:替换全局分配器(jemalloc/tcmalloc/mimalloc),热点对象用自定义池。
  6. 并发结构:优先成熟库,SPSC 环形队列常优于通用 MPMC。
  7. 验证与回归:同一硬件、同一编译器、同一优化选项对比;性能优化不能破坏正确性。

C++20/23 新特性对性能优化的影响
• std::jthread(C++20):自带 joinable 语义与协作式取消(stop_token),可安全地请求线程停止,避免手动管理 std::thread 的 join 与退出标志,简化线程池和长任务的生命周期管理。
• std::barrier(C++20):可复用的同步屏障,适合分阶段流水线(如每轮迭代所有线程到达后再进入下一阶段),比手动用 mutex + condition_variable 实现更简洁、开销更低。
• std::latch(C++20):一次性倒计时门闩,适合「等待 N 个任务全部完成」的场景,如并行分块处理结束后统一合并结果,比逐线程 join 更轻量。
• std::atomic_ref(C++20):对非原子对象按原子方式访问,便于在既有数据结构上做无锁改造,无需重写整个容器。
• std::execution 并行算法(C++17,C++20 完善):配合 TBB 后端,一行代码即可把 std::for_each/std::transform 并行化,降低手写线程池的维护成本。
• C++23 展望:std::mdspan 提供多维数组视图,配合 SIMD 友好的连续布局,可简化图像/矩阵类高性能代码;std::expected 等错误处理改进减少异常开销;未来 std::simd 有望把向量化从 intrinsics 提升到可移植的库层面。

结语
性能优化没有银弹:先测量、再优化、再测量。多线程、SIMD、内存池、并发结构是四把常用工具,但真正的收益来自对瓶颈的准确判断和每一步的回归验证。随着 C++20/23 引入 jthread、barrier、latch 等更安全、更轻量的并发原语,编写高性能并发代码的门槛正在逐步降低——善用标准库,把精力留给真正需要手写优化的热点。

返回列表