STL learning notes - space Configurator

Keywords: C C++ STL memory management

1, Overview

Allocator is one of the six components of STL, space configurator. Its function is to manage memory for each container (memory recovery). The allocator configuration object is not only memory, it can also ask for space from the hard disk. Here we mainly consider the memory management. When using STL library, we don't have to consider various memory operations because allocator has helped us configure memory. So how does allocator manage memory?

For a standard STL container, when vector < int > VEC, the real statement should be

vetor<int, allocator<int>>vec

The second parameter is the allocator we use. Here is the standard version of space configurator, that is, it is available in different versions of STL. However, in the SGI version of STL, it also has a more efficient spatial configurator called alloc. When using this spatial configurator, you can't use the standard writing method, and it doesn't accept any parameters. In other words, there are two space configurators in the SGI version of STL, one is the standard allocator, and the other is SGI's own allocator.

The usage of the two configurators are:

vetor<int, allocator<int>>vec//Standard allocator space Configurator
vetor<int, alloc>vec//alloc space Configurator

2, SGI standard space configurator allocator

When we create and destroy an object,

class FOO{};
FOO *pt = new FOO;    
delete pt;

When you create a new object, the program performs two actions: first call:: operator new to allocate memory of the size of an object, and then call FOO::FOO() on this memory to construct the object.

Again,   When deleting an object, the two actions are: first call FOO::~FOO() to destruct the object, and then call:: operator delete to free the memory where the object is located.

STL separates the four operations allocator. It is realized by four functions respectively.

	//Request memory
	pointer allocate(size_type n, const void* hint = 0)
	{
		T* tmp = (T*)(::operator new((size_t)(n * sizeof(T))));
		//operator new is different from new operator
		if (!tmp)
			cerr << "out of memory"<<endl;	
		return tmp;
	} 
	//Free memory
	void deallocate(pointer p)
	{
		::operator delete(p);
	}
	//structure
	void construct(pointer p, const T& value)
	{
		new(p) T1(value);
	}
	//Deconstruction
	void destroy(pointer p)
	{
		p->~T();
	}
	

It can be seen that the four phases in the allocator source code correspond to four functions respectively, and the creation and release of memory in these four functions are simple encapsulation of new and delete respectively. Although SGI has this space configurator, SGI itself has never used it, and we are not recommended to use it. The main reason is that the efficiency is poor. Only the:: operator new and:: operator delete of C + + are packaged in one layer.

3, SGI special space configurator alloc

The four operations mentioned above have four functions corresponding to them in the space configurator:

Memory configuration: alloc::allocate();

Object construction: construct();

Destruct of object: destroy();

Memory release: alloc::deallocate();

The distribution of these four functions in the header file of SGI version STL is as follows

STL specifies that the space configurator is placed in < memory >, where there are three files as shown in the above figure: STL_ Two functions are defined in structure, which are responsible for the construction and Deconstruction of objects;     In stl_alloc.h is the place where the alloc function of the space configurator is placed. It is responsible for the development and recycling of memory. Here, the primary and secondary configurators are defined. Another header file defines some global functions, which are mainly responsible for filling or copying large blocks of memory data.

1,<stl_ Structure. H > --- construction and Deconstruction of objects

template <class T>
inline void destroy(T* pointer) {
    pointer->~T(); //Only one layer of packaging is made to destruct the object pointed to by the pointer - by directly calling the destructor of the class
}
 
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
  new (p) T1(value); //Use placement new to create an object on the object indicated by p. value is the value of the initialization object.
}
 
template <class ForwardIterator> //A generalized version of destory that accepts two iterators as parameters
inline void destroy(ForwardIterator first, ForwardIterator last) {
  __destroy(first, last, value_type(first));//Call built-in__ destory(),value_type() extracts the type of the element referred to by the iterator
}
 
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());//trival_destructor() is equivalent to determining whether the type indicated by the iterator has a trival destructor
}
 
 
template <class ForwardIterator>
inline void //If there is no trival destructor, you need to call the destroy() function to destruct the object elements between the two iterators one by one
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
  for ( ; first < last; ++first)
    destroy(&*first);
}
 
template <class ForwardIterator> //If there is a trival destructor, you don't have to do anything
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}
 
inline void destroy(char*, char*) {}          //Special Edition for char *
inline void destroy(wchar_t*, wchar_t*) {}    //For wchar_ Special edition of T *

Destruct of object::: destroy()

