SoFunction
Updated on 2024-10-28

How the Python memory manager implements pooling techniques

preamble

Everything in Python is an object, and memory for those objects is dynamically allocated on the heap at runtime; even the stack used by the Python virtual machine is emulated on the heap. Since everything is an object, objects are created and freed frequently during the course of a Python program, and requesting or freeing memory from the operating system with malloc() and free() every time has a performance impact; after all, these functions end up making system calls that cause context switches. Let's take a look at how Python's memory manager manages memory efficiently.

In fact, the core is the pooling technology, a one-time application to the operating system for a number of consecutive memory space, each time you need to create an object in this space to find a free memory block to allocate, the object will be released when the corresponding block of memory will be marked as free, which avoids each time to the operating system to apply for and release the memory, as long as the total object memory space in the program is stable, Python to the As long as the total object memory space in the program is stable, Python will request and release memory from the operating system very infrequently. This scenario is not familiar. Database connection pooling is a similar idea. Typically, a backend application establishes multiple connections to the database in advance, finds an available connection to interact with the database each time it executes SQL, returns the connection to the connection pool when the SQL completes, and then releases the connection if it hasn't been used for a long time. Essentially, these are just exchanging space for time, consuming some not-so-large amount of memory, reducing the frequency of time-consuming operations such as memory requests and TCP connection establishment, and improving the overall speed of the program.

The next step is to take a look at how Python's memory manager implements the pooling technique, first outlining the memory hierarchy and the process of allocating memory, and then expanding on it in detail with the source code.

memory hierarchy

The Python memory manager has a hierarchy of memory, consisting of arena, pool, and block, in descending order. arena is a large chunk of memory that the memory manager requests directly from the operating system by calling malloc() or calloc(), and is where Python objects are created and freed. The arena is divided into pools, each of which is divided into equal-sized blocks, and each time memory is allocated, a block is selected from one of the pools and returned. The size of the blocks in each pool is equal, and the size of the blocks in different pools can be different.

The sizes of arena, pool, and block are different on 32-bit and 64-bit machines. The size of block must be a multiple of ALIGNMENT and up to a maximum of 512 bytes, and the following table lists the sizes of the various types of memory on different machines.

  32-bit machines 64-bit machines
arena size 256 KB 1 MB
pool size 4 KB 16 KB
ALIGNMENT 8 B 16 B

Taking a 64-bit machine as an example, all possible block sizes are 16, 32, 48, 496, 512, and each size corresponds to a size class, which is 0, 1, 2, 30, 31 from smallest to largest. each time the memory is allocated, it is a matter of finding the smallest free block that is not smaller than the requested size. The purpose of classifying the block size is to adapt to different sizes of memory requests, reduce the generation of memory fragments, and improve the utilization of arena.

Memory Management Logic

Once you understand the concepts of arena, pool, and block, you can describe the logic of memory allocation. Suppose you need memory of size n bytes.

  • If n > 512, fallback to malloc(), since the block is 512 bytes max.
  • Otherwise, calculate the smallest block size that is not less than n. For example, if n=105, the smallest block size on a 64-bit machine is 112.
  • Find the pool that corresponds to the block size in 2 and allocate a block from it, if there is no pool available allocate a pool from the available arena, if there is no arena available use malloc() to request a new arena from the OS.

The logic for freeing memory is as follows

  • First determine whether the memory to be freed is allocated by the Python memory manager, if not, just return
  • Find the block and pool that correspond to the memory to be freed, and return the block to the pool for the next allocation.
  • If the arena in which the freed block resides is free except for itself, then the entire arena is free after the block is returned, and the arena can be freed back to the operating system with free().

Objects in Python are generally small and have short lifecycles, so once arena is requested, the allocation and release of the object is mostly done in arena, improving efficiency.
The core logic of Python's memory manager has been described above, but there are a few details that have yet to be resolved, such as how to find the corresponding pools based on block size when allocating memory, how these pools are related to each other, and how to determine whether the memory to be freed is allocated by Python's memory manager or not when freeing it, and so on. The following is the source code for memory allocation and pooling. The following is a detailed description of the logic of memory allocation and freeing in conjunction with the source code.

