起因是有群友问了一句“deallocate的底层机制是啥”,正好我也不知道,就开始翻C++的源码,看看这个deallocate函数是怎么实现的,下面来总结一下大概的机制和其余未解决的问题。
一、示例代码
先贴出一段示例代码,来自于《C++ Primer 5th》的练习12.2.2,其实就是一个简单的构造-销毁的程序,只不过从new-delete换成allocator实现:
#ifndef LOG(x)
#define LOG(x) std::cout << x << std::endl;
#endif // LOG(x)
void solution
{
using string = std::string;
size_t size = 10; // 分配10个string的空间
std::allocator<string> a;
string* p = a.allocate(size); // return T* 指向内存开始的地址
string s;
string* q = p;
while (std::cin >> s && q != p + size && s != "") {
a.construct(q++, s); // constuct, then copy
}
const size_t count = q - p;
LOG(count);
// free memory space
while (q != p) {
a.destroy(--q);
}
a.deallocate(p, size);
}
二、底层探究
使用 ctrl+左键单击 跟踪deallocate这个函数到内部的实现,即xstring
文件,查找到这个函数的实现如下,对于这些代码我已经做了一些粗略的注释:
_CONSTEXPR20 void deallocate(_Ty* const _Ptr, const size_t _Count) {
// 检查传入的参数,防止释放空地址和0长度内存
_STL_ASSERT(_Ptr != nullptr || _Count == 0, "null pointer cannot point to a block of non-zero size");
// no overflow check on the following multiply; we assume _Allocate did that check
// 译:这里不检查内存溢出的情况,该函数会假定_Allocate分配内存的时候已经做过了
_Deallocate<_New_alignof<_Ty>>(_Ptr, sizeof(_Ty) * _Count);
}
其中, _Ptr
就是我们传入的指针,sizeof(_Ty) * _Count
计算的就是占用内存空间的大小。
对于_New_alignof<_Ty>
,在xstring
中是这样定义的:
template <class _Ty>
_INLINE_VAR constexpr size_t _New_alignof = (_STD max)(alignof(_Ty), __STDCPP_DEFAULT_NEW_ALIGNMENT__);
该语句会返回一个size_t
类型,表明该类型_Ty
应该在内存中对齐的大小,这是为了加快CPU访问缓存的速度,更多内容详见:内存对齐
所以对于
_Deallocate<_New_alignof<_Ty>>(_Ptr, sizeof(_Ty) * _Count); // 对于示例代码,_Ty是std::string类型,count是分配的string数量,即size
我们暂且可以先看作
_Deallocate<size_t>(_Ptr, sizeof(_Ty) * _Count); // size_t 是应该对齐的大小
接下来看_Deallocate
模板,该模板在xstring
中定义如下:
_CONSTEXPR20 void _Deallocate(void* _Ptr, size_t _Bytes) noexcept
{
if (_Bytes >= _Big_allocation_threshold) { // boost the alignment of big allocations to help autovectorization
_Adjust_manually_vector_aligned(_Ptr, _Bytes);
}
::operator delete(_Ptr, _Bytes); // 向_Ptr位置释放_Bytes大小的内存空间
}
其实到这里已经能很明显的看出,deallocate
本质上就是根据内存分配的情况,使用delete
关键字对指定地址、指定大小的内存块进行释放操作,不过笔者进一步查看了一下_Adjust_manually_vector_aligned
,有了新的发现,以及更多的疑问。
三、给自己挖坑
我们先查看一下_Big_allocation_threshold
的定义,为了方便起见,我将后续会用到的其它定义也贴在下面:
// 用到的宏定义变量
_INLINE_VAR constexpr size_t _Big_allocation_threshold = 4096;
_INLINE_VAR constexpr size_t _Big_allocation_alignment = 32;
#ifdef _WIN64
_INLINE_VAR constexpr size_t _Big_allocation_sentinel = 0xFAFAFAFAFAFAFAFAULL; // 内存保护字,哨兵; ULL表示unsigned long long
#ifdef _DEBUG
_INLINE_VAR constexpr size_t _Non_user_size = 2 * sizeof(void*) + _Big_allocation_alignment - 1; // 调试模式下
#else // _DEBUG
_INLINE_VAR constexpr size_t _Non_user_size = sizeof(void*) + _Big_allocation_alignment - 1; // 非调试模式下
#endif // _DEBUG
首先,if
分支会判断字节数是否大于一个阈值,如果大于,就采用_Adjust_manually_vector_aligned
来加速分配(这里不确定,看注释应该也是负责内存对齐的一部分):
if (_Bytes >= _Big_allocation_threshold) { // boost the alignment of big allocations to help autovectorization
_Adjust_manually_vector_aligned(_Ptr, _Bytes);
}
_INLINE_VAR constexpr size_t _Big_allocation_threshold = 4096; // 4096字节就是这个阈值
这个阈值为4096
字节(4KB),不同的系统可能会有所不同。如果计算出需要释放的空间大小_Bytes
大于这个阈值,则调用函数_Adjust_manually_vector_aligned
。以下是_Adjust_manually_vector_aligned
的实现,我在代码内做了部分注释,请放心食用:
inline void _Adjust_manually_vector_aligned(void*& _Ptr, size_t& _Bytes) {
// adjust parameters from _Allocate_manually_vector_aligned to pass to operator delete
_Bytes += _Non_user_size;
const uintptr_t* const _Ptr_user = static_cast<uintptr_t*>(_Ptr); // 指针类型强制转换
const uintptr_t _Ptr_container = _Ptr_user[-1]; // 指向传入指针的内存地址之前(-1)的位置
// If the following asserts, it likely means that we are performing
// an aligned delete on memory coming from an unaligned allocation.
_STL_ASSERT(_Ptr_user[-2] == _Big_allocation_sentinel, "invalid argument"); // 内存保护断言
// Extra paranoia on aligned allocation/deallocation; ensure _Ptr_container is
// in range [_Min_back_shift, _Non_user_size]
#ifdef _DEBUG
constexpr uintptr_t _Min_back_shift = 2 * sizeof(void*); // 偏移字节量
// 与编译器的目标平台有关。如果目标平台是32位的,那么sizeof(void*)就是4,如果是64位的,那么sizeof就是8,如果是16位的,就是2
#else // ^^^ _DEBUG / !_DEBUG vvv
constexpr uintptr_t _Min_back_shift = sizeof(void*);
#endif // _DEBUG
const uintptr_t _Back_shift = reinterpret_cast<uintptr_t>(_Ptr) - _Ptr_container;
// _Back_shift: 当前指针指向的地址 与 实际容器的开始地址 之差,即_Ptr需要向后倒退的字节数
_STL_VERIFY(_Back_shift >= _Min_back_shift && _Back_shift <= _Non_user_size, "invalid argument");
// 检查该字节数是否合法,会不会命中非用户态的内存区(?),会不会大于最多倒退的距离
_Ptr = reinterpret_cast<void*>(_Ptr_container);
// 将该指针重新变为void*类型
}
其中比较让我困惑的是_Non_user_size
,该变量出现了多次,但是我不清楚这个变量是否与用户态和内核态有关系(或者高位内存?)。总体看来,这个函数实现的东西其实并不复杂,不过有几个疑点:
_Non_user_size
的主要功能是什么?是否是防止OOM的机制?- 为什么有
_Back_shift
?其主要作用是什么? - 这个函数是怎么加速
allocation
的?
如果有大佬知道这些问题的答案,请私聊我或在评论区内评论,我会及时更新~
本人C++初学者,如果有什么错误,还请各位斧正,感谢阅读到结尾的各位读者~