C++ 性能优化:从缓存友好的数据布局开始

先问一个问题 同样是 O(n),为什么有的循环快很多? 答案常常不在算法复杂度,而在 CPU cache 命中率。现代 CPU 的瓶颈常常是内存访问,不是算术指令。 AoS 与 SoA 常见结构: struct Particle { float x, y, z; float vx, vy, vz; int alive; }; std::vector<Particle> particles; 如果你只更新位置,实际上每次还会把速度和状态也加载进缓存。更好的方式是 SoA: struct Particles { std::vector<float> x, y, z; std::vector<float> vx, vy, vz; std::vector<int> alive; }; 这样 CPU 读取的数据更“纯”,预取更有效。 减少伪共享 多线程里,两个线程写不同变量也可能互相拖慢,因为它们落在同一 cache line。 struct alignas(64) Counter { std::atomic<uint64_t> value; }; 用对齐把热点写入隔离开,通常能明显降低抖动。 少做指针追逐 链表、树这类结构在理论上优雅,但在缓存层面很吃亏。工程上更常见的折中是: 用连续数组表示节点池 索引代替裸指针 批量遍历而不是随机跳转 实践建议 先用 profiler 看 cache miss,再改代码 热路径优先考虑连续内存 把“数据怎么放”当成接口设计的一部分 小结 性能优化不是玄学。对于 C++,缓存友好的数据布局往往比微观语法技巧更值回票价。

2026年4月20日 · 1 分钟 · BvBeJ

C++ 内存池实践:高频对象分配的性能优化

背景 写 C++ 服务或者引擎代码时,经常会碰到一种情况:CPU 看着不高,但延迟就是压不下去。 最后一 profile,热点不在算法,也不在锁,而是在 operator new 和 operator delete。 这类问题在下面几种场景里尤其常见: 网络服务里大量创建短生命周期请求对象 游戏引擎里频繁分配小型组件 消息队列消费者持续构造临时 buffer 如果对象大小固定,或者分布相对集中,内存池通常是很直接的一刀。 为什么默认分配器会成为瓶颈 通用分配器要解决的问题很多: 不同尺寸的内存申请 跨线程竞争 碎片整理 对齐要求 这些能力都很重要,但它们也意味着额外开销。 如果你的场景很单一,比如“每次都申请一个 256 字节的请求对象”,那继续走通用分配器其实是在为用不到的能力买单。 一个简单的固定块内存池 先看一个足够说明问题的版本。 #include <cstddef> #include <new> #include <vector> class MemoryPool { public: MemoryPool(std::size_t blockSize, std::size_t blockCount) : block_size_(blockSize) { data_.resize(blockSize * blockCount); for (std::size_t i = 0; i < blockCount; ++i) { void* ptr = data_.data() + i * blockSize; free_list_.push_back(ptr); } } void* allocate() { if (free_list_.empty()) { throw std::bad_alloc(); } void* ptr = free_list_.back(); free_list_.pop_back(); return ptr; } void deallocate(void* ptr) { free_list_.push_back(ptr); } private: std::size_t block_size_; std::vector<char> data_; std::vector<void*> free_list_; }; 思路很直接: ...

2026年4月16日 · 2 分钟 · BvBeJ

Rust 所有权与借用:我的理解之路

缘起 从 C++ 转 Rust,最不适应的不是语法,而是一种全新的思维模式。 Rust 的所有权系统(Ownership)是语言最核心的创新,也是最陡峭的学习曲线。 C++ 的惯性思维 在 C++ 里,我们习惯了这样的写法: std::string get_name() { return "BvBeJ"; // 编译器会处理返回值优化 } void process() { std::string name = get_name(); std::string alias = name; // 拷贝?还是引用? // ... } // name 和 alias 都会析构 直觉告诉我们这里发生了拷贝。但在 Rust 里,同样的思维会让你碰壁。 Rust 的所有权规则 Rust 遵循三条简单规则: 每个值有一个所有者(Owner) 同一时间只有一个所有者 当所有者离开作用域,值被丢弃(Dropped) fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 被"移动"到 s2 // println!("{}", s1); // ❌ 编译错误!s1 已经无效 println!("{}", s2); // ✅ } // s2 离开作用域,内存被释放 “移动"语义取代了 C++ 的拷贝——这是最大的思维转变。 ...

2026年4月8日 · 2 分钟 · BvBeJ