First, we introduce the memory layout and corresponding data structures of arena and pool, and then we analyze the logic of pymalloc_alloc() and pymalloc_free(), using a 64-bit machine as an example.

Memory layout and corresponding data structures

Arena

An arena is 1 MB, a pool is 16 KB, and pools are adjacent to each other in the arena, so there are at most 1 MB / 16 KB = 64 pools in an arena. The Python memory manager aligns the first address of the first pool in the arena with POOL_SIZE, so that the first address of each pool is an integer multiple of POOL_SIZE. so that the first address of each pool is an integer multiple of POOL_SIZE. Given any memory address, it is easy to calculate the first address of the pool it is in, a feature that is used when memory is freed. POOL_SIZE is 4 KB on 32-bit machines and 16 KB on 64-bit machines, which also has the advantage of making each pool fall into one or more physical pages, improving access efficiency. The gray blocks in the above figure are discarded for alignment purposes; if the first address of the memory allocated by malloc() is aligned, then the number of pools is 64, otherwise it's 63. arena doesn't divide all the pools at the beginning, but rather, it divides them into new pools when there are no more pools available, and the layout is shown in the following figure when the pools have been divided. When all the pools are divided, the layout is as shown above.

Each arena is represented by a struct arena_object, but not all struct arena_objects have a corresponding arena, because the corresponding struct arena_objects remain after the arena is released, and the struct arena_objects that don't have a corresponding arena are stored in a single linked table, unused_arena_objects, and can be used when the arena is next allocated. objects are stored in a single linked table, unused_arena_objects, and can be used in the next arena allocation. If struct arena_object has a corresponding arena, and there is a pool in the arena that can be allocated, then struct arena_object will be stored in usable_arenas, which is a bidirectional linked list, and all struct arena_objects will be stored in arenas. The arena in usable_arenas are sorted according to the number of free pools they contain from smallest to largest, so that the arena that has already used more memory is prioritized to be used in the next allocation of pools, and the arena that has more free memory will be more likely to become arena that have more free memory are more likely to become completely free and be freed, returning their memory space to the operating system and reducing overall memory consumption.

The structure of struct arena_object and the meaning of each field are as follows

struct arena_object {
    uintptr_t address; // Points to the starting address of the arena, or address = 0 if the arena_object has no memory for the arena.
    block* pool_address; // pools need to be initialized before they can be used, and the address pointed to by pool_address can be used to initialize a pool for allocation.
    int nfreepools; // the number of pools currently available for allocation in arena
    uint ntotalpools; // total number of pools in arena, 64 or 63
    struct pool_header* freepools; // The pools that can be allocated in arena form a single linked table, and the freepools pointer is the first node of the single linked table.
    struct arena_object* nextarena; // point to the next node in usable_arenas or unused_arena_objects
    struct arena_object* prevarena; // point to the previous node in usable_arenas
}

Pool

The interior of a pool is divided into equal blocks of equal size, and like arena, there is a data structure struct pool_header that represents the pool. unlike arena, struct pool_header is located inside the pool, at the very beginning of the section of memory, and is immediately followed by the first block, as shown in the gray part of the figure. In order to align the address of each block with the step of the machine accessing the memory, it may be necessary to do some padding between struct pool_header and the first block, as shown in gray in the figure. This padding may not always be there; on a 64-bit machine, sizeof(struct pool_header) is 48 bytes, which is already aligned, and is directly followed by the first block, with no padding in between. even so, the small block at the end of the pool may not be usable, as shown by the grayed-out portion of the figure below, because The block size in each pool is equal, assuming the block is 64 bytes, a pool can be divided into 255 blocks, the first 48 bytes to store the struct pool_header, the next 16 bytes can not be used, of course, if the block size of 48 bytes or 16 bytes, then the entire pool will be fully utilized. As with arena, the pool is not divided into all blocks at the beginning, but only when there are no more blocks available, the layout of the pool is shown in the figure above.