The generalized version of the destroy function takes two iterators as parameters, that is, it will destruct the objects between the two iterators pointing to memory. Its internal call__ Destroy function. This function will first judge whether the iterator points to a trivial destructor (judged by the third parameter trival_destructor()). If so, this value becomes__ true_type is then called through the function template mechanism__ destroy_ Next to aux, nothing was done in it. If not, this value becomes__ false_type, and then called through the function template mechanism__ destroy_ The previous one of aux, in which the destructor of the object will be called.

If the user does not define a destructor, but uses the system's own destructor, it means that the destructor is basically useless (but it will be called by default). We call it a trivial destructor. On the contrary, if a destructor is defined, it means that something needs to be done before space is released. This destructor is called a non trivial destructor. If there are only basic types in a class, it is not necessary to call the destructor. When delelte p, the destructor code will not be generated.  

In C + + classes, if there are only basic data types, there is no need to write an explicit destructor. The default destructor is enough, that is, trival is used directly_ Destructor. At this time, you don't have to write your own destructor to call the default trival_destructor, but if there is a pointer to other classes in the class and a new space is allocated during construction, this space must be explicitly released in the destructor, otherwise a memory leak will occur.

  When configuring space in STL, the destroy () function will judge whether the object pointed to by the iterator to be released has any   Trival destructor (there is a has_trival_destructor function in STL, which is easy to detect). If there is a trival destructor, nothing will be done. If there is no trival destructor, some operations need to be performed, the real destruction function will be executed. Destruction () has two versions. The first version accepts a pointer and is ready to destruct the object referred to by the pointer. The second version accepts two iterators, first and last, and is ready to destruct all objects within the range of [first, last]. We don't know how big this range is. If it is very large and the destructors of each object are irrelevant, calling these destructors again and again will hurt efficiency. Therefore, we use value first_ Type() gets the category of the object referred to by the iterator, and then uses it_ type_ Traits < T > judge whether the destructor of this type is irrelevant. If it is (_true_type), it will end without doing anything. If not (_false_type), it will patrol the whole range in a loop, and call the first version of destroy() every time an object is experienced in the loop.

  2,<stl_ Alloc. H > --- memory configuration and release

In order to solve the problem of memory fragmentation caused by small blocks, SGI designs a two-level configurator. When the applied memory is greater than 128 bytes, it starts the first level allocator to allocate directly from the heap space of the system through malloc; If the requested memory is less than 128 bytes, start the second level allocator to take a piece of memory from a pre allocated memory pool and deliver it to the user. This memory pool consists of 16 free lists of different sizes (multiple of 8, 8~128byte s). The allocator will take the header block from the corresponding free block list to the user according to the size of the requested memory (round up the size to multiple of 8).   This approach has two advantages:

1) Fast allocation of small objects. Small objects are allocated from the memory pool. This memory pool is a system call. malloc allocates a large enough area to the program for standby. When the memory pool is exhausted, it applies to the system for a new area. The whole process is similar to wholesale and retail. A problem here is that memory pool will bring some memory waste. For example, when only one small object needs to be allocated, a large memory pool may be applied for this small object. This situation is rare in practical application.

2) The generation of memory fragments is avoided. The allocation of small objects in the program is very easy to cause memory fragments, which brings great pressure to the memory management of the operating system. The increase of fragments in the system will not only affect the speed of memory allocation, but also greatly reduce the utilization of memory. The memory pool is used to organize the memory of small objects. From the perspective of the system, it is only a large memory pool, and the allocation and release of memory of small objects cannot be seen.

 

Primary configurator:__ malloc_alloc_template

The call of the primary adapter is very simple. Directly apply for memory with malloc, release memory with free, and simply encapsulate malloc and free.

  static void* allocate(size_t __n)   //Request memory
  {
    void* __result = malloc(__n);
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }
  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p);
  }

The first level configurator performs the actual memory configuration, release and reconfiguration operations with C functions such as malloe(). free(). realloc(), and implements a mechanism similar to C + + new handler. It cannot directly use the C + + new handler mechanism because it does not use:: operator new to configure memory. The so-called C++newhandler mechanism is that you can ask the system to call a function you specify when the memory configuration requirements cannot be met. In other words, once:: operator new fails to complete the task, it will call the processing routine specified by the client before throwing the std:bada lloc exception state. This processing routine is usually called new handler.

