内存分配器

article/2025/10/1 7:26:04

内存分配器(Memory Allocator)负责内存分配与管理。内存分配器是所有容器的基础,它总是隐藏在容器背后工作,因此内存分配器对实现者非常重要,而对应用者几乎可以忽略。内存分配器分为两级,第一级分配器直接调用C函数分配和释放内存,第二级分配器则采用内存池来管理内存。如果申请的内存块足够大,那么启动第一级分配器,否则启动第二级分配器。这种设计的优点是可以快速分配和释放小块内存,同时避免内存碎片;缺点是内存池的生命周期比较长,并且很难显式释放。

第一级分配器只是简单的调用malloc()、realloc()和free()来分配、重新分配和释放内存。

第二级分配器需要维护16个空闲块链表和一个内存池。每个链表中的空闲块的大小都是固定的,假定默认内存齐数为n,各个链码空闲块大小则依次为n、2n、3n、4n、5n、6n、7n、8n、9n、10n、11n、12n、13n、14n、15n、16n。内存池由两个指针来描述,free_start记录起始地址,free_end记录结束地址。另外两个变量heap_size和used_size分别纪录堆的大小和已用内存大小。

图 1‑1第二级分配器

一 结构定义

由于内存分配器的所有变量都被定义为静态成员变量,因此所有对象都是同一个内存分配器。为了解决某些应用场景下需要创建多个不同的内存分配器问题,借鉴了SGI STL的实现技巧,增加了一个非类型参数inst。不同inst参数的内存分配器虽然在功能上完全一样,但会被认为是不同的类型,从而达到创建多个内存分配器的目的。一个典型的应用场景就是保证标准库容器的线程安全。虽然也通过线程同步机制解决标准库容器的线程安全问题,但是使用非类型参数显然更简洁和高效。

 为了维护一个链表,每一个节点都需要额外的指针来指向下一个节点。为了避免这种额外的内存开销,这里将节点定义为联合体,既可以将其视为指针也可以将其视为内存块来使用。

template <int inst>
class allocator_base
{
private:union free_chunk_node{free_chunk_node* next;char             buffer[1];};
private:static constexpr size_t align = 8;static constexpr size_t max_bytes = align << 4;static constexpr size_t free_list_size = 16;static constexpr size_t chunk_count = 16;static constexpr size_t heap_threshold = (max_bytes * chunk_count) << 5;static size_t           heap_size;static size_t           used_size;static char*            free_start;static char*            free_end;static void*            memory_list;static free_chunk_node* free_list[free_list_size];
};

二 向系统申请内存

由于向系统申请内存空间是有额外负担的,并且申请的内存空间越小,这种额外负担的比例就越大。为了减少不必要的开销,内存分配器所采取的内存分配策略如下:初始阶段,每次向系统申请的内存大小为所需内存大小的两倍;当内存总容量足够大的时候,每次按照总容量的1/16递增;如果系统无法提供足够的内存时,请求空间大小减半;若仍无法申请成功,则再循环上述过程,直到无法满足所需大小,分配失败。