Next, look at the structure of struct pool_header

struct pool_header {
    union { block *_padding;
            uint count; } ref; // the number of blocks already in use in the current pool, only the count field is meaningful in the commons, and _padding is used to make the ref field take up 8 bytes, a feature that is useful when initializing usedpools
    block *freeblock; // Header pointer to a single linked table of blocks in the pool that can be allocated.
    struct pool_header *nextpool; // Point to the next pool in arena_object.freepools or usedpools.
    struct pool_header *prevpool; // point to the previous pool in usedpools
    uint arenaindex; // index of the arena where the pool is located in the arenas array
    uint szidx; // Hierarchy of block sizes in a pool
    uint nextoffset; // If you need a new block, you can allocate it from nextoffset.
    uint maxnextoffset; // nextoffset Maximum valid value
};

typedef struct pool_header *poolp;

Once a pool is allocated, it must be in one of three states: full, empty, or used.

  • full All blocks are assigned
  • empty All blocks are free and available for allocation, and all pools in the empty state are in a single linked table represented by the freepools field of the arena_object in which they reside.
  • used There are allocated blocks and free blocks, and all used pools are in a bi-directional circular chain pointed to by an element in the global array usedpools.

usedpools is the most frequently accessed data structure for memory allocation. When allocating memory, it first calculates the block level i corresponding to the requested memory size. usedpools[i+i] points to the head node of a two-way circular chain list composed of all used pools belonging to level i. If the list is not empty, it will select a free block from the head node and allocate it. The next step is to analyze why usedpools[usedpools[usedpools] is used. Next, we analyze why usedpools[i+i] points to the chain table of pools belonging to level i.

The original definition of usedpools is as follows

#define PTA(x)  ((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)
static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = { 
    PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7),
    …
}

Expanding the macro definitions a bit

static poolp usedpools[64] = { 
    PTA(0), PTA(0), PTA(1), PTA(1), PTA(2), PTA(2), PTA(3), PTA(3),
    PTA(4), PTA(4), PTA(5), PTA(5), PTA(6), PTA(6), PTA(7), PTA(7),
    …
}

PTA(x) represents the address of the 2*xth element of the usedpools array minus the size of the two pointers, which is 16 bytes (on a 64-bit machine). Assuming that the first address of the usedpools array is 1000, the array is initialized with the values shown below

Assuming i = 2, theusedpools[i+i] = usedpools[4] = 1016If we consider that 1016 stores a struct pool_header, then the values of usedpools[4] and usedpools[5], which are the values stored at addresses 1032 and 1040, are the values of fields nextpool and prevpool, respectively. prevpool, respectively.

usedpools[4]->prevpool = usedpools[4]->nextpool = usedpools[4] = 1016

commander-in-chief (military)usedpools[4] Using the pointer p, we havep->prevpool = p->nextpool = pThen p is the sentinel node of a bidirectional cyclic chain table, which is initialized with its front and back pointers pointing to itself, indicating that the current chain table is empty.

Although the definition of usedpools is very convoluted, it has the advantage of eliminating the data fields of the sentinel nodes and keeping only the front and back pointers, which can be considered as the ultimate memory saver.

Here is a look at the source code to see how to implement the logic of memory allocation and release, the following source code is based on Python 3.10.4. In addition, the source code has more detailed comments than this article, interested readers can look directly at the source code, this article in order to code is not too long will be simplified to deal with the code and omit most of the comments.

memory allocation

The main logic for memory allocation is in the function pymalloc_alloc, simplified as follows

