IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 第二講:std::allocator -> 正文阅读

[C++知识库]第二講:std::allocator

第二講:std::allocator

標準庫的興起,意味我們可以擺脫內存管理的繁複瑣屑,直接使用容器。但是容 器背後的分配器(allocator)攸關容器的速度效能和空間效能。我將比較 Visual C++, Borland C++, GNU C++標準庫中的 allocator,並深入探索其中最精巧的 GNU C++ allocator 的設計。

西北有高楼,上与浮云齐

image-20211201201707350

G4.9 std::allocator 标准分配器之实现

和以前的分配器一样 用 operator::new 和 operator::delete 完成分配

image-20211201205507982

G4.9 pool_allocator

用了vector的分配器 —— 省掉了cookie

image-20211201205335908

G2.9 std::alloc 运行模式

std::alloc里的操作是,先创出一个可放置16种大小【以8为倍数关系】的总记录链表,即8~128的16种大小。超出这个大小范围的则std::alloc不为其服务分配内存【每一大段带cookie,小段共享所处大段的cookie】,而是调用malloc去分配内存【每个内存都带cookie】

image-20211202124419715

#0记录大小为8的数据类型,
#1记录大小为16的数据类型 …
#15记录大小为128的数据类型,

  • 如果此时需在容器vector中放入一个类型大小为30的元素,则编译器会将其大小调节为8的倍数即调整到32而放入 #3 所malloc出的一大段free list的第一小片中,且此时free list1的头指针向下移一位,则剩下19小段是空的。而在这 #3下所malloc分配得的一大块内存中是带有头尾两个cookie的,然后其实际生成大小为32 * 20 * 2,前20个用来存放32大小的元素数据,而后一半则作为战备池提供后续放置元素使用。
  • 比如,继续在容器list中放入一个类型大小为63的元素,则编译器会将其大小调节为8的倍数即调整到64而放入 #7 中,但 #7下并无挂free list,所以跑到前面 #3 提供的战备池去分配空间。此时战备池大小为32 * 20 =640,所以放置64大小的元素时,可以分成640/64=10块,则此时战备池 被划分为10小段,第一段已用来放置元素对象,free list2的指针向下移一位,且剩下9小段是空的。
    若此时容器set需要放入一个类型大小为256的元素,则此时已不属于std::alloc的服务范围,则调用的是malloc去分配内存空间。

embedded pointers(内嵌指针)

image-20211202125025670

用共用体 —— 当容器开始在free list中放置元素值时,就会覆盖掉整小片,此时指针信息已经覆盖掉了,而且指向free list头端的指针下移一位跳到下一个空的位置。
这里指向空表free list头端的指针是嵌属于一小片空间的一部分,因而不占用额外的内存空间

具体运行过程

取第8、12张图 作为 经典过程

image-20211202131917027

容器申请放置104bytes的元素,则应放置于#(104/8-1)即#12中,而#12中为空,且pool【战备池】有余量80,但此时pool【战备池】余量已不足放入104大小的元素即不可能分割出片段来放置元素,则此80称为碎片。所以将#0下的pool碎片拨给#(80/8-1)即#9下。然后调用malloc创建出带头尾两个cookie的一大段内存空间,
申请量为 104 * 20 * 2 + RoundUp(5200>>4) = 9688:

104 为按所放置容器的元素的大小而设定,20 * 2则是表示每个最多需要切分成20小段,前半段用于存放操作,而后半段则当作pool【战备池】为后续放置提供操作空间。
RoundUp(5200>>4) 表示为上一次累计申请量除以16所得的大小【还要调整为8的倍数】即5200/16=325->328。
因而所得累计申请量为,且除去容器所分配的内存大小104 * 20 = 2080,
最终pool大小为:9688 - 5200 - 2080 = 2408。

以上为碎片的处理!

image-20211202131305563

容器申请放置72bytes的元素,则应放置于#(72/8-1)即#8中,但#8中已无可用区块,而pool【战备池】的余量8已不足放入72大小的元素即不可能分割出片段来放置元素,则此8称为碎片。所以将#8下的pool碎片拨给#(8/8-1)即#0下。然后 企图 调用malloc创建出带头尾两个cookie的一大段内存空间,需索取申请量为 72 * 20 * 2 + RoundUp(9688>>4) > 10000 了,则此时索取分配新空间失败:

