深入了解C++ STL中的std::allocator中deallocate函数的作用

起因是有群友问了一句“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++初学者,如果有什么错误,还请各位斧正,感谢阅读到结尾的各位读者~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值