static inline void*
pymalloc_alloc(void *ctx, size_t nbytes)
{  
    // Calculate the memory size requested ntybes corresponding to the memory hierarchy size
    uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    // Find the head pointer to the pool of the two-way circular chain where the pool belonging to the memory hierarchy size is located.
    poolp pool = usedpools[size + size];
    block *bp;
    // pool ! = pool->nextpool, indicating that pool is not a sentinel node and is a real pool
    if (LIKELY(pool != pool->nextpool)) {
        ++pool->;
        // Assign the block pointed to by pool->freeblock to bp, since pool is taken from usedpools.
        // According to the definition of usedpools, pool->freeblock must point to a free block.
        bp = pool->freeblock;
        // If pool->freeblock is empty after allocating a bp, divide a free block from the pool.
        // Leave sub-allocations in the pool->freeblock chain table.
        if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL)) {
            pymalloc_pool_extend(pool, size);
        }
    }
    // If there is no pool available for the corresponding memory hierarchy, allocate a pool from arena and then allocate the block from it.
    else {
        bp = allocate_from_new_pool(size);
    }
    
    return (void *)bp;
}

The main logic is still relatively clear, the code in the comments are made to explain, but still need to explain the following judgment statement.

if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL))

Previously describedpool->freeblock The head pointer of the single linked table that represents the block available for allocation in the pool is of type block*, but the definition of block is typedef uint8_t block;, which is not a structure, so there is no pointer field, so how do you implement a single linked table. Considerpool->freeblock The Python memory manager treats the first 8 bytes of free block memory (on 64-bit machines) as a virtual next pointer to the next free block, which is accomplished with *(block **)bp, by first converting bp to a secondary pointer to the block with (block **), and then dereferencing it with * to convert the contents of the first address of bp to a (block *) type pointer to the address of the next block, which has to be said to be the address of the next block. First use (block **) to convert bp into a secondary pointer to the block, and then use * to dereference the contents of the first address of the memory pointed to by bp into the (block *) type, which represents the address of the next block, and I have to say, the C language can really do whatever it wants. Let's take a look at the above judgment statement again. First, the next free block address of bp is assigned topool->freeblockIf pymalloc_pool_extend is not available, it is necessary to call pymalloc_pool_extend to expand the block if it is NULL.

The source code for pymalloc_pool_extend is simplified as follows

static void
pymalloc_pool_extend(poolp pool, uint size)
{
    // If there is more space in the pool, divide a free block and put it in pool->freeblock.
    if (UNLIKELY(pool->nextoffset <= pool->maxnextoffset)) {
        pool->freeblock = (block*)pool + pool->nextoffset;
        pool->nextoffset += INDEX2SIZE(size);
        // pool->freeblock has only one block, you need to set the virtual next pointer to NULL.
        *(block **)(pool->freeblock) = NULL;
        return;
    }

    // If there is no more space, you need to remove the pool from usedpools[size+size].
    poolp next;
    next = pool->nextpool;
    pool = pool->prevpool;
    next->prevpool = pool;
    pool->nextpool = next;

}

The process is also clear: if there is more space, divide a block into thepool->freeblockIf you don't have more space, remove the pool from theusedpools[size+size] Remove.pool->nextoffset points to an address in the pool that has never been used, and is used first when allocating a blockpool->nextoffset These free blocks are usually blocks that have been allocated before and later released to thepool->freeblock in the pool. This way of reusing free blocks makes the pool more durable if it is reused every time from thepool->nextoffset Dividing a new block, the pool is quickly consumed and becomes full.

In pymalloc_alloc, if there is no pool available, allocate_from_new_pool will be called to allocate a new pool first, and then allocate the block from the new pool, the source code of which is simplified as follows

