【STL源码剖析】【2、空间配置器——allocator】

文章目录

  • 1、什么是空间配置器?
    • 1.1设计一个简单的空间配置器,JJ::allocator
  • 2、具备次配置力( sub-allocation)的 SGI 空间配置器
    • 2.1 什么是次配置力
    • 2.2 SGI标准的空间配置器,std::allocator
    • 2.2 SGI特殊的空间配置器,std::alloc
    • 2.3 构造和析构的基本工具:construct()和destroy()
      • 补充:
        • 2.3.1、什么是 **trivial destruct:**
        • 2.3.2、从__destroy_aux来看,他的销毁范围是[first,last),为什么没有销毁最后一个
    • 2.4 空间的配置和释放,std::alloc
    • 2.5 第一级配置器 __malloc_alloc_template剖析
    • 2.6 第二级配置器 __default_alloc_template剖析
      • 2.6.2 自由链表自由在哪?又该如何维护呢?
      • 2.6.3 第二级配置器的部分实现内容
    • 2.7空间配置函数allocate()
      • 2.7.1空间配置函数allocate()实现原理:
      • 2.7.2空间配置函数allocate()代码实现:
    • 2.8 空间释放函数 deallocate()
      • 2.8.1空间释放函数 deallocate()实现原理:
      • 2.8.2空间释放函数 deallocate()代码实现:
    • 2.9 重新填充 free lists
      • 2.9.1重新填充 free lists()实现原理:
      • 2.9.2重新填充 free lists()代码实现:
    • 2.10 内存池 memory pool
      • 2.10.1内存池 memory pool实现原理:
      • 2.10.2内存池 memory pool代码实现:
  • 3、内存 基本处理工具
    • 3.1 uninitialized_copy
    • 3.2 uninitialized_fill
    • 3.3 uninitialized_fill_n
  • 4、总结
  • 5、参考

1、什么是空间配置器?

空间配置器是STL(标准模板库)中的一个重要组件,它的主要作用是高效管理各个容器的空间,包括空间的申请与回收。在程序运行时,内存可能存在于栈上(由系统自动分配)或堆上(由用户自行申请)。如果用户频繁地申请和释放空间而不加以管理,可能会导致内存碎片,从而影响内存使用效率并降低程序效率。为了解决这些问题,STL库提供了空间配置器,其原理类似于利用内存池来提高效率。

1.1设计一个简单的空间配置器,JJ::allocator

jjalloc.h

#include "jjalloc.h"
#include <vector>
#include <iostream>
using namespace std;
int main(){int ia[5] = {0,1,2,3,4};unsigned int i;vector<int,JJ::allocator<int>> iv(ia,ia+5);for(i = 0;i < iv.size();i++){cout << iv[i] << " ";}cout <<endl;
}

jjalloc.cpp

