Keep simple Keep easy

本文详细介绍了simple.base库的基础数据结构,包括Image和Tensor,以及它们的内存布局、对齐和内存池管理。重点讲解了内存对齐的必要性和规则,以及内存优化的思考,为深度学习部署中的内存效率提升提供了技术细节。
摘要由CSDN通过智能技术生成


simple.base 地址

simple.base基础数据介绍

simple.base是一个基础库,为simple系列提供类两大数据结构Image和Tensor。

Image数据结构成员

  • number_:内存块中图片的数量,是一个整数类型
  • width_ :图片的宽,矩阵的列数量,是一个整数类型
  • height_ :图片的高,矩阵的行数量,是一个整数类型
  • channel_:图片的通道数,是一个整数类型
  • stride_ :内存块的步长,是一个整数类型
  • data_manager_:内存块的地址分配器,支持内存池分配

Image数据结构成员

  • shape_:tensor的shape,支持四维度
  • shape_mode_:tensor的内存布局,支持NCHW/NHWC等
  • elem_type_:tensor数据类型,fp32/int8等
  • mem_type_:内存的类型,如CPU/GPU/OCL等
  • data_manager_:内存块的地址分配器,支持内存池分配

Image/Tensor支持四维,data内存块地址对齐,支持内存复用,在simple.base/include/manager/data_manager.h中的alingn_ptrd、fast_malloc和fast_free三个静态函数进行内存的分配对齐,如下:

static inline u_char_ptr* align_ptr(u_char_ptr* ptr, int n /*(int)sizeof(u_char_ptr)*/) {
    return (u_char_ptr*)(((size_t)ptr + n - 1) & -n); }

static inline size_t align_size(size_t sz, int n) {
    return (sz + n - 1) & -n; }

static inline void* fast_malloc(size_t size) {
    uint8_t* udata = (uint8_t*)malloc(size + sizeof(void*) + MALLOC_ALIGN + MALLOC_OVERREAD);
    if (!udata) {
        return 0;
    }
    uint8_t** adata = align_ptr((uint8_t**)udata + 1, MALLOC_ALIGN);
    adata[-1]       = udata;
    return adata; }

static inline void fast_free(void* ptr) {
    if (ptr) {
        uint8_t* udata = ((uint8_t**)ptr)[-1];
        free(udata);
    } } 
  1. 内存的分配
uint8_t* udata = (uint8_t*)malloc(size + sizeof(void*) + MALLOC_ALIGN + MALLOC_OVERREAD);

(1)为什么要多分配sizeof(void*)的内存?

用来保存malloc后的地址;

(2)为什么要多分配MALLOC_ALIGN+MALLOC_OVERREAD的内存?

对齐单位是MALLOC_ALIGN,数据指针不一定指在分配的内存的起始位置,为了保证对其后仍然有目标内存可用,要多分配MALLOC_ALIGN,MALLOC_OVERREAD一般可设为0,可以作为拓展指针来用;

  1. 内存的对齐
uint8_t** adata = align_ptr((uint8_t**)udata + 1, MALLOC_ALIGN);

(1)为什么对齐的地址输入(uint8_t**)+1?

udata是uint8_t指针类型,目的是要预留8个字节(64位系统,32位系统为4)的内存用来存放udata的地址,假设udata的地址为0x30,udata+1则会0x31并没有达到8个字节的内存,故将udata转为指针的指针加1才会出现sizeof(void*)大小的内存0x38;

(2)如何实现偏移?

(u_char_ptr*)(((size_t)ptr + n - 1) & -n);

目的:让目标内存的地址指向n的倍数,假设n为16,16=00010000,-16=11110000。目标数字按位与后,自然就是一个能被16整除的数字。如下图中adata之后的内存才是真正用户使用的内存

内存分配图

simple.base内存池介绍

预先在内存中申请一定数量的内存块留作备用,当需要新的内存分配时,就先从内存池中分配内存返回,在释放的时候,将内存返回给内存池而不是操作系统,在下次申请的时候,重新进行分配。

  • current_size_:记录当前内存池中一共分配的字节大小
  • pool_:存储内存的map表
  • last_id_ :上一个内存块分配的ID
  • capacity_ :内存池的容量,可动态改变
  • max_expand_times_ :最大扩容次数
  • expand_times_ :扩容的次数
  • unused_timeout_ :内存块的空闲时间
  • mutex_:互斥锁