static void*
allocate_from_new_pool(uint size)
{
    // If you don't have an available arena, request a new one.
    if (UNLIKELY(usable_arenas == NULL)) {
        usable_arenas = new_arena();
        if (usable_arenas == NULL) {
            return NULL;
        }
        // Use the new arena as the head node of the usable_arenas linked table.
        usable_arenas->nextarena = usable_arenas->prevarena = NULL;
        nfp2lasta[usable_arenas->nfreepools] = usable_arenas;
    }

    // Allocate a free pool if there are any available arena and adjust the position of the current arena in the usable_arenas so that the usable_arenas are sorted by the number of free pools, from smallest to largest.
    if (nfp2lasta[usable_arenas->nfreepools] == usable_arenas) {
        nfp2lasta[usable_arenas->nfreepools] = NULL;
    }
    if (usable_arenas->nfreepools > 1) {
        nfp2lasta[usable_arenas->nfreepools - 1] = usable_arenas;
    }

    // At this point in the process, usable_arenas->freepools is the currently available pool.
    poolp pool = usable_arenas->freepools;
    // Update the freepools linked list and the nfreepools count.
    if (LIKELY(pool != NULL)) {
        usable_arenas->freepools = pool->nextpool;
        usable_arenas->nfreepools--;
        // After allocation, if there are no free pools in the arena, you need to update the usable_arenas link table.
        if (UNLIKELY(usable_arenas->nfreepools == 0)) {
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
            }
        }
    }
    // If there are no pools available in the current arena, reallocate one.
    else {
        pool = (poolp)usable_arenas->pool_address;
        pool->arenaindex = (uint)(usable_arenas - arenas);
        pool->szidx = DUMMY_SIZE_IDX;
        usable_arenas->pool_address += POOL_SIZE;
        --usable_arenas->nfreepools;
        // If there are no free pools in the arena after partitioning, you need to update the usable_arenas linkage table
        if (usable_arenas->nfreepools == 0) {
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
            }
        }
    }

    // At this point, the variable pool is the available pool found, and is placed at the head of usedpools[size+size].
    block *bp;
    poolp next = usedpools[size + size];
    pool->nextpool = next;
    pool->prevpool = next;
    next->nextpool = pool;
    next->prevpool = pool;
    pool-> = 1;
    // If the pool's memory hierarchy matches the requested one, allocate a block from it and return it.
    // Proving that the pool was previously used and then released into freepools.
    // and the memory hierarchy was also size at the time it was used.
    if (pool->szidx == size) {
        bp = pool->freeblock;
        pool->freeblock = *(block **)bp;
        return bp;
    }
    
    // At this point, the pool is new to arena and needs to be initialized.
    // Then allocate the block return
    pool->szidx = size;
    size = INDEX2SIZE(size);
    bp = (block *)pool + POOL_OVERHEAD;
    pool->nextoffset = POOL_OVERHEAD + (size << 1);
    pool->maxnextoffset = POOL_SIZE - size;
    pool->freeblock = bp + size;
    *(block **)(pool->freeblock) = NULL;
    return bp;
}

This code is quite long, but in summary it does the following 3 things

  • If you don't have an available arena, request a new one.
  • Allocate a new pool from the available arena
  • Allocate free blocks from the allocated pool.

First of all, it is the application of arena, the application process is in the function new_arena(), after the application, the corresponding arena_object will be set as the head node of the bilinear chain table usable_arenas, and the pointers before and after will be set to NULL, because only when there is no usable arena will we go back to call new_arena(), so there is only one usable arena in the system after the application. There is also another operation as follows

nfp2lasta[usable_arenas->nfreepools] = usable_arenas;

nfp2lasta is an array ofnfp2lasta[i] represents the last arena in the usable_arenas chain table among all the arena whose number of free pools is i. As explained in the previous section, usable_arenas are sorted according to the number of free pools in the arena from smallest to largest, in order to maintain the order of usable_arenas, when inserting or deleting an arena, we need to find the corresponding position, and the time complexity is O(N). In order to maintain the order of usable_arenas, when inserting or deleting an arena, we need to find the corresponding position, which has O(N) time complexity.Introduced nfp2lastathat reduces the time complexity to a constant level.

Once you have available arena, you can allocate pools from it. After allocating pools, the number of arena->nfreepools decreases, and you need to update nfp2lasta. Since you are using the header node of the chained table usable_arenas, and reducing the number of pools it has available, the whole chained table is still in order. Next, we prioritize the reuse of thearena->freepools If there are no free pools in the pool, then the pools from thearena->pool_address This is the same strategy as reusing free blocks in a pool.