1.则std::alloc从右边最接近其的#9取资源,但#9此时已空,所以又到#10上索取资源。而#10中仍剩16小段空白区块(每个区块大小为88),则返回#10 free list头端空表的第一块【实际上已是链表的第五块】返回容器并放置元素对象,然后此时#10的free list的指针下移一位指向其free list的第6块了。此时这区块划分为88/72=1块,剩余的88-72=16当作pool【这一区块88的大小已由#8接管了!】。
2.#8返回对应地址给容器并放置元素对象,#8的指针也下移一位直接跳到pool的头端位置即start_free。
3.此时累计申请量仍为9688,pool剩余大小为 16。

G2.9 std::alloc 源码剖析

第一级分配器

在G2.9版本中有第一级分配器,而G4.9中则已经无了:其作用主要是当主要的第二级分配器分配失败时,则调用第一级分配来进行分配内存。

第一级模拟 new handler,G4.9 C++ 用本身设置的 new handler

第一级分配器

image-20211202140108619

第二级分配器

enum {__ALIGN = 8};  // 小区块的上调边界,即为8的倍数【如#0与#1间存放的元素类型大小相差8】
enum {__MAX_BYTES = 128} // 小区块的上限
enum {__NFREELISTS = _MAX_BYTES/__ALIGN}; // free list个数

// 以下两个template参数完全没派上用场
template<bool threads, int inst>
class __default_alloc_template{
private:
	static size_t ROUND_UP(size_t bytes){  // 将传入大小上调为8的倍数,即传入bytes为 13 则传入为 16!
		return((bytes) + __ALIGN - 1) & ~(__ALIGN - 1); // 即(13+7) & ~(7) --> 10100&11000得10000即16
	}
private:
	union obj{
		union obj* free_list_link;  // 实际上为前面说的next指针
	}; // 改用 struct 亦可
private:
	static obj* free_list[__NFREELISTS];  // 创建一个记录16个编号的链表的指针数组,即存放数组内部的是指针元素。free_list则表示这个数组的起始地址即编号链的起始指针
	static size_t FREELIST_INDEX(size_t bytes){ // 求取所放入bytes大小类型的元素应该放在哪个编号内
		return (((bytes) + __ALIGN - 1)/__ALIGN - 1); 
		// 若bytes为8,则(8+7)/8-1=0
		// 若bytes为16,则(8+7)/8-1=1
		// 若bytes为20,则(20+7)/8-1=2
		// 若bytes为24,则(24+7)/8-1=2
	}
	
	// 若所申请放入的编号链表为空,则进行创建
	static void* refill(size_t n);

	// 在上面refill的创建过程中需要分配一大块内存进行操作切分
	static char* chunk_alloc(size_t size, int &nobjs);
	
	// 战备池
	static char* start_free; // 指向pool的头
	static char* end_free; // 指向pool的尾
	static size_t heap_size; // 分配累计量
public:
	static void* allocate(size_t n){ // n必须大于0
		obj* *my_free_list; // obj**,这是指向编号链对应编号位置的指针变量
		obj* result;  // 指向free list空表第一个空位置的指针变量

		if(n > (size_t)__MAX_BYTES){ // 申请放入元素类型大小超出128,则改用第一级分配器
			return(malloc_alloc::allocate(n));
		}
		my_free_list = free_list + FREELIST_INDEX(n); // 指向编号链起始端的指针 + 申请放置元素应落在的编号 = 对应的编号位置(其解引用则是取出对应编号位置下的free list的第一个空位置)
		result = *my_free_list; // 编号链的对应编号位置下指向其free list的空表第一个空位置的指针
		if(result == 0){ // 若编号链某编号位置下的free list为空,则调用refill进行填充
			void* r = refill(ROUND_UP(n));
			return r;
		}
		// 若编号链某编号位置下的free list不为空,
		*my_free_list = result->free_list_link; // 则移动对应编号位置下的free list的第一个空位置下移一位
		return (result); // 返回当前free list的第一个空位置
	}
	
	static void deallocate(void* p, size_t n){ // 传入指针不能为空
		// 若传入的指针p,满足其解引用的元素大小在0~128范围内,但p却不是有std::alloc生成的比如是malloc生成的
		// 此时传入这里仍可进行处理,虽然其带有cookie但不影响,只是将指针指向修改以下而已
		// 即此时std::alloc外的指针内存地址只要满足其解引用元素大小在0~128范围内均可被其回收使用,
		// 只不过如果p所指大小不是8的倍数时,可能会导致灾难!!
		obj* q = (obj*)p;
		obj* my_free_list; // obj**
		if(n > (size_T)__MAX_BYTES){ // 传入的元素大小超出128,不属于std::alloc的操作范围,调用的是free
			malloc_alloc::deallocate(p, n); 
			return;
		}
		my_free_list = free_list + FREELIST_INDEX(n);
		q->free_list_link = *my_free_list;  // q->next = freeStore
		*my_free_list = q;					// freeStore = q
	}
};