simple.base中的内存池有严格的限制,移动端的内存大小有限,如不能连续扩容最大扩容限制次数等限制条件,具体实现在simple.base/src/data_manager.cc中,如下内存块的设计:

class DataBlock final {
public:
    DataBlock(const std::shared_ptr<DataManager>& data_ptr, bool in_use) : data_ptr_(data_ptr) {
        SetState(in_use);
    }

    std::shared_ptr<DataManager>& GetData() { return data_ptr_; }
    void SetState(const bool in_use) {
        in_use_     = in_use;
        time_stamp_ = TimeStamp();
    }
    bool IsUsing() const { return in_use_; }
    TimeStamp GetLastTimeUpdated() const { return time_stamp_; }

private:
    bool in_use_;                           // 是否正在使用
    TimeStamp time_stamp_;                  // 分配内存块的时间戳
    std::shared_ptr<DataManager> data_ptr_; // 内存分配器
};

1. 设计思路
simple.base的设计类似“三级缓存”的机制,第一级的按照内存的类型如CPU/GPU/OCl等进行查询,第二级按照内存块的字节大小进行查询,第三级按照真正的内存块的ID进行查询,如下图:
内存池2. 内存池分配内存的步骤

  1. 判断是否内存回收
  2. 判断是否存在扩容的需要,线程池容量每次扩容都是乘以2倍
  3. 查找是否存在相同的内存类型,有则进行3,没有则分配类型内存+字节大小+Id,返回分配的内存地址
  4. 在3的基础上,继续查找是否存在相同的大小的内存块,有则进行5,没有则分配该大小的字节内存,返回分配的内存地址
  5. 在4的基础上,继续查找是否存在相同ID的内存块,有则判断是否在使用,如若在使用,需要分配另外ID的内存块,如果不再使用,则直接返回该内存地址
  6. 判断是否存在缩容的需要,线程池容量每次扩容都是除以2倍

simple.base线程池介绍

待更新…


补充一:内存对齐

1. 内存对齐的必要性

提高内存访问效率:由于CPU在访问内存时,一次性会以固定的长度(4/8字节)读取数据,如果访问非连续内存或者未对齐的内存时,会出现“半包”的现象从而加大CPU读取数据的时间和效率;

现在假设一个整型变量(4字节)不是自然对齐的,它的起始地址落在0x2(图中蓝色区域),CPU想要访问值时,按照4字节的块进行读取,从图中的0x0起读,读取4字节大小,读到0x3
内存对其
这样的一次读取之后,我们并不能取到我们要访问的整型数据,紧接着CPU会继续再往下读,偏移4个字节,从0x4开始,读到0x7,CPU用两次的访问时间来读取4字节的数据,期间CPU还要对两次读取的数据进行裁剪和拼接,才能完成数据的读取,如若读取的地址进行对齐后,一次性即可读取4字节的数据;

2. 对齐规则

内存地址满足CPU一次性读取长度的整数倍;有效的内存对齐可以减少内存的占用,如下

struct Base {
  int a;
  long b;
  short c;
};

第一个int a占用4个字节,占用分布0x0 ~ 0x3;
结构体a第二个long b,占8字节,此时内存首地址的偏移量0x4不是8的整数倍,要填充字节到0x7的位置,然后将long类型的数据b写入内存,占用分布0x8 ~ 0x16;

结构体b第三个short c,占2个字节,此时内存首地址0x16是2的整数倍,所以直接写入内存;
结构体c到此结构体内的数据数据成员已对齐,但是当前结构体的总大小为18,不满足结构体的总大小为最大对齐数的整数倍,编译器会在最末一个成员之后加上填充字节0x18 ~ 0x23,使其总大小为24。

Base base;
// sizeof(base)= 24 

总结下规则:
(1)各成员变量存放的起始地址,相对于结构的起始地址的偏移量,必须为该变量的类型所占用的字节数的倍数;
(2)各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节自动填充同时为了确保结构的大小为结构的字节边界数的倍数

3. 如何在代码体现对齐

// 1. 预编译的方式进行对齐规则,对于小于n的字节数,则不受pragma中n的影响
#pragma pack(n)
// 或者写成如下方式
/*
#pragma pack(push,n)
#pragma pack(pop)
*/

// 2. 结构成员对齐在n字节。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐
__attribute__((aligned (n)))
__attribute__((packed)) // 取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐

// 3. 自定义对齐规则,参考simple.base中fastMalloc函数

补充二:内存优化的思考

内存优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值