If the pool is not obtained from the newly allocated arena, then the pool is the one that was released after initialization, and if the pool's hierarchy happens to be the requested memory hierarchy, then the pool is allocated directly from thepool->freeblock If you don't, you need to re-initialize the pool, and of course, if the pool comes from a newly allocated arena, you need to initialize it as well. When initializing, the first block address is memory-aligned, and then thepool->freeblock to point to the second block to leave behind for use (the first block is to be returned this time), and set thepool->nextoffset Points to the third block, to be used the next time a new block is created.

memory release

The main logic for memory freeing is in the pymalloc_free function, the code is simplified as follows

static inline int
pymalloc_free(void *ctx, void *p)
{
    // Assuming p is pool-allocated, compute the first address of the pool where p is located.
    poolp pool = POOL_ADDR(p);
    // If p is not allocated by the memory manager, return it.
    if (UNLIKELY(!address_in_range(p, pool))) {
        return 0;
    }
    
    // Return the block pointed to by p to the pool as the head node of pool->freeblock.
    block *lastfree = pool->freeblock;
    *(block **)p = lastfree;
    pool->freeblock = (block *)p;
    pool->--;
    // If the pool was in the full state and now has a free block, it becomes used.
    // It needs to be inserted as a header node into usedpools[size+size].
    if (UNLIKELY(lastfree == NULL)) {
        insert_to_usedpool(pool);
        return 1;
    }

    if (LIKELY(pool-> != 0)) {
        return 1;
    }

    // If all the blocks in the pool are free after the block is freed, the
    // move pool from usedpools[size+size] to arena->freepools
    insert_to_freepool(pool);
    return 1;
}

The logic of the pymalloc_free function is also clear

  • Calculate the first address of the pool where the address p is located, as described above, the first address of each pool is an integer multiple of POOL_SIZE, so the low position of p is 0 to get the address of the pool
  • address_in_range(p, pool) Determine whether p is allocated by pool, if not, return directly.
  • Free the block pointed to by p bypool->freeblock recall (a defective product)
  • If the pool is full at the beginning, then it is used after the block is recycled and the function is calledinsert_to_usedpool(pool) Set it tousedpools[size+size] The header node of the pool is the same as usable_arenas. The strategy here is the same as for usable_arenas, prioritizing the use of pools that are almost full, so that free pools have a higher probability of being released.
  • If the pool becomes empty after reclaiming the block, you need to call theinsert_to_freepool(pool) Release the pool as well.

The address_in_range function is as follows

address_in_range(void *p, poolp pool)
{
    uint arenaindex = *((volatile uint *)&pool->arenaindex);
    return arenaindex < maxarenas &&
        (uintptr_t)p - arenas[arenaindex].address < ARENA_SIZE &&
        arenas[arenaindex].address != 0;
}

This logic determines whether p is allocated by a pool in a constant amount of time, but there is one thing that can go wrong. After all, the pool is calculated assuming that p is allocated by a pool, so it's possible that the address pointed to by the pool may not have been initialized yet.pool->arenaindex Python 3.10 has a problem in thiscommit The base tree is used to determine whether any address p is allocated by the memory manager, avoiding possible memory access errors.

insert_to_usedpool function is just a simple pointer operation will not be expanded, insert_to_freepool slightly more complex, the following and then expand the