The allocate() and realloc() of the SGI first level configurator call oom instead after calling malloc() and realloc() unsuccessfully_ Malloc() and oom_ realloc(). The latter two have inner loops, constantly calling the "out of memory processing routine", hoping to get enough memory to complete the task after a call. However, if the "out of memory processing routine" is not set by the client, oom_malloc() and oom_ realloc() is called_ THROW_BAD_ALLOC, throw out bad_alloc exception information, or use exit(1) to abort the program.

Secondary configurator:__ default_alloc_template

If the memory requested by the program is greater than 128 bytes, the first level configurator is called; If it is less than 128 bytes, internal storage pool management: configure a large block of memory each time and maintain the corresponding free list. Next time, if there is a memory demand of the same size, dial it directly from the free list. If the client releases a small block, it will be recycled to the free list by the configurator. In addition to configuration, the configurator is also responsible for recycling. In order to facilitate management, the SGI secondary configurator will actively increase the memory demand of any small block to a multiple of 8 (for example, if the client requires 30 bytes, it will be automatically adjusted to 32 bytes), and maintain 16 feelists with management sizes of 8, 16, 24, 32. 40, 48, 56, 64, 72, 80, 88, 96, 104, 112 and 120 respectively, The node of free lists is an array + linked list structure. The array stores the head pointers of the linked list. Here, the array has 16 elements, corresponding to the head pointers of 16 linked lists respectively. The nodes in these linked lists represent the sizes of different memory blocks. The memory block sizes of the 16 linked list nodes are 8bytes, 16bytes, 32bytes... 128bytes respectively, increasing by a multiple of 8. Each linked list generally has more than a dozen such nodes connected together, and the structure is as follows

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

  In order to maintain lists, each node needs an additional pointer (pointing to the next node), which creates another additional burden? Note that the above obj uses Union. Because of union, from its first field, OBJ can be regarded as a pointer to another obj of the same form; From its second field, OBJ can be regarded as a pointer to the actual block, as shown in the figure below. As a result, it will not cause another waste of memory in order to maintain the pointers necessary for the linked list.

__ default._alloc_template has the standard interface function allocate() of the configurator. This function first determines the size of the block, greater than 128 bytes, calls the first level configurator, and checks the corresponding free list. if it is less than 128 byes. If there is any available block in free list, it will be used directly. If there is no available block, the block size will be raised to the 8 Multiple boundary, then the refill() will be invoked. Prepare to refill the space for the free list.

The refill function in step 3 above is responsible for requesting memory from the memory pool when there is no memory in the free list. How does the refill() function work?  

//Returns an object of size n, and sometimes adds nodes to the appropriate free list
//It is assumed that n has been appropriately raised to a multiple of 8
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    //Call_ S_chunk_alloc(), trying to get__ nobjs blocks are used as new nodes of the free list
    //Attention parameters__ nobjs is a reference
    char* __chunk = _S_chunk_alloc(__n, __nobjs);
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;
 
    //If only one block is obtained, the block is allocated to the caller. There is no new node in the free list
    if (1 == __nobjs) return(__chunk);
    //Otherwise, prepare to adjust the free list to include the new node
    __my_free_list = _S_free_list + _S_freelist_index(__n);
 
    //In__ Create free list in chunk space
      __result = (_Obj*)__chunk; //This piece is ready to be returned to the customer
      //The following boot free list points to the newly configured space (taken from the memory pool)
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n); 
      //The nodes of the free list are connected in series
      for (__i = 1; ; __i++) { //Start from 1, because the 0 will be returned to the client
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
        if (__nobjs - 1 == __i) {
            __current_obj -> _M_free_list_link = 0;
            break;
        } else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
      }
    return(__result);
}

In fact, the core is to call chunk_ The alloc function passes the required number of memory blocks nobjs (the default setting is 20) and the memory size n of each block (which has been adjusted to a multiple of 8) to chunk_alloc function, chunk_alloc will request the corresponding memory from the memory pool. If you go through chunk_ If only one memory block is obtained from the alloc application, the memory block will be directly returned to the caller, and no new node will be added to the corresponding free list. If you go through chunk_ If the number of memory blocks obtained from the alloc application is greater than 1, adjust the free list, add a new node to the free list, and point to the chunk space.

According to the above analysis, chunk plays a key role_ Alloc function. So how does he work

