std::allocator
是所有标准库容器默认使用的内存分配器(封装了内存分配策略)。
new
把内存分配和对象构造耦合在一起,delete
则把内存释放和对象析构耦合在一起。
但是对于内存池等需求,这两个过程应该解耦成两步(内存管理与数据构造)。显然对于容器类型,
在使用reserve
扩容时,只是执行了内存分配,不应该进行对象构造。
1 | allocator<T> a; |
所以可以自己实现一个allocator(比如使用全局静态的buffer)
去替换标准库容器的内存分配逻辑。
list在每次插入时都需要进行内存分配;vector在size达到capacity时需要进行内存重分配。
std::pmr::memory_resource
对内存资源进行抽象,可以提供给std::pmr::polymorphic_allocator
进行分配和释放。
1 | allocate(); |
具体的memory_resource
类型,即memory_resource
的子类,override了纯虚函数。
std::pmr::monotonic_buffer_resource
:仅在析构时释放申请的内存std::pmr::unsynchronized_pool_resource
:内存池,用于单线程std::pmr::synchronized_pool_resource
:可以用于多线程,有锁保护std::new_delete_resource
:使用全局new
和delete
std::null_memory_resource
:什么都不干
使用就只需要创建一个内存资源,然后提供给polymorphic_allocator
使用即可。
因为在polymorphic_allocator
里拿到的是基类指针(即memory_resource *
),所以在执行具体的内存管理策略时是
依靠的运行时多态
1 | monotonic_buffer_resource( void* buffer, std::size_t buffer_size, |
placement new
注意operator new可以选择使用在分配失败时抛出异常的版本还是返回空指针的版本。
实际上operator new只是分配内存(大概率是调用了malloc),且可以被重载,并加入其它形参。
1 | void *A::operator new(size_t ); // operator new |
new operator首先调用operator new分配空间,然后转换成所需类型的指针后调用相关对象的构造函数(即调用placement new),并返回指针。
不能重载,其行为不能也不应被改变。
所以new其实也是有分配内存(operator new)和构造对象(placement new)两步构成的。
placement new是对operator new的一个全局重载,忽略size_t参数,
仅在传入的地址上执行构造函数创建对象,并返回地址。
注意在使用完后需要自行调用析构函数销毁。
new (ptr) T(1)
:operator new的额外参数加在new后面的括号里
内存分配器
目标:解决内部碎片(一个page内没用完)、外部碎片(申请大page时,连续的小page没法用)
链表:first fit, next fit, best fit
buddy(用多个链表分别维护不同的2的幂次的内存块,有时会进行合并或分裂,有效减缓外部碎片)
slab(在buddy的基础上,对小内存分配进行优化,进一步减缓内部碎片)
jemalloc进一步分为small、large、huge三种场景。
- arena:用于内存分配,每个CPU会绑定若干arena
- tcache:每个线程私有的缓存
重要的tricks:
- boudary tags:每个块之前和之后都有meta信息(该块的大小;如果该块被使用,末尾的meta信息可以被覆盖掉),这样可以从任意一个块往前或往后遍历,还可以合并连续的块。
- binning:相同大小的块放在一个bin里,用链表串联。
- caching:延迟空闲块的合并、提前空闲块的分裂