char* chunk_alloc(size_t align_bytes, size_t& count)
{char* result = nullptr;size_t memory_size;size_t total_bytes = align_bytes * count;if (heap_size < heap_threshold)memory_size = total_bytes << 1;elsememory_size = (heap_size >> 7) << 3;while (memory_size >= total_bytes){void* p = malloc(memory_size + sizeof(void*));// std::cout << "malloc: ";// std::cout << std::hex << std::setiosflags(std::ios::showbase);// std::cout << reinterpret_cast<size_t>(p) << std::endl;if (p != nullptr){result = reinterpret_cast<char*>(reinterpret_cast<size_t>(p) + sizeof(void*));free_start = result + total_bytes;free_end = result + memory_size;heap_size += memory_size;*reinterpret_cast<void**>(p) = memory_list;memory_list = reinterpret_cast<void**>(p);break;}memory_size >>= 1;}if (result == nullptr){for (size_t size = align_bytes; size <= max_bytes; size += align){free_chunk_node** current_list = reinterpret_cast<free_chunk_node**>(get_free_list(size));if (*current_list != nullptr){count = 1;result = (*current_list)->buffer;free_start = result + align_bytes;free_end = result + size;break;}}}return result;
}

内存池向系统申请的内存空间,在使用过程中会被划分为更小的内存块,而这些小内存块的使用和归还几乎是随机的。如果试图对这些小内存块进行合并和释放,其高昂的代价会大幅降低内存池的性能。但在内存池的已用内存大小为0时,释放内存是安全的。内存分配器维护一个指针链表,用于内存空间的统一释放。

三 分配内存

当所需内存块大于16n时,使用第一级分配器进行内存分配,否则使用第二级别分配器,具体步骤如下:

  1. 申请内存的大小上调至n的倍数,根据所需内存块的大小查找对应的空闲链表;
  2. 如果空闲链表中有可用的内存块,则直接返回此空闲块,并从空闲链表中删除该块,否则继续下面的步骤;
  3. 计算内存池中所剩空间的大小,如果足以分配16个内存块,则从中取出16个内存块,调整内存池起始地址,返回第一个内存块,并将剩余的15个块并入空闲链表,否则继续下面的步骤;
  4. 如果剩余空间足以分配至少1个内存块,则从中取出尽可能多的内存块,调整内存池起始地址,返回第一个内存块,并将剩余的内存块并入空闲链表,否则继续下面的步骤;
  5. 如果内存池中还有一些内存,则将剩余空间并入其对应大小的空闲链表中;
  6. 向系统申请一个较大的内存块,如果申请成功,返回第一个内存块,调整内存池起始地址,否则继续下面的步骤;
  7. 遍历空闲链表,如果存在更大的空闲内存块,则从空闲链表中删除该块,返回该块首地址,并将剩余的部分内存交给内存池管理,否则分配失败。
void* arw_allocate(size_t size)
{if (size > max_bytes){void* result = malloc(size);if (result == nullptr)throw std::bad_alloc();return result;}else{char* result = nullptr;size_t count = chunk_count;free_chunk_node** current_list = reinterpret_cast<free_chunk_node**>(get_free_list(size));if (*current_list != nullptr){result = (*current_list)->buffer;*current_list = (*current_list)->next;}else{size_t align_bytes = round_up(size);size_t total_bytes = align_bytes * count;size_t free_bytes = free_end - free_start;if (free_bytes >= total_bytes){result = free_start;free_start += total_bytes;}else if (free_bytes >= align_bytes){count = free_bytes / align_bytes;total_bytes = align_bytes * count;result = free_start;free_start += total_bytes;}else{if (free_bytes > 0){free_chunk_node** free_list_left = reinterpret_cast<free_chunk_node**>(get_free_list(free_bytes));reinterpret_cast<free_chunk_node*>(free_start)->next = *free_list_left;*free_list_left = reinterpret_cast<free_chunk_node*>(free_start);free_start = free_end;}result = chunk_alloc(align_bytes, count);}if (result != nullptr && count > 1){char *cur, *next = result + align_bytes;*current_list = reinterpret_cast<free_chunk_node*>(next);for (size_t i = 2; i < count; ++i){cur = next;next += align_bytes;reinterpret_cast<free_chunk_node*>(cur)->next = reinterpret_cast<free_chunk_node*>(next);}reinterpret_cast<free_chunk_node*>(next)->next = nullptr;}}if (result != nullptr)used_size += size;elsethrow std::bad_alloc();return reinterpret_cast<void*>(result);}
}

四 释放内存

当释放内存块大于16n时,使用free()释放内存,否则将该内存块归还给空闲块链表,供以后再次使用。

void arw_deallocate(void* p, size_t size)
{if (size > max_bytes){free(p);}else{free_chunk_node** current_list = reinterpret_cast<free_chunk_node**>(get_free_list(size));reinterpret_cast<free_chunk_node*>(p)->next = *current_list;*current_list = reinterpret_cast<free_chunk_node*>(p);used_size -= size;}
}

 五 重新分配内存

当所需内存块大于16n时,使用realloc()重新分配内存,否则重新分配一块新内存,并将原有数据拷贝到新内存中,并删除原内存。

void* arw_reallocate(void* p, size_t old_size, size_t new_size)
{void* result = nullptr;size_t copy_size;if (old_size > max_bytes && new_size > max_bytes){result = realloc(p, new_size);}if (round_up(old_size) == round_up(new_size)){result = p;used_size -= old_size;used_size += new_size;}else{result = arw_allocate(new_size);if (result == nullptr){copy_size = new_size > old_size ? old_size : new_size;std::memcpy(result, p, copy_size);arw_deallocate(p, old_size);}}if (result == nullptr)throw std::bad_alloc();return result;
}

六 释放内存池 

对于第二级分配器而言,arw_deallocate()只是将内存块归还给空闲块链表,并没有真正释放。在使用过程中,内存池大小只会增大而不会减小,这是为了追求效率所采用的内存管理机制所决定的。但在已用内存大小为0时(如:进程结束前),可以安全地释放内存池。

void release(void)
{if (used_size != 0)return;while (memory_list != nullptr){void* next = *reinterpret_cast<void**>(memory_list);// std::cout << "free: ";// std::cout << std::hex << std::setiosflags(std::ios::showbase);// std::cout << reinterpret_cast<size_t>(memory_list) << std::endl;free(memory_list);memory_list = next;}heap_size = 0;used_size = 0;
}

 


http://chatgpt.dhexx.cn/article/GvsgUGqH.shtml

相关文章

小笔记——内存控制器

内存控制器 what is memory controller? 内存控制器是一个用于管理与规划从内存到CPU间传输速度的总线电路控制器。 工作方式 内存控制器控制着必要的逻辑读写DRAM&#xff0c;每隔一段时间刷新动态随机存取存储器&#xff08;DRAM&#xff09;的内容。进行读取和写入动作时&…

DDR内存控制器

DDRDouble Data Rate双倍速率同步动态随机存储器。严格的说DDR应该叫DDR SDRAM&#xff0c;人们习惯称为DDR&#xff0c;其中&#xff0c;SDRAM 是Synchronous Dynamic Random Access Memory的缩写&#xff0c;即同步动态随机存取存储器。而DDR SDRAM是Double Data Rate SDRAM的…

Linux内存控制器(二)

1. memcg_stock_pcp // 每处理器记账缓存一次从内存控制组批量申请32页, 然后把内存控制组的内存使用量加上32页 #define CHARGE_BATCH 32U // 在内存控制组记账(charge)时, 先查看当前处理器的memcg_stock_pcp // 如果memcg_stock_pcp保存的内存控制组(memcg_stock_pcp->c…

DDR控制器

SCL&#xff1a;Self-Calibration Logic&#xff0c;通过寄存器编程方式实现DDR物理层信号校准的逻辑&#xff0c;这部分逻辑全部由硬件实现&#xff0c;软件需要在物理层自动校准之前对寄存器进行初始化。 SDRAM接口宽度在保持相同速率的前提下&#xff0c;可以采用全宽、半宽…

XLINX系列之Zynq-7000系列DDR内存控制器详解

1DDR内存控制器介绍 DDR内存控制器支持DDR2&#xff0c;DDR3&#xff0c;DDR3L和LPDDR2设备&#xff0c;包括三个主要块&#xff1a;AXI存储器端口接口&#xff08;DDRI&#xff09;&#xff0c;带有交易调度器&#xff08;DDRC&#xff09;的核心控制器和具有数字PHY&#xf…

3. 内存控制器与SDRAM

内存控制器&#xff08;内存接口设备&#xff09; 地址处于不同的范围&#xff0c;会发出不同的片选引脚&#xff0c;换句话说&#xff0c;SOC外接的不同内存芯片&#xff0c;会有不同的地址范围。 CPU统一编址包括GPIO&#xff0c;各种协议类接口控制器&#xff08;UART&…

存储控制器

存储控制器是按照一定的时序规则对存储器的访问进行必要控制的设备&#xff0c;包括地址信号、数据信号以及各种命令信号的控制&#xff0c;使主设备(访问存储器的设备)能够根据自己的要求使用存储器上的存储资源。 存储控制器的作用主要就是进行接口的转换&#xff0c;将主设…

内存控制器

1.内存控制器&#xff08;Memory Controller&#xff09; 内存控制器&#xff08;Memory Controller&#xff09;是计算机系统内部控制内存并且通过内存控制器使内存与CPU之间交换数据的重要组成部分。内存控制器决定了计算机系统所能使用的最大内存容量、内存BANK数、内存类型…

Java并发包中常用类小结(一)

Java并发包中常用类小结(一) 从JDK1.5以后&#xff0c;Java为我们引入了一个并发包&#xff0c;用于解决实际开发中经常用到的并发问题&#xff0c;那我们今天就来简单看一下相关的一些常见类的使用情况。 1、ConcurrentHashMap ConcurrentHashMap其实就是线程安全版本的has…

java并发包源码分析

java并发包之AbstractQueuedSynchronizer源码分析 分析并发包首先要了解AbstractQueuedSynchronizer&#xff08;AQS&#xff09;&#xff0c;因为AQS是并发包的基础工具类。本文从ReentrantLock的公平锁出发&#xff0c;分析AbstractQueuedSynchronizer的工作过程。 lock与u…

【Java进阶】Java并发包提供了哪些并发工具类?

通过前面的学习&#xff0c;我们一起回顾了线程、锁等各种并发编程的基本元素&#xff0c;也逐步涉及了 Java 并发包中的部分内容&#xff0c;相信经过前面的热身&#xff0c;我们能够更快地理解 Java 并发包。 今天我要问你的问题是&#xff0c;Java 并发包提供了哪些并发工具…

java---JUC并发包详解

目录 前言 一、atomic包 AtomicInteger类 AtomicReference类 AtomicStampedReference类 二、locks包 接口 Condition Lock ReadWriteLock 实现类 ReentrantLock类 ReentrantReadWriteLock类 三、CountDownLatch 四、Semaphore(信号量) 总结 前言 JUC是java.u…

Java多线程并发编程--Java并发包(JUC)

Java多线程并发–Java并发包&#xff08;JUC&#xff09; 前言 前一篇文章中&#xff0c;笔者已经介绍了Java多线程的一些基础知识&#xff0c;但是想要成为一名中高级Java程序员还必须懂得Java并发包&#xff08;JUC&#xff09;的知识点&#xff0c;而且JUC现在也是面试中必…

接口测试--apipost如何自定义header中的content-type

使用apipost进行接口测试的时候&#xff0c;有时候会用到一些自定义或者不常见的content-type格式&#xff0c;这个时候就要手动在header头部自定义content-type。 这里我们自定义一个content-type&#xff0c;格式为application/octet-stream 然后body选择的为form-data&…

Type-c引脚定义

Type-c口是什么口&#xff0c;有什么作用&#xff1f; Type-c口在大家的视野中或许比较陌生&#xff0c;但是生活中处处离不开Type-c口的存在。手机&#xff0c;电脑&#xff0c;音箱&#xff0c;小家电&#xff0c;无人机…等等&#xff0c;都存在Type-c接口。 Type-c只是一种…

TYPE-C12PIN接口电路

16PIN封装只有12个PIN脚&#xff0c;所有16/12PIN其实是一种规格 特点&#xff0c;正反插 VBS-VCC A5,B5接电阻拉地 A7,B7串联接电阻成为DN A6,B6串联接电阻成为DP GND拉地 外壳拉地 USB接口介绍

TypeScript-interface接口篇

简介 接口的作用&#xff1a;在面向对象的编程中&#xff0c;接口是一种规范的定义&#xff0c;它定义了行为和动作的规范&#xff0c;在程序设计里面&#xff0c;接口起到一种限制和规范的作用。接口定义了某一批类所需要遵守的规范&#xff0c;接口不关心这些类的内部状态数…