static void
insert_to_freepool(poolp pool)
{
    poolp next = pool->nextpool;
    poolp prev = pool->prevpool;
    next->prevpool = prev;
    prev->nextpool = next;
    // Make pool the ao->freepools header node.
    struct arena_object *ao = &arenas[pool->arenaindex];
    pool->nextpool = ao->freepools;
    ao->freepools = pool;
    uint nf = ao->nfreepools;
    struct arena_object* lastnf = nfp2lasta[nf];
    // If arena is the last arena in the queue with nf free pools, then
    // Need to set nfp2lasta[nf] to the predecessor node of arena or NULL
    if (lastnf == ao) { /* it is the rightmost */
        struct arena_object* p = ao->prevarena;
        nfp2lasta[nf] = (p != NULL && p->nfreepools == nf) ? p : NULL;
    }
    ao->nfreepools = ++nf;

    // If the arena becomes completely idle after the pool is freed, and there are other available arena in the system.
    // You need to remove arena from usable_arenas and call free() to return it to the OS.
    if (nf == ao->ntotalpools && ao->nextarena != NULL) {
        if (ao->prevarena == NULL) {
            usable_arenas = ao->nextarena;
        }
        else {
            ao->prevarena->nextarena = ao->nextarena;
        }
        if (ao->nextarena != NULL) {
            ao->nextarena->prevarena = ao->prevarena;
        }
        ao->nextarena = unused_arena_objects;
        unused_arena_objects = ao;
        arena_map_mark_used(ao->address, 0);
        _PyObject_Arena.free(_PyObject_Arena.ctx, (void *)ao->address, ARENA_SIZE);
        ao->address = 0;          
        --narenas_currently_allocated;
        return;
    }
    // If the arena goes from full to available after the pool is freed, it needs to be set to the usable_arenas header node.
    // Because arena free pool is 1, being the head node does not break usable_arenas ordering
    if (nf == 1) {
        ao->nextarena = usable_arenas;
        ao->prevarena = NULL;
        if (usable_arenas)
            usable_arenas->prevarena = ao;
        usable_arenas = ao;
        if (nfp2lasta[1] == NULL) {
            nfp2lasta[1] = ao;
        }
        return;
    }

    if (nfp2lasta[nf] == NULL) {
        nfp2lasta[nf] = ao;
    } 
    // If arena was the last of the lastnf free pools, now add 1 to the number of free pools.
    // The entire usable_arenas is still ordered.
    if (ao == lastnf) {
        return;
    }

    // the addition of arena->nfreepools caused usable_arenas to go out of order.
    // Realign arena in usable_arenas
    if (ao->prevarena != NULL) {
        ao->prevarena->nextarena = ao->nextarena;
    }
    else {
        usable_arenas = ao->nextarena;
    }
    ao->nextarena->prevarena = ao->prevarena;
    ao->prevarena = lastnf;
    ao->nextarena = lastnf->nextarena;
    if (ao->nextarena != NULL) {
        ao->nextarena->prevarena = ao;
    }
    lastnf->nextarena = ao;
}

First set this free pool toao->freepools This ensures that the last pool to be released is the first to be used and improves access efficiency, since the previously released pools may have been displaced from physical memory. Then nfp2lasta is updated according to different situations, so that it is easier to maintain the order of usable_arenas. Then we do different operations based on the change of arena state of the pool before and after it is released.

  • If the arena changes from free to free, and there are other free arena in the system, free() is called to free the arena and return it to the operating system. If there is only one free arena in the system, it is not freed to avoid memory jitter.
  • If the arena changes from an unavailable state (all pools are allocated) to an available state, make it the head node of usable_arenas.
  • If the arena is available before and after the pool is released, i.e., it is always in the usable_arenas chain table, and if the increase in the number of available pools causes the usable_arenas chain table to go out of order, the arena needs to be moved to a suitable location to maintain the order of the usable_arenas.

To summarize, this article first introduces the background of Python's memory managers, which are designed to avoid frequent calls to malloc() and free() to create and free objects, and to improve efficiency by using a memory pooling scheme. Then it introduces the details of the memory manager, the meaning of Arena, Pool, and Block, and specifies the memory layout of Arena and Pool and their related data structures, based on which it expands the memory allocation algorithm in detail.pymalloc_alloc() and Memory Release Algorithmpymalloc_free(). I hope this article has helped you understand memory management in Python.

summarize

To this article on how the Python memory manager pooling technology is introduced to this article, more related Python memory management content, please search for my previous articles or continue to browse the following related articles I hope you will support me in the future!