< NCNN-Lession-6 > 内存管理,allocator的实现
开始
由于在下一节我们要学习最重要的Mat类的实现,所以我们这节要为下节做一下准备.我们这一节学习实现Mat类的一个关键,那就是内存分配方面的实现:allocator.
我们再插一个小红旗(压压惊):
作用
allocator的作用主要是给后面需要用到的mat类中的数据空间分配内存.
其次,出于平台移植安全性和性能的原因,需要对分配的内存进行人工指定字节的内存对齐.
好,下面我们就来看看allocator的实现.
实现
假如说我们不按照主动对齐的方式来实现一个内存分配函数(尽管编译器可能会为我们做内存对齐的事情),我们可以用如下来实现:
1 | static inline void* fastMalloc(size_t size) { |
但是,如果我们要自定义内存对齐的话(自定义对齐内存的位数),我们就需要换一种方式来实现.
首先,一段内存n位对齐可以这样理解:这个内存的首地址可以被n整除.那么为什么要内存的首地址被n整除呢?因为CPU就是按照这样来存取内存里面的数据.具体n为多少合适,这个和cpu支持多少位有关.
所以,假如说我们申请的内存首地址不能被n整除,我们需要向前移动这段内存的首地址,直到能够被n整除.所以首先,我们需要申请一段移动n之后还能够有足够大小的内存:
1 |
|
这里的MALLOC_ALIGN就是对齐的位数n.
小伙伴可能不理解,都加上了MALLOC_ALIGN了,为什么还要加上sizeof(void*)?这是因为在移动首地址之后,就我们就把本来申请的内存的首地址给丢了,这样在释放的时候就会出现问题,所以我们要在一个地方记录这次这次申请的内存的首地址,如何记住呢,我们往下看.
1 | unsigned char** tmpdata = (unsigned char**)udata + 1; |
我们可以从tmpdata开始移动内存,因为现在tmpdata就是udata移动了一个地址长度(8Byte)的地址.而我们可以把刚开始申请的内存首地址放到移动后的首地址的前一个buffer里面.这样我们就记住了刚开始申请的内存的首地址.
下面就要看这个移动内存首地址的操作是怎么实现的了:
1 | template <typename _Tp> |
我们可以看到,
(size_t)ptr + n - 1的目的是使得你的内存指针向前移动足够用的距离来对齐n位(被n整除),然后和-n进行逻辑与的操作.
由于当n是2的y次幂的时候,-n的二进制表示是前面x位是1后面y位0(如n=16的时候的-n的二进制就是111110000),所以一个数与-n进行与的操作就是这个数的后面的y位被截断了.所以这样的数必对2的y次幂整除,所以也就达到了n为对齐的目的.
其实上述手动n位内存对齐的方法有个替代操作,就是当你的系统是unix的时候,假如你的代码符合POSIX的C标准,你可以通过如下方式来自动对齐内存:
1 | posix_memalign(&ptr, MALLOC_ALIGN, size); |
代码示例
测试程序放到了这里,
代码结构如下:
< NCNN-Lession-6 > 内存管理,allocator的实现