https://blog.csdn.net/vjhghjghj/article/details/88647336
https://blog.csdn.net/songchuwang1868/article/details/89951543
空间配置器
allocator 是 STL 的六大组件(容器、适配器、算法、函数对象、迭代器、空间配置器)之一
函数对象比较少见到,例如 std::bind
其作用就是为各个容器管理内存(内存开辟与回收),有时不一定是内存,也可以是硬盘,这里主要考虑内存
STL 中,allocator 将 new
和 delete
一个对象的两个阶段分开,分别用四个函数实现:
alloc::allocate()
::construct()
::destroy()
alloc::deallocate()
空间配置器放在 memory 头文件中,它引用了 bits 下的几个重要的头文件:
- stl_construct.h:定义了全局函数
::construct()
和::destroy()
stl_alloc.h(SGI 风格,现已被废弃):定义了一二级配置器- new_alloctor.h、alloctor.h、alloc_traits.h:定义了空间配置器和它的类型萃取
- stl_uninitialized.h:定义了一些全局函数,用来填充或拷贝大块的内存数据,例如
un_initialized_copy()
函数
构造和析构
在 bits/stl_construct.h 头文件中,源码简略表示如下:
template <typename T>
constexpr void destroy_at(T* location) {
if constexpr (is_array_v<T>) {
for (auto& x : *location) {
// 如果是个 array,那么要对所有位置执行 destroy
std::destroy_at(std::addressof(x));
}
} else {
location->~T();
}
}
template <typename T, typename... Args>
constexpr auto construct_at(T* location, Args&&... args)
noexcept(noexcept(::new(static_cast<void*>(nullptr)) T(std::declval<Args>()...)))
-> decltype(::new(static_cast<void*>(nullptr)) T(std::declval<Args>()...)) {
// 在 location 位置传入 args 构造一个对象
return ::new(static_cast<void*>(location)) T(std::forward<Args>(args)...);
}
template <typename ForwardIterator>
constexpr void Destroy(ForwardIterator first, ForwardIterator last);
template <typename T>
constexpr void Destroy(T* pointer) {
// 单个指针直接调用 destroy_at
std::destroy_at(pointer);
}
template <bool>
struct Destroy_aux {
template <typename ForwardIterator>
static constexpr void destroy(ForwardIterator first, ForwardIterator last) {
for (; first != last; ++first) {
std::Destroy(std::addressof(*first));
}
}
};
template <>
struct Destroy_aux<true> {
template <typename ForwardIterator>
static void destroy(ForwardIterator, ForwardIterator) {
}
};
template <typename ForwardIterator>
constexpr void Destroy(ForwardIterator first, ForwardIterator last) {
using Value_type = typename std::iterator_traits<ForwardIterator>::value_typ;
// 必须可以被析构
static_assert(std::is_destructible_v<Value_type>, "value type is destructible");
if (std::is_constant_evaluated()) {
// 如果当前是 constexpr 语境,那么直接使用 constexpr 版本的函数,在编译期展开
return Destroy_aux<false>::destroy(first, last);
}
// 如果非 constexpr 语境,因为调用函数会产生开销,只需要在非平凡析构时执行析构函数
// __has_trivial_destructor 在用户有自定义的析构函数时返回 false
std::Destroy_aux<__has_trivial_destructor(Value_type)>::destroy(first, last);
}
template <bool>
struct Destroy_n_aux {
template <typename ForwardIterator, typename Size>
static constexpr ForwardIterator destroy_n(ForwardIterator first, Size count) {
for (; count > 0; (void)++first, --count) {
std::Destroy(std::addressof(*first));
}
return first;
}
};
template <>
struct Destroy_n_aux<true> {
template <typename ForwardIterator, typename Size>
static ForwardIterator destroy_n(ForwardIterator first, Size count) {
// 将 first 迭代器直接移动到 count 处
std::advance(first, count);
return first;
}
};
template <typename ForwardIterator, typename Size>
constexpr ForwardIterator Destroy_n(ForwardIterator first, Size count) {
using Value_type = typename std::iterator_traits<ForwardIterator>::value_type;
static_assert(std::is_destructible_v<Value_type>, "value type is destructible");
if (std::is_constant_evaluated()) {
return Destroy_n_aux<false>::destroy_n(first, count);
}
return std::Destroy_n_aux<__has_trivial_destructor(Value_type)>::destroy_n(first, count);
}
template <typename ForwardIterator>
constexpr void destroy(ForwardIterator first, ForwardIterator last) {
std::Destroy(first, last);
}
template <typename ForwardIterator, typename Size>
constexpr ForwardIterator destroy_n(ForwardIterator first, Size count) {
return std::Destroy_n(first, count);
}
SGI 风格的内存的释放与分配
在 SGI 风格 STL 中(现在已经不再使用),为了解决小型区块所可能造成的内存破碎问题,设计了双层级配置器:
- 当申请的内存大小大于 128 字节时,就启动第一级分配器通过
malloc
直接向系统的堆空间分配 - 小于等于 128 字节时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由 16 个不同大小(8 的倍数,8 到 128 字节)的空闲列表组成,分配器会根据申请内存的大小(将这个大小向上取整成 8 的倍数)从对应的空闲块列表取表头块给用户
优点:
- 小对象的快速分配
- 减少内存碎片的产生
现代 STL 中的内存分配与释放
alloctor<T>::allocate(size_t)
直接调用::operator new(size_t)
alloctor<T>::deallocate()
直接调用::operator delete()
简化的代码如下:
template<typename T>
class new_allocator {
...
[[no_discard]] T allocate(std::size_t n, const void* = static_cast<const void*>(nullptr)) {
static_assert(sizeof(T) != 0, "cannot allocate incomplete types");
// 检查一下数量
if (__builtin_expect(n > std::size_t(__PTRDIFF_MAX__) / sizeof(T), false)) {
if (n > (std::size_t(-1) / sizeof(T))) {
std::throw_bad_array_new_length();
}
std::throw_bad_alloc();
}
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) {
::operator delete(p, n * sizeof(T));
}
...
};
template<typename T>
class allocator : public new_allocator<T> {
...
constexpr T* allocate(std::size_t n) {
if (std::is_constant_evaluated()) {
// 看一下 n * sizeof(T) 是否会溢出
if (__builtin_mul_overflow(n, sizeof(T), &n)) {
std::throw_bad_array_new_length();
}
return static_cast<T*>(::operator new(n));
}
return new_allocator<T>::allocate(n);
}
constexpr void deallocate(T* p, std::size_t n) {
if (std::is_constant_evaluated()) {
::operator delete(p);
return;
}
new_allocator<T>::deallocate(p, n);
}
...
};
然后使用了对空间配置器的类型萃取来简化使用
底层内存分配器
delete
通常不会直接将内存立即交还给操作系统,而是交还给 C++ 运行时的内存分配器(如 malloc
/free
实现),由它决定何时释放给操作系统
而 malloc
/free
的实现与操作系统有关,会调用操作系统层次的底层分配器
常见的有:
- glibc 的
ptmalloc
策略:malloc()
会从堆区或通过mmap
分配内存free()
会将小块内存(通常 < 128KB)放入fastbins
或unsorted bins
。只有当内存块足够且位于独立的mmap
区域时才可能真正调用munmap()
把内存归还操作系统
- Windows 的
HeapAlloc/HeapFree
- jemalloc、tcmalloc
下面直接看比较典型的 glibc 的 ptmalloc
策略
brk()
和 sbrk()
函数(系统调用)
放在 unistd.h 头文件中
这两者的作用是扩展 heap 的上界 brk,其中:
int brk(const void*)
会将它的参数设置为新的 brk 上界地址(堆空间从低地址到高地址),成功返回 1,失败返回 0void* sbrk(intptr_t)
的参数为申请内存的大小,它会扩展 brk 的上界地址,返回新的 brk 的地址
mmap()
和 munmap()
函数(系统调用)
放在 sys/mman.h 头文件中
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
mmap()
有两种用法:
- 第一种用法是映射此盘文件到内存中
- 第二种用法是匿名映射,不映射磁盘文件,而向映射区申请一块内存。
malloc
使用的是 mmap
的第二种用法(匿名映射)
munmap()
函数用于释放内存
malloc()
实现原理
brk
、sbrk
、mmap
都属于系统调用,若每次申请内存,都调用这三个,那么:
- 每次都会产生系统调用,影响性能
- 会产生碎片,如果高地址的内存没有被释放,低地址的内存就不能被回收
所以 malloc
采用的是内存池的管理方式(ptmalloc)
它使用边界标识法(Boundary Tag Method)将连续的内存划分为连续的若干个块(chunk),从而对内存的分配与回收进行管理
内存块的描述结构
块的定义如下:
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; // 如果前一个块空闲,那么这个值记录的是前一个块的大小,否则无意义
INTERNAL_SIZE_T size; // 当前块的大小,在这个变量的某几位记录了 overhead 头信息
// 下面两个变量只有当当前块空闲时才有用
struct malloc_chunk* fd; // 空闲块链表 next
struct malloc_chunk* bk; // 空闲块链表 prev
// 下面两个变量只有当当前的块存在于 large bins 时才有用
struct malloc_chunk* fd_nextsize; // 指向下一个比当前空闲块大的第一个空闲块
struct malloc_chunk* bk_nextsize; // 指向前一个比当前空闲块大的第一个空闲块
};
其中,overhead 头信息包含在 size
变量中,可以使用位运算获取:
#define PREV_INUSE 0x1 // 前一个块是否正在使用
#define IS_MMAPPED 0x2 // 是否由 mmap 分配
#define NON_MAIN_ARENA 0x4 // 是否非主线程使用的 arena
真正的大小其实是 chunk->size & ~0x7
,这就要求块的大小一定是 8 的倍数
关于为什么需要在 large bins 时设置两个 nextsize
变量:large bins 中的空闲块是按照大小排序的,但同一个大小的块可能有多个,设置这两个变量可以加快空闲空间的查找
注意:四个指针变量仅仅当块为空时才有用,否则会交给用户使用,作为空闲空间的一部分
内存块的结构
chunk 的结构可以分为使用中的 chunk 和空闲的 chunk,使用中的 chunk 和空闲的 chunk 数据结构基本相同,但是会有一些设计上的小技巧,巧妙的节省了内存
使用中的 chunk 如下:
其中:
- A = 0 为主分配区分配;A = 1 为非主分配区分配
- M = 1 为
mmap
映射区域分配;M = 0 为 heap 区域分配(由brk
分配) - P = 0 表示前一个 chunk 空闲;P = 1 表示前一个 chunk 正在使用
空闲的 chunk 如下:
其中:
- 当 chunk 空闲时,其 M 状态是不存在的,只有 A 和 P 状态。因为 M 表示是由
brk
还是mmap
分配的内存,而mmap
分配的内存free
时直接ummap
,不会放到空闲链表中。换言之空闲链表中的都是brk
分配的,所以不用额外记录
ptmalloc
的空间复用比较离谱,在 chunk 的使用中 prev_size
并没有意义,所以它可能被“借”给前一个 chunk。
所以,计算出:
in_use_size = (request_size + 8 - 4)
(需要存储prev_size
和size
,然后向下一个 chunk 借了一个prev_size
),然后对齐到 8 字节。这表示使用中的 chunk 需要的内存空间chunk_size = max(in_use_size, 16)
(空闲的 chunk 由于要存至少两个指针,后面两个可能不需要,所以共 16 字节),这表示一个 chunk 需要的内存空间
空闲链表
当用户使用 free
函数释放掉的内存,ptmalloc
并不会马上交还给操作系统,而是被 ptmalloc
本身的空闲链表 bins 管理起来了
malloc
将相似大小的 chunk 用双向链表链接起来,这样一个链表被称为一个 bin。ptmalloc
共维护了 128 个 bin,每个 bin 都用双向链表维护了一些大小相近的 chunk
基于 chunk 的大小,有下列几种类型的 bin:
- Fast bins
- Unsorted bin
- Small bins
- Large bins
Fast bins
除非特定情况,fast bins 上两个毗连的空闲 chunk 并不会被合并成一个空闲 chunk
fast bins 是一种高速缓冲,它大约有 10 个定长队列,每个定长队列记录了相同大小的 chunk,如下:
当用户释放一块不大于 max_fast
(默认值 64B)的 chunk 的时候,会默认会被放到 fast bins 上
当需要给用户分配的 chunk 小于或等于 max_fast
时,malloc
首先会到 fast bins 上寻找是否有合适的chunk
Unsorted bin
unsorted bin 也是一种缓冲区,用户释放的内存大于 max_fast
或者 fast bins 合并后的 chunk 都会首先进入 unsorted bin 上
这里的 chunk 大小无限制,也没有排序,这样的方式给予 malloc
第二次机会以重新使用最近 free
掉的 chunk
用户 malloc
时,如果在 fast bins 中没有找到合适的 chunk,则 malloc
会先在 unsorted bin 中查找合适的空闲 chunk
Small bins 和 Large bins
小于 512 字节的 chunk 被称为 small chunk,大于等于 512 字节的被称为 large chunk
在前两种特殊的缓冲区 bin 之后就是普通的 bin,普通的 bin 分为 Small bins 和 Large bins
在这两种 bin 中,两个毗连的空闲 chunk 会被合并成一个空闲 chunk
- 前 64 个为 Small bins(每个 bin 以 8 字节为单位递增)
- 在 Small bins 后面就是 Large bins,共有 63 个
三种特殊的 chunk
有三种特殊的 chunk:
- top chunk:分配区的顶部空闲内存,每次
malloc()
不能从 bin 找到合适块时,会尝试从 top chunk 分割内存 - mmaped chunk:
mmap
分配的内存 - last remainder chunk:一种优化方法,当从 unsorted bin 或 large bin 找到一个比需要的 malloc 大的块时,需要拆分(split),多余部分叫 “remainder”,并作为 last remainder 暂存。下一次
malloc
时会优先尝试使用这个块,避免插回 bin 再取出来