#ifndef _JJALLOC_
#define _JJALLOC_#include <new>
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <climits>
using namespace std;namespace JJ {
//分配内存空间来存储指定类型的对象
template <class T>
inline T* _allocate(ptrdiff_t size,T*){//禁用新的处理器set_new_handler(0);  //调用全局的operator new来分配内存T* tmp = (T*)(::operator new ((size_t)(size * sizeof(T))));if(tmp == 0){std::cerr << "out of memory" << endl;exit(1);}return tmp;
}//处理动态分配的内存的释放
template <class T>
inline void _deallocate(T* buffer){::operator delete(buffer);
}//在已分配但尚未构造的内存中构造一个对象。
//它接受两个参数:
//一个指向 T1 类型对象的指针 p 
//一个 T2 类型的常量引用 value
template <class T1,class T2>
inline void _construct(T1*p ,const T2& value){new(p) T1(value);
}//显式调用对象的析构函数来销毁对象,但不释放内存。
//它接受一个指向 T 类型对象的指针 ptr,并调用该对象的析构函数。
template <class T>
inline void _destroy(T* ptr){ptr->~T();
}
//定义allocator的模板类
template <class T>
class allocator{
public://定义类型别名typedef T          value_type;typedef T*         pointer;typedef const T*   const_pointer;typedef T&         reference;typedef const T&   const_reference;typedef size_t     size_type;typedef ptrdiff_t  difference_type;//从一个类型的分配器“重新绑定”到另一个类型的分配器template<class U>struct rebind{typedef allocator<U> other;};// 用于分配内存来存储 n 个 value_type 类型的对象。// 它调用之前定义的 _allocate 函数来执行实际的内存分配,// 并将返回的内存地址转换为正确的指针类型。pointer allocate(size_type n,const void* hint = 0){return _allocate((difference_type)n,(pointer)0);}// 用于释放之前通过 allocate 函数分配的内存。//它调用 _deallocate 函数来执行实际的内存释放void deallocate(pointer p,size_type n){_deallocate(p);}//在已分配但尚未构造的内存中构造一个对象。//它调用 _construct 函数void construct(pointer p,const T& value){_construct(p,value);}//显式调用对象的析构函数以销毁对象,但不释放内存。//它调用 _destroy 函数来执行此操作void destroy(pointer p){_destroy(p);}//返回对象的地址pointer address(reference x){return (pointer) &x;}//返回常量对象的地址const_pointer const_address(const_reference x){return (const_pointer) &x;}//返回分配器可以分配的最大对象数size_type max_size() const{return size_type(UINT_MAX/sizeof(T));}
};// end of allocator   } //end of namespce JJ#endif //_JJALLOC_

2、具备次配置力( sub-allocation)的 SGI 空间配置器

2.1 什么是次配置力

“次配置力”(sub-allocator)这个概念不是C++标准中的术语,但在某些上下文中,特别是在讨论更复杂的内存管理策略时,可能会使用这个概念。次配置器通常指的是用于管理较小内存块或特定类型对象的配置器。在复杂的内存管理系统中,你可能会有一个主配置器(用于处理大块内存分配)和多个次配置器(用于处理不同类型的对象或更小的内存块)。
次配置器的使用可以提高内存管理的效率,因为它们可以针对特定类型的对象或特定大小的内存块进行优化。例如,一个次配置器可能使用内存池技术来快速分配和回收小块内存,而另一个次配置器可能使用特定的对齐策略来优化特定类型对象的存储。

2.2 SGI标准的空间配置器,std::allocator

虽然 SGI 也配置了 allocatalor,但是它自己并不使用,也不建议我们使用,原因是效率比较 感人,因为它只是在基层进行配置/释放空间而已,而且不接受任何参数。
看一下std::allocator的代码示例:

#ifndef DEFALLOC_H
#define DEFALLOC_H#include <new.h>
#include <stddef.h>
#include <stdlib.h>
#include <limits.h>
#include <iostream.h>
#include <algobase.h>
template <class T>
inline T* allocate(ptrdiff_t size,T*){set_new_handler(0);T* tmp = (T*)(::operator new((size_t)(size*(sizeof(T))));if(tmp == 0){cerr<<"out of memory"<<endl;exit(1);}return tmp;
}
template <class T>
inline void deallocate(T* buffer){::operator delete(buffer);
}
template <class T>
class allocator{
public:typedef T 			value_type;typedef T* 			pointer;typedef const T* 	const_pointer;typedef T& 			reference;typedef const T& 	const_reference;typedef size_t		size_type;typedef ptrdiff_t	difference_type;	pointer allocate(size_type n){return ::allocate((difference_type)n,(pointer)0);}void deallocate(pointer p){::deallocate(p);}pointer address(reference x){return (pointer)&x;}const_pointer const_address(const_refernce x){return (const_pointer)&x;}//确定一个“页面”或“块”的大小size_type int_page_size(){return max(size_type(1),size_type(4096/sizeof(T)));}size_type max_size() const{return max(size_type(1),size_type(UINT_MAX/sizeof(T)));}
};
class allocator<void>{
public:typedef void* pointer;
}
#endif //DEFALLOC_H

从源码可以看到allocator只是基层内存配置/释放行为的简单包装,在效率上没有任何的提升。

2.2 SGI特殊的空间配置器,std::alloc

一般而言,对C++内存配置操作和释放操作

class Foo{...};
Foo* pf = new Foo;   //配置内存,然后构造对象
delete pf;			 //将对象析构,然后释放内存

为了精密分工,STL allocator决定将这两阶段的操作区分开来,
内存配置操作由alloc:allocate()负责,内存释放操作由alloc:deallocate()负责;
对象构造操作由::construct()负责,对象析构操作由::destroy()负责。
SGI内含有以下两个文件:

#include<stl_alloc.h>           //负责内存空间的配置与释放
#include<stl_construct.h>       //负责对象内容的构造与析构

在这里插入图片描述

图1. STL配置器的框架

2.3 构造和析构的基本工具:construct()和destroy()

以下是<stl_construct.h>的部分内容:

#include<new.h> //欲使用placement new,需要包含次文件
template <class T1,class T2>{
inline void construct(T1* p,const T2& value){new(p) T1(value);  //placement new
}//destroy有2个版本
//版本1,接受一个指针;
template<class T1>
inline void destroy(T* pointer){pointer->~T();    
}
//版本2,接受2个迭代器,此函数设法找出元素的数值型别
template<class ForwardIterator>
inline void destroy(ForwardIterator first,ForwardIterator last){__destroy(first,last,value_type(first));
}//判断元素的数值类型,是否有trivial destructor
template<class ForwardIterator, class T>
inline void __destroy(ForwardIterator first,ForwardIterator last, T*){typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;__destroy_aux(first,last,trivial_destructor());
}
//如果元素的数值型别有 non-trivial destructor
template<class ForwardIterator>
inline void __destroy_aux(ForwardIterator first,ForwardIterator last, __false_type){for(;first<last;++first)destroy(& *first);
}
//如果元素的数值型别有 trivial destructor
template<class ForwardIterator>
inline void __destroy_aux(ForwardIterator first,ForwardIterator last, __true_type){}//destroy()第二版本对迭代器char* wchar_t*的特化版
inline void destroy(char*,char*){}
inline void destroy(wchar_t*,wchar_t*){}

construct()接受一个指针p和一个初值value,该函数的用途就是将初值设定到指针所指的空间上。
destruct()有2个版本。版本1接受一个指针,准备将该指针所指之物析构调,直接调用该对象的析构函数即可;版本2接受first,last两个迭代器,讲[first,last)范围内的对象都析构掉。做了一些处理,万一对象有(trivial destructor),每一次调用他们,会极大的降低效率。所以做了一些处理来判断析构函数是否是trivial destruct,如果是的话,什么也不做就立马结束。如果不是的话,以循环方式巡访整个范围,在循环中每访问一个对象就调用第一个版本的destroy()。
在这里插入图片描述

图2. construct()和destroy()函数

补充:

2.3.1、什么是 trivial destruct:
“trivial destructor”指的是用户没有自定义析构函数,而是由系统生成的析构函数。这种析构函数在对象生命周期结束时
被调用,但由于用户没有定义任何特定的析构逻辑,它实际上并不执行任何特定的清理或资源释放操作。因此,它被称
为“trivial”,即无用的或无关紧要的析构函数。
相对而言,如果用户为某个类定义了特定的析构函数,那么这个析构函数就被称为“non-trivial destructor”。在这种情况
下,析构函数可能会执行一些必要的清理操作,如释放动态分配的内存、关闭文件句柄或断开网络连接等。需要注意的是,即
使析构函数是trivial的,它仍然会在对象生命周期结束时被自动调用。这是因为C++的析构机制确保了每个构造的对象最终都会
被正确地销毁。然而,对于trivial destructor来说,由于它不执行任何实质性的操作,频繁地调用它可能会对性能产生一定的
影响。因此,在一些高级编程场景中,可能会使用特定的技术或工具来优化或避免不必要的trivial destructor调用。
2.3.2、从__destroy_aux来看,他的销毁范围是[first,last),为什么没有销毁最后一个

在C++中,迭代器通常遵循“左闭右开”的原则,即迭代器first指向的范围是包括的,而迭代器last指向的位置是不包括的。这意味着 last迭代器实际上指向的是范围之外的第一个位置 ,而不是范围内的最后一个元素的下一个位置。因此,循环在first小于last时继续,这样last指向的元素就不会被包括在销毁的范围内。
这种设计有几个原因:
(1)一致性:C++标准库中的许多算法和容器都遵循这种 “左闭右开” 的范围定义方式,保持一致性使得代码更容易理解和使用。
(2)避免越界访问(安全性):如果尝试访问或销毁last指向的元素,那么可能会越界访问内存,导致未定义行为。保持“左闭右开”的范围定义可以避免这种潜在的问题。
(3)效率:在某些情况下,last可能正好指向一个容器的末尾之后的位置,这个位置本身并不存储任何对象,因此没有销毁的必要。
(4)语义清晰:对于用户来说,知道last不指向要销毁的最后一个元素,可以更容易地理解函数的行为,特别是当使用自定义迭代器或特殊容器时。
综上所述,不将last销毁是出于一致性、安全性、效率和语义清晰性的考虑。在设计类似的算法或函数时,通常应该遵循这些原则来确保代码的正确性和可维护性。

2.4 空间的配置和释放,std::alloc

2.3讲的是对象构造行为与对象析构行为,现在来看一下内存的配置与释放。这一部分代码都在<stl_alloc.h>中。stl对此的设计哲学是:

  • (1)向 system heap 要求空间
  • (2)考虑多线程 (multi-threads) 状态
  • (3)考虑内存不足时的应变措施
  • (4)考虑过多“小型区块”可能造成的内存碎片 (fragment) 问题
    考虑到小型区块可能造成的内存破碎问题,SGI 为此设计了双层级配置器。当配置区块超过 128bytes 时,称为足够大,使用第一级配置器,直接使用 malloc() 和 free()。
    当配置区块不大于 128bytes 时,为了降低额外负担,直接使用第二级配置器,采用复杂的 memory pool 处理方式。
#ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; //另alloc为第一级配置器
#else
...
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS,0> alloc;
#endif /*  !__USE_MALLOC*/

其中, __malloc_alloc_template 就是第一级配置器; __default_alloc_template 就是第二级配置器。无论使用第一级配接器(malloc_alloc_template)或是第二级配接器 (default_alloc_template),alloc 都为其包装了接口,使其能够符合 STL 标准。

template<class T,class Alloc>
class simple_alloc{
public:static T*allocate(size_t n){return 0==n?:(T*)Alloc::allocate(n*sizeof(T));}static T*allocate(void){return (T*) Alloc::allocate(sizeof(T));}static T*deallocate(T* p,size_t n){if(0!=n) Alloc::deallocate(p,n*sizeof(T));}static void deallocate(T* p){Alloc::deallocate(p,sizeof(T));}
};

在这里插入图片描述

图3. 第一级配置器与第二级配置器

在这里插入图片描述

图4. 第一级配置器与第二级配置器包装接口和运用方式

2.5 第一级配置器 __malloc_alloc_template剖析

#if 0
#  include<new>
#  define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
#  include<iostream.h>
#  define __THROW_BAD_ALLOC cerr << "out of memory"<<endl; exit(1)
#endiftemplate <int inst>
class __malloc_alloc_template{
private:
//以下都是函数指针,所代表的函数将用来处理内存不足的情况
//oom:out of memory
static void *oom_malloc(size_t);
static void *oom_realloc(void* ,size_t);
static void (* __malloc_alloc_oom_handler());public:static void *allocate(size_t n)
{//第一级配置器直接使用malloc()void *result = malloc(n);if(0 == result) result = oom_malloc(n);return result;
}
static void deallocate(void *p ,size_t /* n */)
{//第一级配置器直接使用free()free(p);
}
static void* reallocate(void *p,size_t/*old_sz*/,size_t new_sz){//第一级配置器直接使用realloc()void * result = realloc(p,new_sz);//当以下无法满足要求时,改用oom_realloc()if(0 == result) result = oom_realloc(p,new_sz);return result;
}
static void(* set_malloc_handler(void(*f)()))()
{void(*old) = __malloc_alloc_oom_handler;_malloc_alloc_oom_handler = f;return (old);
}
};
//malloc_alloc out-of-memory handling
//初值为0,有待客段设定
template <int inst>
void (* __malloc_alloc_template<inst>::_malloc_alloc_oom_handler)() = 0;template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{void(* my_malloc_handler)();void * result;for(;;){//不断地尝试释放,配置,再释放,再配置my_malloc_handler = __malloc_alloc_oom_handler;if(0 == my_malloc_handler) {__THROW_BAD_ALLOC;}(*my_malloc_handler)(); //调用处理例程,企图释放内存result = malloc(n);     //再次尝试配置内存if(result) return (result);}
}template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p ,size_t n)
{void(* my_malloc_handler)();void * result;for(;;){.                       //不断地尝试释放,配置,再释放,再配置my_malloc_handler = __malloc_alloc_oom_handler;if(0 == my_malloc_handler) {__THROW_BAD_ALLOC;}(*my_malloc_handler)();     //调用处理例程,企图释放内存result = realloc(p,n);      //再次尝试配置内存if(result) return (result);}
}
//以下直接将参数inst指定为0
typedef __malloc_alloc_template<0> malloc_alloc;

(1)第一级配置器以 malloc(), free(), realloc() 等 C 函数执行实际的内存配置、释放和重配置 操作,并实现类似 C++ new-handler 的机制(因为它并非使用 ::operator new 来配置内存, 所以不能直接使用C++ new-handler 机制)。
(2)SGI 第一级配置器的 allocate() 和 reallocate() 都是在调用malloc() 和 realloc() 不成功 后,改调用 oom_malloc() 和oom_realloc()。
(3)oom_malloc() 和 oom_realloc() 都有内循环,不断调用“内存不足处理例程”,期望某次 调用后,获得足够的内存而圆满完成任务,哪怕有一丝希望也要全力以赴申请啊,如果用户并 没有指定“内存不足处理程序”,这个时候便无力乏天,真的是没内存了,STL 便抛出异常。或 调用exit(1) 终止程序。

2.6 第二级配置器 __default_alloc_template剖析

第二级配置器多了一些机制,专⻔针对内存碎片。内存碎片化带来的不仅仅是回收时的困难,配置也是一个负担,额外负担永远无法避免,毕竟系统要划出这么多的资源来管理另外的资源,但是区块越小,额外负担率就越高。
在这里插入图片描述

图5. 索取内存,需要cookie记录大小
### 2.6.1 SGI 第二级配置器到底解决了多少问题呢? 简单来说 SGI第二级配置器的做法是:**sub-allocation (层次架构):** 前面也说过了,SGI STL 的第一级配置器是直接使用 malloc(), free(), realloc() 并配合类似 C++ new-handler 机制实现的。第二级配置器的工作机制要根据区块的大小是否大于 128bytes 来采取不同的策略,这种方法叫做层次配置。

在这里插入图片描述

图6. 层次配置

在这里插入图片描述

图7. 内存池原理

二级配置器内存池的原理主要涉及两个方面:内存池的管理两级配置器的使用
(1)首先,内存池的主要作用是在应用程序初始化时分配一块连续的内存空间,并将其划分为多个固定大小的块(也称为“内存池对象”)。这些块可以被反复使用,从而避免了频繁地向操作系统请求新的内存空间。当应用程序需要分配新的内存时,它可以直接从预先分配好的内存池中取出一个可用的块,而不是向操作系统申请。这样可以减少系统开销,提高程序效率,同时也可以避免内存碎片和泄漏等问题。
(2)其次,二级配置器则是对内存池管理的一种优化策略。它采用两级策略来配置内存:当所需内存区块小于某个阈值(如128字节)时,使用第二级配置器进行管理。这个配置器会一次性配置一大块内存,并维护对应的空闲链表(free-list)。当下次有相同大小的内存需求时,直接从空闲链表中取出。如果有小的内存区块被释放,它们会被回收到空闲链表中,以供后续使用。这种策略有效地避免了由于太多小区块造成的内存碎片问题,同时减少了配置时的额外负担。
具体来说,二级配置器会维护多个空闲链表,每个链表管理不同大小的内存块。例如,可以维护16个空闲链表,分别管理大小为8、16、24…120、128字节的数据块。当需要分配内存时,根据所需大小计算索引,从对应的空闲链表中取出内存块。当内存块被释放时,它会被回收到对应的空闲链表中。
最后,内存池的管理涉及对内存空间的分配和释放。当向系统申请一块内存放入内存池时,会使用start和end指针来标记这块空间的起始和结束位置。每分配一部分内存,start指针就会向后移动相应的距离。当start和end指针重合时,表示内存池中的空间已全部分配完毕。释放内存时,需要将释放的内存块归还到内存池中,并更新start指针的位置。
综上所述,二级配置器内存池的原理通过预分配连续内存空间、划分固定大小的块、使用两级配置器以及维护空闲链表等方式,实现了对内存的高效管理和利用,避免了内存碎片和泄漏等问题,提高了程序的运行效率。

2.6.2 自由链表自由在哪?又该如何维护呢?

我们知道,一方面,自由链表中有些区块已经分配给了客端使用,所以 free_list 不需要再指向它们;另一方面,为了维护 free-list,每个节点还需要额外的指针指向下一个节点。
那么问题来了,如果每个节点有两个指针?这不就造成了额外负担吗?本来我们设计 STL 容 器就是用来保存对象的,这倒好,对象还没保存之前,已经占据了额外的内存空间了。
STL给出的解决办法是:

union obj{union obj* free_list_link;char client_data[1]
}

在 union obj 中,定义了两个字段,再结合上图来分析:
从第一个字段看,obj 可以看做一个指针,指向链表中的下一个节点;
从第二个字段看,obj 可以也看做一个指针,不过此时是指向实际的内存区。
一物二用的好处就是不会为了维护链表所必须的指针而造成内存的另一种浪费,

在二级配置器中的自由链表(free-list),**“自由”**体现在链表中的内存块是未被应用程序当前使用的,即它们是“空闲”的,随时可以被分配给需要内存的空间。自由链表的存在允许内存块在被释放后能够被回收和重用,而不是直接归还给操作系统,从而提高了内存的使用效率。
自由链表的维护主要涉及以下几个方面:
(1) 插入操作:当释放一个内存块时,该内存块会被插入到相应的自由链表中。通常,链表会按照内存块的大小进行划分,每个链表管理特定大小的内存块。因此,释放内存块时,需要根据其大小计算它应该插入到哪个链表中。
(2) 删除操作:当应用程序请求分配内存时,二级配置器会从自由链表中删除一个内存块,并将其分配给应用程序。这通常涉及到从链表中取出第一个节点(或根据某种策略选取一个节点)并更新链表的结构。
(3) 链表合并:如果两个相邻大小的自由链表都只有少量的内存块,为了提高内存管理的效率,可能会将它们合并成一个更大的链表。这样可以减少链表的数量,简化管理。
(4) 内存块合并:当连续的内存块都被释放时,可以考虑将它们合并成一个更大的内存块,以减少碎片并便于后续的分配。
(5) 内存块分割:当请求的内存大小与自由链表中现有内存块的大小不完全匹配时,可能需要将现有的内存块分割成两部分,一部分用于满足当前的请求,另一部分作为新的空闲内存块加入自由链表。
(6) 链表大小调整:随着程序的运行,某些大小的内存块可能会被频繁地申请和释放,导致某些链表特别长或特别短。此时,可以根据实际情况调整链表的大小,例如通过合并或分割链表来平衡内存的使用。
(7) 内存泄露检查:定期检查自由链表中的内存块是否都被正确管理,防止因为编程错误导致的内存泄露。
为了实现这些操作,二级配置器通常会维护一些数据结构来跟踪每个链表的状态,例如链表的头指针、链表长度、已分配和未分配的内存块数量等。这些数据结构可以帮助配置器高效地管理内存块,确保它们能够在需要时被正确地分配和回收。
总的来说,自由链表的维护涉及到内存块的插入、删除、合并和分割等操作,以及链表本身的调整和优化。这些操作共同确保了内存的有效利用和程序的稳定运行。

2.6.3 第二级配置器的部分实现内容

enum{__ALIGN = 8};                        //小型区块的上调边界
enum{__MAX_BYTES = 128};                  //小型区块的上限
enum{__NFREELISTS = __MAX_BYTES/__ALIGH}; //free_lists的个数template <bool threads,int inst>
class __default_alloc_template{
private:
//ROUND UP
static size_t ROUND_UP(size_t bytes){return (((bytes)+ __ALIGN_1) & ~(__ALIGN-1));
}
private:union obj{ //free lists的节点构造union obj * free_list_link;char clinet_data[1]};
private://16个free-listsstatic obj* volatile free_list[__NFREELISTS];//根据区块大小,决定使用第n号listsstatic size_t FREELIST_INDEX(size_t bytes){return (((bytes) + __ALIGN -1)/__ALIGN -1);}
static void *refill(size_t n)
//配置一大块空间,可容纳nobjs个大小为size的区域
//如果配置nobjs个区域有所不便,nobjs可能会降低
static char* chunk_alloc(size_t size,int &nobjs);
//chunk allocation state
static char* start_free;  //内存池起始位置
static char* end_free;    //内存池结束位置
static char* heap_size;public:static void * allocate(size_t n)static void * deallocate(void *p,size_t n)static void * reallocate(void *p,size_t old_sz,size_t new_sz);
};
//以下是 static data member的定义与初值设定
template<bool threads,int inst>
char *__default_alloc_template<threads,inst>::start_free = 0;template<bool threads,int inst>
char *__default_alloc_template<threads,inst>::end_free = 0;template<bool threads,int inst>
char *__default_alloc_template<threads,inst>::heap_size = 0;template<bool threads,int inst>
__default_alloc_template<threads,inst>::obj* volatile
__default_alloc_template<threads,inst>::free_list[__NFREELISTS]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
}

2.7空间配置函数allocate()

我们知道第二级配置器拥有配置器的标准接口函数 allocate()。此函数首先判断区块的大小, 如果大于 128bytes –> 调用第一级配置器;小于128bytes–> 就检查对应的 free_list(如果没 有可用区块,就将区块上调至 8 倍数的边界,然后调用 refill(), 为 free list 新填充空间。

2.7.1空间配置函数allocate()实现原理:

allocate()代码实现了一个简单的内存分配策略,它首先检查请求的内存大小是否超过预设的阈值,如果是则调用另一个配置器进行分配。对于小块内存,它使用一组free lists来快速分配和回收固定大小的内存块。当free list为空时,它会调用 refill 函数来重新填充free list。这种策略有助于减少内存碎片,并提高小块内存的分配和回收效率。

2.7.2空间配置函数allocate()代码实现:

//n must be >0
static void *allocate(size_t n){obj* volatile *my_free_list;obj* result;//大于128就调用第一级配置器if(n > (size_t)__MAX_BYTES){return (malloc_alloc::allocate(n));}//寻找16个free lists中符合的一个my_free_list = free_list + FREELIST_INDEX(n);result = *my_free_list;if(result == 0){//没找到可用的free lists,准备重新填充 free lstsvoid *r = refill(ROUND_UP(n));return r;}//调整free lists*my_free_list = result->free_list_link;return(result);
};

在这里插入图片描述

图8. allocate()函数工作原理

2.8 空间释放函数 deallocate()

身为一个配置器,__default_alloc_template 拥有配置器标准接口函数 deallocate()。该函数首先判断区块大小,大于 128bytes就调用第一级配置器, 小于 128bytes就找出对应的free lists,将区块进行回收。

2.8.1空间释放函数 deallocate()实现原理:

这个函数是一个自定义内存分配器的一部分,用于将释放的内存块添加到适当的空闲链表中,以便将来可以重新使用。对于大于某个阈值的内存块,它使用另一个(可能是更通用的)分配器来释放内存。这种自定义的内存分配器通常用于优化特定类型的内存使用,例如小对象的快速分配和释放。

2.8.2空间释放函数 deallocate()代码实现:

//p不可以是0
static void *allocate(void* p,size_t n){obj *q = (obj*)p;obj *volatile *my_free_list;//大于128就调用第一级配置器if(n > (size_t)__MAX_BYTES){malloc_alloc::deallocate(p,n);return ;}//寻找对应的free listmy_free_list = free_list + FREELIST_INDEX(n);//调整free list,收回区块q->free_list_link = *my_free_list;*my_free_list = q;
}

在这里插入图片描述

图9. deallocate()函数工作原理

2.9 重新填充 free lists

先前说过的allocate()当它发现freelist 中没有可用区块了时, 就调用 refill(},准备为freelist重新填充空间.新的空间将取自内存池(经由
chu出_alloc()完成)。缺省取得20 个新节点(新区块),但万一内存池空间不 足,获得的节点数(区块数)可能小于 20:

2.9.1重新填充 free lists()实现原理:

这个函数的主要目的是为自定义内存分配器“refill”空闲链表。它首先分配一个较大的内存块(chunk),然后将其分割成多个较小的对象,并将这些对象链接成一个空闲链表。最后,它返回第一个对象的地址给调用者,以便该对象可以被分配给客户端。这种策略通常用于优化内存分配和释放,特别是在需要频繁分配和释放小块内存的情况下。

2.9.2重新填充 free lists()代码实现:

(1)当发现 free_list 中没有可用区块时,就会调用 refill() 为free_list 重新填充空间;
(2)新的空间将取自内存池(经由 chunk_alloc() 完成);
(3)缺省取得20个新节点(区块),但万一内存池空间不足,获得的节点数可能小于 20。

/* Returns an object of size n, and optionally adds to size n free list.*/
/* We assume that n is properly aligned.                                */
/* We hold the allocation lock.                                         */
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{int nobjs = 20;char * chunk = chunk_alloc(n, nobjs);obj * __VOLATILE * my_free_list;obj * result;obj * current_obj, * next_obj;int i;if (1 == nobjs) return(chunk);my_free_list = free_list + FREELIST_INDEX(n);/* Build free list in chunk */result = (obj *)chunk;*my_free_list = next_obj = (obj *)(chunk + n);      // my_free_list 跳过第一块内存  从第2块开始     result指向第一块  返回给客户端for (i = 1; ; i++) {current_obj = next_obj;next_obj = (obj *)((char *)next_obj + n);if (nobjs - 1 == i) {                           // 19 块空间 放到相应的 free_list 里面   current_obj -> free_list_link = 0;break;} else {current_obj -> free_list_link = next_obj;}}return(result);
}

2.10 内存池 memory pool

首先,我们要知道从内存池中取空间给 free_list 使用,是 chunk_alloc() 在工作,它是怎么工 作的呢?
我们先来分析 chunk_alloc() 的工作机制:chunk_alloc() 函数以 end_free – start_free 来判断内存池的“水量”。具体逻辑如下。
在这里插入图片描述

图10. chunk_alloc()函数工作原理
如果第一级配置器的 malloc() 也失败了,就发出 bad_alloc 异常。

2.10.1内存池 memory pool实现原理:

在这里插入图片描述

图11. memory_pool()函数工作原理
举个例子: 假设程序一开始,客端就调用 chunk_alloc (32, 20), 于是 malloc()配置 40 个 32 bytes 区块,其中第 1 个交出,另 19 个交给free_list[3]维护,余 20 个留给内存池。接下来客端调用chunk_alloc(64,20),此时 free_list[7]空空如也,必须向内存池要求支持。内存池只够供应 ( 32*20)/64=10 个 64 bytes 区块,就把这 10 个区块返回,第 1 个 交给客端,余 9 个由 free_list[7]维护。此时内存池全空。接下来再调用 chunk_alloc(96, 20),此时 free_list[11]空空如也,必须向内存池要求支持, 而内存池此时也是空的,于是以 malloc()配置40+n (附加量)个96bytes区块,其中第 1 个交出,另 19 个交给 free_list[11]维护,余 20+n (附加量)个区块 留给内存池。

2.10.2内存池 memory pool代码实现:

template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, int& __nobjs)
{char* __result;size_t __total_bytes = __size * __nobjs;size_t __bytes_left = _S_end_free - _S_start_free;    //内存池剩余空间if (__bytes_left >= __total_bytes)                    //满足内存需求{__result = _S_start_free;_S_start_free += __total_bytes;return(__result);} else if (__bytes_left >= __size)                     //满足至少一个区块的需求{__nobjs = (int)(__bytes_left/__size);__total_bytes = __size * __nobjs;__result = _S_start_free;_S_start_free += __total_bytes;return(__result);} else                                                //完全不满足需求{size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);//利用剩下的一点点零头.if (__bytes_left > 0) {_Obj* __STL_VOLATILE* __my_free_list =_S_free_list + _S_freelist_index(__bytes_left);((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;*__my_free_list = (_Obj*)_S_start_free;}_S_start_free = (char*)malloc(__bytes_to_get);if (0 == _S_start_free) {size_t __i;_Obj* __STL_VOLATILE* __my_free_list;_Obj* __p;//看free-list是否还有内存区块for (__i = __size; __i <= (size_t) _MAX_BYTES; __i += (size_t) _ALIGN){__my_free_list = _S_free_list + _S_freelist_index(__i);__p = *__my_free_list;//有的话,编入,并循环调用自身,直至彻底使用所有零头if (0 != __p) {*__my_free_list = __p -> _M_free_list_link;_S_start_free = (char*)__p;_S_end_free = _S_start_free + __i;return(_S_chunk_alloc(__size, __nobjs));                //反复压榨内存}}//执行到了这一步,说明没内存了,_S_end_free初始化置于零,并调用第一级配置器配置内存重新设定_S_start_free。_S_end_free = 0;    _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);}_S_heap_size += __bytes_to_get;_S_end_free = _S_start_free + __bytes_to_get;//return(_S_chunk_alloc(__size, __nobjs));}
}

3、内存 基本处理工具

STL定义了五个全局函数,分别是construct(),destroy(),uninitialized_copy(),uninitialized_fill(),
uninitialized_fill_n()。构造,析构函数已经说过,现在考虑后面3个函数。

3.1 uninitialized_copy

std::uninitialized_copy()
std::uninitialized_copy() 用于从输入范围复制元素到未初始化的内存区域。它接收两个输入迭代器(表示要复制的元素的范围)和一个指向目标内存区域的原始指针。
函数原型可能类似于:

template<class InputIt, class ForwardIt>  
ForwardIt uninitialized_copy(InputIt first, InputIt last, ForwardIt result);

first 和 last 定义了输入范围的开始和结束。
d_first 是指向目标内存区域的原始指针。
这个函数会将 [first, last) 范围内的所有元素复制到从 d_first 开始的内存区域,并返回指向目标区域中最后一个复制元素的下一个位置的原始指针。

3.2 uninitialized_fill

std::uninitialized_fill() 用于在未初始化的内存区域中填充指定值。它接收一个原始指针(指向目标内存区域的开始)和一个表示要填充的值的参数,以及要填充的元素数量。
函数原型可能类似于:

template<class ForwardIt, class T>  
void uninitialized_fill(ForwardIt first, ForwardIt last, const T& value);

first 和 last 定义了目标内存区域的开始和结束。
value 是要填充的值。
这个函数会将 [first, last) 范围内的所有元素初始化为 value。

3.3 uninitialized_fill_n

std::uninitialized_fill_n() 类似于 std::uninitialized_fill(),但它允许你指定要填充的元素数量,而不是通过两个迭代器来定义范围。
函数原型可能类似于:

template<class ForwardIt, class Size, class T>  
ForwardIt uninitialized_fill_n(ForwardIt first, Size n, const T& value);

first 是指向目标内存区域的原始指针。
n 是要填充的元素数量。
value 是要填充的值。
这个函数会将从 first 开始的 n 个元素初始化为 value,并返回指向最后一个填充元素的下一个位置的原始指针。

4、总结

天堂有路你不走,地狱无门你自来。

5、参考

1、《STL源码剖析》
2、《C++八股文小贺》(图片均来自于他)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/284781.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java代码基础算法练习-公式求和-2024.03.24

任务描述&#xff1a; 求公式Snaaaaaa…aa…aaa&#xff08;有n个a&#xff09;之值&#xff0c;其中a是一个数字&#xff0c;为2。 例如&#xff0c;n5 时222222222222222&#xff0c;n 由键盘输入(n<5)。 任务要求&#xff1a; package march0317_0331;import java.util.…

【大模型】在VS Code(Visual Studio Code)上安装中文汉化版插件

文章目录 一、下载安装二、配置显示语言&#xff08;一&#xff09;调出即将输入命令的搜索模式&#xff08;二&#xff09;在大于号后面输入&#xff1a;Configure Display Language&#xff08;三&#xff09;重启 三、总结 【运行系统】win 11 【本文解决的问题】 1、英文不…

代码随想录算法训练营第三十一天|455.分发饼干,376.摆动序列,53. 最大子序和

455.分发饼干 题目 假设你是一位很棒的家长&#xff0c;想要给你的孩子们一些小饼干。但是&#xff0c;每个孩子最多只能给一块饼干。 对每个孩子 i&#xff0c;都有一个胃口值 g[i]&#xff0c;这是能让孩子们满足胃口的饼干的最小尺寸&#xff1b;并且每块饼干 j&#xff…

wy的leetcode刷题记录_Day93

wy的leetcode刷题记录_Day93 声明 本文章的所有题目信息都来源于leetcode 如有侵权请联系我删掉! 时间&#xff1a;2024-3-23 前言 目录 wy的leetcode刷题记录_Day93声明前言2549. 统计桌面上的不同数字题目介绍思路代码收获 827. 最大人工岛题目介绍思路代码收获 200. 岛屿…

【Godot4.2】像素直线画法及点求取函数

概述 基于CanvasItem提供的绘图函数进行线段绘制只需要直接调用draw_line函数就可以了。 但是对于可以保存和赋值节点直接使用的纹理图片&#xff0c;却需要依靠Image类。而Image类没有直接提供基于像素的绘图函数。只能依靠set_pixel或set_pixelv进行逐个像素的填色。 所以…

数字乡村发展策略:科技引领农村实现跨越式发展

随着信息技术的迅猛发展和数字经济的崛起&#xff0c;数字乡村发展策略已经成为引领农村实现跨越式发展的重要手段。科技的力量正在深刻改变着传统农业的生产方式、农村的社会结构以及农民的生活方式&#xff0c;为农村经济发展注入了新的活力和动力。本文将从数字乡村的内涵、…

java每日一题——买啤酒(递归经典问题)

前言&#xff1a; 非常喜欢的一道题&#xff0c;经典中的经典。打好基础&#xff0c;daydayup!!!啤酒问题&#xff1a;一瓶啤酒2元&#xff0c;4个盖子可以换一瓶&#xff0c;2个空瓶可以换一瓶&#xff0c;请问10元可以喝几瓶 题目如下&#xff1a; 啤酒问题&#xff1a;一瓶…

基于图的在线社区假新闻检测建模

论文原文&#xff1a;Graph-based Modeling of Online Communities for Fake News Detection 论文代码&#xff1a;GitHub - shaanchandra/SAFER: Repository containing the official code for the paper Graph-based Modeling of Online Communities for Fake News Detectio…

KIMI爆了!对比文心一言和通义千问它到底有多强?

原文:赵侠客 前言 最近国产大模型KIMI爆了大部分人应该都知道了&#xff0c;从我个人的感受来看这次KIMI爆了我不是从技术领域接触到的&#xff0c;而是从各种金融领域接触到的。目前国内大模型可以说是百模大战&#xff0c;前几年新能源大战&#xff0c;今年资本割完韭菜后留…

java面向对象编程基础

对象&#xff1a; java程序中的对象&#xff1a; 本质上是一种特殊的数据结构 对象是由类new出来的&#xff0c;有了类就可以创建对象 对象在计算机的执行原理&#xff1a; student s1new student();每次new student(),就是在堆内存中开辟一块内存区域代表一个学生对象s1变…

Matlab DDPG

文章目录 1 rlSimulinkEnv1.1 说明1.2 例子1.2.1 使用工作空间Agent创建Simulink环境1.2.2 为Simulink模型创建强化学习环境1.2.3 创建Simulink多Agents环境2 创建Simulink环境和训练Agent2.1 创建环境接口2.2 创建DDPG Agent2.3 训练Agent2.4 验证已训练的Agent3 创建Simulink…

创建linux虚拟机系统:(安装Ubuntu镜像文件,包含语言设置、中文输入法、时间设置)

我下载的是清华大写开源软件镜像站中的ubuntu-20.04.6-desktop-amd64.iso这个镜像文件&#xff0c; 这个文件我下载完成之后没有解压&#xff0c;直接在创建虚拟机的时候选择的压缩包。 地址为&#xff1a;Index of /ubuntu-releases/20.04/ | 清华大学开源软件镜像站 | Tsin…

Git——IDEA中的使用详解

目录 Git1、IDEA中配置Git2、将本地项目推送到远程仓库2.1、创建项目远程仓库2.2、初始化本地仓库2.3、连接远程仓库2.4、提交到本地仓库2.5、推送到远程仓库 3、克隆远程仓库到本地4、基本操作4.1、代码提交到暂存区4.2、暂存区代码提交到本地库4.3、推送到远程仓库4.4、撤销本…

网络: 网络层

IP地址: 分为网络号和主机号. 用来标识主机 IP协议 IP协议报文 4位版本号(version): 指定IP协议的版本, 对于IPv4来说, 就是4.4位头部长度(header length): IP头部的长度是多少个32bit, 也就是 length * 4 的字节数. 4bit表示最大的数字是15, 因此IP头部最大长度是60字节. 8…

HarmonyOS NEXT应用开发案例集

概述 随着应用代码的复杂度提升&#xff0c;为了使应用有更好的可维护性和可扩展性&#xff0c;良好的应用架构设计变得尤为重要。本篇文章将介绍一个应用通用架构的设计思路&#xff0c;以减少模块间的耦合、提升团队开发效率&#xff0c;为开发者呈现一个清晰且结构化的开发…

YOLOv8:Roboflow公开数据集训练模型

Roboflow公开数据集 Roboflow是一个提供计算机视觉数据集管理和处理工具的平台。虽然Roboflow本身并不创建或策划公开数据集&#xff0c;但它提供了一系列功能&#xff0c;帮助用户组织、预处理、增强和导出计算机视觉数据集。 官方网站&#xff1a;https://universe.roboflow…

FOCUS-AND-DETECT: A SMALL OBJECTDETECTION FRAMEWORK FOR AERIAL IMAGES

摘要 为了解决小对象检测问题&#xff0c;提出了一个叫做 Focus-and Detect 的检测框架&#xff0c;它是一个两阶段的框架。 第 一阶段包括由高斯混合模型监督的对象检测器网络&#xff0c;生成构成聚焦区域的对象簇 。 第二阶段 也是一个物体探测器网络&#xff0c;预测聚焦…

Web框架开发-Ajax

一、 Ajax准备知识:json 1、json(Javascript Obiect Notation,JS对象标记)是一种轻量级的数据交换格式 1 2 它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。 简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。…

谧林涓露门禁

原神武器升级材料谧林涓露和门禁好像聂。 difference(){union(){cylinder(2, 10,10, $fn365);hull(){translate([15,0,0])cylinder(1,2,2,$fn365);cylinder(1,10,10,$fn365);}}translate([15,0,-1])cylinder(4,1,1,$fn365); }

swagger3快速使用

目录 &#x1f37f;1.导入依赖 &#x1f32d;2.添加配置文件 &#x1f9c2;3.添加注解 &#x1f96f;4.访问客户端 1.导入依赖 引入swagger3的依赖包 <dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artif…