0分配不到地址_ncnn初探一: 内存的分配和释放

本文介绍了ncnn库中内存对齐分配的方式,通过ncnn::fastMalloc和ncnn::fastFree实现内存16字节对齐。详细解释了额外分配空间和调整起始地址的原因,帮助理解内存管理机制。
摘要由CSDN通过智能技术生成

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
  • 上面就是我把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
  • 到这里我们我们就讲完了, 内存按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.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值