1. 读完本章你可以学到的知识点
- 什么是内存对齐的分配
- 内存对齐分配如何在ncnn里实现: ncnn::fastMalloc, ncnn::fastFree
- 完整可运行简短代码见文末
2. 什么是内存对齐的分配方式?
- 最开始我看到这个问题时, 也是一脸疑惑, 现在我解释一下: 我们知道在任何程序中, 分配的内存肯定是有一个地址的. 现在假设分配了一个对象a, 它在内存的地址为x(指初始地址, 也就是这块内存的头地址, x看成一个整数), x + 1为下一个字节的地址, x + 2为下下一个字节的地址, 依次类推.
- 一般来说内存对齐我们是这样描述的, 内存按8字节对齐的, 内存按16字节对齐的等等, 一句话解释就是: 我们分配的这个地址(转为整数) addr % p == 0(p等于8 / 16 / 等等)
- addr % p == 0, (addr + 2 * p) % p == 0, ..., (addr + k * p) % p == 0
- 因为随意分配的地址可能addr % p != 0, 所以我们一般的做法是先分配一个比申请空间稍大的空间, 然后将这个头地址, 往前移动几个字节, 使得新的addr满足 % p == 0
// 这里我们假设分配的内存空间是16字节对齐的, 下面的代码分配了6 * 4bytes = 24bytes的空间
// 假设分配的这24个字节的空间的头地址为x
// 当我们说分配的内存大小是16字节对齐的, 也就是说x % 16 == 0, 即x是16的整数倍
// 根据简单的数学知识, x + 1, x + 2, ..., x + 15对16取模都是不等于0的
// (x + 1) % 16 != 0, (x + 2) % 16 != 0, ... (x + 15) % 16 != 0
int *p = new int[6];
// 输出地址
size_t x = (size_t)p;
std::cout << x << std::endl;
assert(x % 16 == 0);
- stackoverflow上类似的问题 what-is-meant-by-memory-is-8-bytes-aligned
3. ncnn::fastMalloc, ncnn::fastFree实现
![5de0ca6cb5a6b518e785835a40298ec3.png](https://i-blog.csdnimg.cn/blog_migrate/275bccbf7e53fe069d4c26e81298ed53.jpeg)
- 上面就是我把ncnn里的fastMalloc, fastFree单独拎出来的代码片段
- 第62行是ncnn::fastMalloc的入口, 当我们这样调用时fastMalloc(100), 表示想要申请size = 100字节大小的内存空间. 紧接着看下面一行代码
unsigned char* udata = (unsigned char*)malloc(size + sizeof(void*) + MALLOC_ALIGN);
// sizeof(void*) 等于8字节,
我们明明只申请了size字节大小的空间, 为啥要多分配sizeof(void*)+ MALLOC_ALIGN这么多的空间呢? 理由是为了保存原始地址 + 对齐.
- 现在我们假设申请空间后, 返回的udata表示的地址为x, 也就是头地址.
- 紧接着我们看下一行
unsigned char** adata = alignPtr((unsigned char**)udata + 1, MALLOC_ALIGN);
这里我们把udata强转了, 可能大多数伙伴看到这里都表示很慌, 我当初第一次看到时也是同样的感觉, 不要慌, 我慢慢解释.
// 采用类比的方式解释
unsigned char *a = new char[5];
// a + 1 表示从a指针指向的地址走了1个字节, 为啥是1个字节
// 因为unsigned char *a前面是unsigned char, unsigned char是1个字节
unsigned char **b = (unsigned char **)a;
// 同理, b + 1 表示从b指针指向的地址走了8个字节, 为啥是8个字节
// 因为unsigned char **b前面是unsigned char*, unsigned char*是8个字节
// unsigned char*是个指针类型, 指针类型占8字节的大小
// 总结: 指针的加减 可以看做去掉一个*, 然后看前面的类型占多少字节
// 所以
(unsigned char**)udata + 1
// 表示udata指向的类型为unsigned char*(是一个指针类型占8字节), 再加一, 即udata从指向的地址
// 往前走了8个字节
- udata是一个指针变量, 依旧是一个变量, 这个变量存的数值为size_t x = (size_t)udata, 后面我们统一用x表示
template<typename _Tp>
static inline _Tp* alignPtr(_Tp* ptr, int n = (int)sizeof(_Tp)) {
return (_Tp*)(((size_t)ptr + n - 1) & -n);
}
// 首先我们需要知道_Tp是什么类型, 当然为unsigned char*
// 我们先看(size_t)ptr, 它等于x + 8, 为一个整数另y = x + 8
// (y + n - 1) & (-n), 当n是16时, 这里就表示寻找 大于等于y的且是16的整数倍的最小整数
// 然后我们找到了这个整数, 也就是地址, 这个地址 % 16是等于0的, 也就我们说的内存对于16是对齐的.
// 如果还有点疑惑, 请看下面这个类比
int *a = new int(101);
std::cout << (size_t)(int*)a << std::endl;
std::cout << (size_t)(int**)a << std::endl;
std::cout << (size_t)(char*)a << std::endl;
std::cout << (size_t)(float*)a << std::endl;
std::cout << (size_t)(char**)a << std::endl;
std::cout << (size_t)(float**)a << std::endl;
// 你会发现上面的输出都是一样的, 这当然一样, 因为他们指的都是同一个地址
// 哪里会不同, 当你对他们进行指针加减时, 同样都是+1, 有的指针一下走4个字节, 有的指针一下走1个字节
- 接着看最后一行代码
unsigned char** adata = alignPtr((unsigned char**)udata + 1, MALLOC_ALIGN);
adata[-1] = udata;
// 有了前面的基础, 我们很容易的知道adata[-1] = udata, 就是保存最开始分配内存的头指针
// 为啥要保存呢, 后面释放的时候会用到
![246051084642210688d1e50be465d079.png](https://i-blog.csdnimg.cn/blog_migrate/1fa92bbd9f16872d3d6a70287be3fbd8.png)
- 到这里我们我们就讲完了, 内存按16字节对齐分配是如何在ncnn里实现的
- 最后总结一下就是, fastMalloc分配的空间, 返回的地址是最开始分配的地址x, 然后往前走了一些字节, 这个返回的地址addre % 16 == 0, 最开始分配的地址x, 存放在addre[-1]中
// 有了前面的知识, 释放内存的代码就很显然了
// 把我们原来保存的头地址拿出来, free掉即可.
static inline void fastFree(void* ptr) {
if (ptr) {
unsigned char* udata = ((unsigned char**)ptr)[-1];
free(udata);
}
}
4. Reference:
- https://www.jianshu.com/p/9c58dd414e5f
- What's the purpose of align C++ pointer position
5. 完整代码
/*
* Author : OFShare
* E-mail : OFShare@outlook.com
* Created Time : 2020-12-05 23:59:53 PM
* File Name : main.cc
*/
#include <bits/stdc++.h>
#define ll long long
void debug() {
#ifdef Acui
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif
}
using namespace std;
// the alignment of all the allocated buffers
#define MALLOC_ALIGN 16
// Aligns a pointer to the specified number of bytes
// ptr Aligned pointer
// n Alignment size that must be a power of two
template<typename _Tp>
static inline _Tp* alignPtr(_Tp* ptr, int n = (int)sizeof(_Tp)) {
return (_Tp*)(((size_t)ptr + n - 1) & -n);
}
// Aligns a buffer size to the specified number of bytes
// The function returns the minimum number that is greater or equal to sz and is divisible by n
// sz Buffer size to align
// n Alignment size that must be a power of two
static inline size_t alignSize(size_t sz, int n) {
return (sz + n - 1) & -n;
}
static inline void* fastMalloc(size_t size) {
unsigned char* udata = (unsigned char*)malloc(size + sizeof(void*) + MALLOC_ALIGN);
if (!udata)
return 0;
unsigned char** adata = alignPtr((unsigned char**)udata + 1, MALLOC_ALIGN);
adata[-1] = udata;
return adata;
}
static inline void fastFree(void* ptr) {
if (ptr) {
unsigned char* udata = ((unsigned char**)ptr)[-1];
free(udata);
}
}
int main() {
int *a = new int(5);
std::cout << "a: " << a << std::endl;
std::cout << "(size_t)a: " << (size_t)a << std::endl;
std::cout << "(ll)a: " << (ll)a << std::endl;
size_t totalsize = alignSize(12, 4);
size_t totalsize2 = alignSize(17, 4);
std::cout << "totalsize: " << totalsize << std::endl;
std::cout << "totalsize: " << totalsize2 << std::endl;
void* data = fastMalloc(totalsize);
fastFree(data);
std::cout << "data: " << data << std::endl;
std::cout << "(void*)data: " << (void*)data << std::endl;
std::cout << "(int*)data: " << (int*)data << std::endl;
std::cout << "(float*)data: " << (float*)data << std::endl;
return 0;
}
6. 写在最后
- 你要是觉得有帮助, 就点个赞吧, peace.