image-20211202140428171

16根指针 —— 不同大小的内存

2根 —— 指向 pool

image-20211202141424612

volatile关键字的作用

C语言关键字volatile(注意它是用来修饰变量而不是上面介绍的volatile)表明某个变量的值可能在外部被改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新存取。该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程。

C++
volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是易变的,

切割就是把指针所指对象 转为 obj再取其next_obj指针 继续切割 ·一而再三…

chunk_alloc

char* chunk_alloc(size_t size, int* nobjs)
{
  char* result;
  size_t total_bytes = size * (*nobjs);   //原 nobjs 改為 (*nobjs)
  size_t bytes_left = end_free - start_free;

  if (bytes_left >= total_bytes) {
      result = start_free;
      start_free += total_bytes;
      return(result);
  } else if (bytes_left >= size) {
      *nobjs = bytes_left / size;         //原 nobjs 改為 (*nobjs)
      total_bytes = size * (*nobjs);      //原 nobjs 改為 (*nobjs)
      result = start_free;
      start_free += total_bytes;
      return(result);
  } else {
      size_t bytes_to_get =
                 2 * total_bytes + ROUND_UP(heap_size >> 4);
      // Try to make use of the left-over piece.
      if (bytes_left > 0) {
          obj* volatile *my_free_list =
                 free_list + FREELIST_INDEX(bytes_left);

          ((obj*)start_free)->free_list_link = *my_free_list;
          *my_free_list = (obj*)start_free;
      }
      start_free = (char*)malloc(bytes_to_get);
      if (0 == start_free) {
          int i;
          obj* volatile *my_free_list, *p;

          //Try to make do with what we have. That can't
          //hurt. We do not try smaller requests, since that tends
          //to result in disaster on multi-process machines.
          for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
              my_free_list = free_list + FREELIST_INDEX(i);
              p = *my_free_list;
              if (0 != p) {
                  *my_free_list = p -> free_list_link;
                  start_free = (char*)p;
                  end_free = start_free + i;
                  return(chunk_alloc(size, nobjs));
                  //Any leftover piece will eventually make it to the
                  //right free list.
              }
          }
          end_free = 0;       //In case of exception.
          start_free = (char*)malloc_allocate(bytes_to_get);
          //This should either throw an exception or
          //remedy the situation. Thus we assume it
          //succeeded.
      }
      heap_size += bytes_to_get;
      end_free = start_free + bytes_to_get;
      return(chunk_alloc(size, nobjs));
  }
}

G2.9 std::alloc观念大整理

image-20211202145949640

Foo(1)是在stack(栈)中创建出来的临时对象,不带cookie的。由于这个Foo类对象的大小是在0~128范围内的,所以调用std::alloc去申请放入对应编号区下的free list的内存空间。另外由于该内存空间是通过alloc获得的,所以是不带cookie的。

*p是在heap(堆)中创建出来的对象,带cookie的。调用std::alloc去申请放入对应编号区下的free list的内存空间。另外由于该内存空间是通过alloc获得的,所以是不带cookie的【虽然copy进去的*p带cookie】。记住new出的东西,最终要用delete来释放掉内存空间。

示例 —— 元素类型不符

vector<A> a;
a.push_back(new A());

image-20211202150419809

G4.9 pool allocactor 运行观察

通过重载全局的 ::operator new

image-20211202151545638

检讨

image-20211202152606548

answer:

在申请索取量失败时,不去尝试将索取量折半再进行索取而最大程度利用完剩余空间。因为一般场景下,都是多线程的机制,这样子操作会占用了更多的内存空间,会造成对其他线程有影响!
而且,通过上面的操作原理可以知道,调用std::alloc去申请分配内存空间使用时,实际往往各编号区下的free list仍有很多容量【即各free lists链上的小区块都是没被放置元素对象的】,这在std::alloc分配失败时,会达到一个分配内存空间的峰值!!此时通过调用std::alloc操作申请分配内存空间其本身就已经占用了很多内存空间了,若仍采取检讨2的做法,则让其他线程下的可利用内存空间愈加少了!!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-12-04 13:12:48  更:2021-12-04 13:14:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:46:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码