引用计数对象池
Reference Counted Pool
题目详情
高频交易系统常因关键路径上的频繁动态内存分配而遭受延迟毛刺。引用计数对象池通过重用对象来缓解此问题,有效消除 new 和 delete 的开销。通过强制执行已释放对象的 LIFO 排序,对象池保持高缓存局部性并减少缺页中断。
任务:设计一个线程安全的引用计数对象池。
英文原题
High-frequency trading systems often suffer from latency spikes due to frequent dynamic memory allocation during critical paths. A reference-counted object pool mitigates this by reusing objects, effectively eliminating new and delete overhead. By enforcing Last-In, First-Out (LIFO) reuse, the pool also maximizes cache locality for rapidly recycled trade objects.
Task
Build a generic ObjectPool<T> class in C++ that manages a pool of reusable objects. It should provide a std::shared_ptr<T> to an object from the pool.
解析
问题分析
频繁的对象创建和销毁在高频交易中会引入不可预测的延迟。引用计数对象池通过预分配对象并回收重用,消除了运行时分配开销。LIFO(后进先出)回收策略利用缓存热数据,提高访问效率。
解决方案
template<typename T>
class RefCountedPool {
struct Slot { T obj; std::atomic<int> refs{0}; bool free{true}; };
std::vector<Slot> slots_;
std::stack<size_t> free_list_;
mutable std::mutex mtx_;
public:
explicit RefCountedPool(size_t cap) : slots_(cap) {
for (size_t i = 0; i < cap; ++i) free_list_.push(i);
}
template<typename... Args>
T* acquire(Args&&... args) {
std::lock_guard lk(mtx_);
if (free_list_.empty()) return nullptr;
size_t idx = free_list_.top(); free_list_.pop();
auto& slot = slots_[idx];
new (&slot.obj) T(std::forward<Args>(args)...);
slot.free = false; slot.refs = 1;
return &slot.obj;
}
void incref(T* ptr) { slotOf(ptr)->refs++; }
void decref(T* ptr) {
auto* s = slotOf(ptr);
if (--s->refs == 0) {
s->obj.~T(); s->free = true;
std::lock_guard lk(mtx_);
free_list_.push(s - slots_.data());
}
}
};关键考虑
- 无运行时分配:池在构造时一次性分配所有槽位,之后 acquire/release 仅为 O(1) 的栈操作。
- LIFO 回收:最近释放的对象最先被重用,其缓存行更可能仍在 CPU 缓存中。
- 线程安全:free_list 受互斥锁保护。引用计数使用 `std::atomic` 确保无竞争递增/递减。
- 边界条件:池满时 acquire 返回 nullptr;双重释放检测;use-after-free 防范。
- 时间复杂度:acquire/release 均为 O(1);空间复杂度:O(capacity)。
英文解析
Analysis
Frequent object creation and destruction in high-frequency trading introduces unpredictable latency. A reference-counted object pool eliminates runtime allocation overhead by pre-allocating objects and recycling them. The LIFO (last-in-first-out) reclamation strategy leverages cache-warm data for improved access efficiency.
Solution
template<typename T>
class RefCountedPool {
struct Slot { T obj; std::atomic<int> refs{0}; bool free{true}; };
std::vector<Slot> slots_;
std::stack<size_t> free_list_;
mutable std::mutex mtx_;
public:
explicit RefCountedPool(size_t cap) : slots_(cap) {
for (size_t i = 0; i < cap; ++i) free_list_.push(i);
}
template<typename... Args>
T* acquire(Args&&... args) {
std::lock_guard lk(mtx_);
if (free_list_.empty()) return nullptr;
size_t idx = free_list_.top(); free_list_.pop();
auto& slot = slots_[idx];
new (&slot.obj) T(std::forward<Args>(args)...);
slot.free = false; slot.refs = 1;
return &slot.obj;
}
void incref(T* ptr) { slotOf(ptr)->refs++; }
void decref(T* ptr) {
auto* s = slotOf(ptr);
if (--s->refs == 0) {
s->obj.~T(); s->free = true;
std::lock_guard lk(mtx_);
free_list_.push(s - slots_.data());
}
}
};Complexity & Edge Cases
- Time complexity: O(1) for acquire/release with atomic counter; O(N) for bulk reset
- Space complexity: O(PoolSize) for pre-allocated objects
- Edge cases: (1) Reference count overflow on 32-bit counters. (2) Release when count is already 0 indicates double-free. (3) Pool exhaustion when all objects are acquired simultaneously.
Key Considerations
- No runtime allocation: The pool allocates all slots at construction; subsequent acquire/release are O(1) stack operations.
- LIFO reclamation: Recently released objects are reused first, as their cache lines are more likely still in CPU cache.
- Thread safety: free_list is protected by a mutex. Reference counts use `std::atomic` for race-free increment/decrement.
- Edge cases: Pool full → acquire returns nullptr; double-free detection; use-after-free prevention.
- Time complexity: acquire/release O(1); Space complexity: O(capacity).