//Assumptions__ size has been appropriately raised to a multiple of 8
//Attention__ nobjs is a reference type
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; //Memory pool remaining space
 
    if (__bytes_left >= __total_bytes) {
    //The remaining space of the memory pool fully meets the demand
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else if (__bytes_left >= __size) {
    //The remaining space in the memory pool cannot fully meet the demand, but it is enough for more than one block
        __nobjs = (int)(__bytes_left/__size);
        __total_bytes = __size * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else {
    //The remaining space in the memory pool cannot be provided even for a block size
        size_t __bytes_to_get = 
      2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
        // Try to make the remaining bits in the memory pool useful
        if (__bytes_left > 0) {
            //The memory pool still has a fraction. Allocate the appropriate free list first
            //First, find the appropriate free list
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left);
            //Adjust the free list to include the remaining memory space in the memory
            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }
        //Configure heap space to fill the memory pool
        _S_start_free = (char*)malloc(__bytes_to_get);
        //Insufficient heap space, malloc() failed
        if (0 == _S_start_free) {
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
        _Obj* __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 <= (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) { //There are unused blocks in the free list
                    //Adjust the free list to free unused blocks
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;
                    _S_end_free = _S_start_free + __i;
   //Call yourself recursively to correct nobjs return (_s_chunk_alloc (_size, _nobjs));
                    // Any remaining fraction will eventually be compiled into the appropriate free list for standby
                }
            }
        _S_end_free = 0;    // If there is an accident
            //Call the first configurator to see if the out of memory mechanism can do its best
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
            //This can cause exceptions to be thrown, or an out of memory condition to be improved
        }
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        //Recurse yourself and correct nobjs
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

First record the total space to be applied_ Bytes and memory pool remaining space size bytes_left. If the remaining space of the memory pool fully meets the demand, that is, bytes_left>size*nobjs,   Then point the result pointer to the starting free position of the current memory pool,
Then move back at the starting position and return the result pointer. The first memory block is handed over to the client, and the others are handed over to the corresponding free list for maintenance.


If the remaining space of the memory pool cannot fully meet all memory requests, but is enough to supply one request, the memory pool will give out all the remaining space. The first memory block is handed over to the client, and the others are handed over to the corresponding free list for maintenance.


If the remaining space of the memory pool can't meet even one, there are still some residual bits. For example, the application is 30bytes, but now there are only 15bytes left in the memory pool. The current practice is to match the 15 bytes under the appropriate free list, that is, this is under the second free list.

If the remaining space of the memory pool is not enough, and there is not even any remaining fraction. At this time, if the heap space is enough, configure 2*n+m(m is the additional amount) memory blocks with malloc, one of which is given to the user, the other n-1 is given to the corresponding free list maintenance, and all the rest is given to the memory pool.

If the system heap is exhausted, malloc() fails and chunks_ Alloc () recursively calls itself to look around for the memory block node of the free list with "there are unused blocks, and the blocks are large enough". If you find one, dig one and hand it over. If you can't find it, call the first level configurator. In fact, the first level configurator also uses malloc () to configure memory, but it has an out of memory mechanism, which may have the opportunity to release other memory for use, If it can, it will succeed, otherwise it will issue bad_alloc exception.

Example:

For example, as shown in the figure above, suppose that the client calls chunk al10c (32, 20) at the beginning of the program, so malloe() configures 40 32 bytes blocks, of which the first one is handed over and the other 19 are handed over to free_ 1ist[3] maintenance, the remaining 20 are reserved for the memory pool, and then called by the client
chunk_alloc(64,20), free_list[7] is empty. You must ask the memory pool for support. The memory pool is only enough to supply (32 * 20) / 64 = 10 64 bytes blocks. Return these 10 blocks, give the first one to the client, and free the remaining 9_ list[7]   maintain. The memory pool is now empty. Next, call chunk_ alloc(96, 20), free_ list11 is empty. You must ask the memory pool for support, and the memory pool is empty at this time. Therefore, configure 40+n (additional amount) 96 bytes blocks with malloe(), of which the first one is handed over and the other 19 are handed over to free_ 1ist[11] maintenance, the remaining 20+n (additional) blocks are reserved for the memory pool.
 

Memory recycling:

_ default_alloc_template has the configurator standard interface function deallocate (). This function first judges the block size, and calls the first level configurator when it is greater than 128 bytes; If it is less than 128 bytes, find the corresponding free list and recycle the block.

void alloc::deallocate(void* ptr, size_t size) {
    if (size > MaxBytes) {
        free(ptr);
    }
    else {
        size_t index = FREELIST_INDEX(size);
        static_cast<node*>(ptr)->next = freeLists[index];
        freeLists[index] = static_cast<node*>(ptr);
    }
}

Posted by niki77 on Sun, 10 Oct 2021 02:54:54 -0700