How PostgreSQL Allocates Memory Fast
palloc is not malloc: PostgreSQL quantises every allocation to a power of two, keeps a freelist per size class, carves the tail of a dying block into free chunks, and recycles whole memory contexts.
Every C program that wants to be fast needs to be fast at allocating memory. PostgreSQL allocates memory in the tightest loop imaginable: a hash join probes its hash table once per outer row, an aggregate re-evaluates its transition function once per input row, and a sort copies tuples into a buffer. In a query that touches ten million rows, that is tens of millions of allocations, and every one of them has to be cheap.
PostgreSQL’s answer is palloc(), and palloc() is emphatically not
malloc(). It does not call malloc() per allocation. It does not keep
a general-purpose heap. It quantises every request to a power of two,
keeps one free list per size class, carves the tail of an
exhausted block into free chunks rather than throwing it away, and
recycles whole memory context objects so that a per-tuple context
never has to go back to malloc() at all.
This is a walk through how it all works, based on reading the PostgreSQL
source (version 20devel, commit e75fcc9867f).
The idea: lifetimes, not ownership
The foundational idea is not an allocator technique at all. It is that memory has a lifetime, and the lifetime should decide who frees it.
PostgreSQL’s src/backend/utils/mmgr/README states it plainly:
The main advantage of memory contexts over plain use of malloc/free is that the entire contents of a memory context can be freed easily, without having to request freeing of each individual chunk within it. This is both faster and more reliable than per-chunk bookkeeping.
So instead of tracking every individual allocation, PostgreSQL builds a tree of memory contexts, each with its own lifetime, and frees a whole subtree in one operation. A per-tuple context is reset after every tuple. A per-query context is reset after every query. A per-transaction context is reset after every transaction. And the allocator inside each context is free to make whatever local trade-offs it likes, because the context boundary, not the individual free, is what actually matters for memory return.
The tree is a plain first-child/next-sibling structure in
src/include/nodes/memnodes.h:
typedef struct MemoryContextData
{
pg_node_attr(abstract) /* there are no nodes of this type */
NodeTag type; /* identifies exact kind of context */
/* these two fields are placed here to minimize alignment wastage: */
bool isReset; /* T = no space allocated since last reset */
bool allowInCritSection; /* allow palloc in critical section */
Size mem_allocated; /* track memory allocated for this context */
const MemoryContextMethods *methods; /* virtual function table */
MemoryContext parent; /* NULL if no parent (toplevel context) */
MemoryContext firstchild; /* head of linked list of children */
MemoryContext prevchild; /* previous child of same parent */
MemoryContext nextchild; /* next child of same parent */
const char *name; /* context name */
const char *ident; /* context ID if any */
MemoryContextCallback *reset_cbs; /* list of reset/delete callbacks */
} MemoryContextData;Note the methods pointer: a memory context is a virtual object. The
same tree can be rooted at an AllocSetContext, a SlabContext, a
BumpContext or a GenerationContext, and each has completely
different allocation behaviour while presenting the same API.
One indirection, but a deliberate one
palloc() does not call malloc(). It calls through the context’s
method table. From src/backend/utils/mmgr/mcxt.c:
/*
* Call the given function in the MemoryContextMethods for the memory context
* type that 'pointer' belongs to.
*/
#define MCXT_METHOD(pointer, method) \
mcxt_methods[GetMemoryChunkMethodID(pointer)].methodAnd palloc() itself:
void *
palloc(Size size)
{
/* duplicates MemoryContextAlloc to avoid increased overhead */
void *ret;
MemoryContext context = CurrentMemoryContext;
Assert(MemoryContextIsValid(context));
AssertNotInCriticalSection(context);
context->isReset = false;
/*
* For efficiency reasons, we purposefully offload the handling of
* allocation failures to the MemoryContextMethods implementation as this
* allows these checks to be performed only when an actual malloc needs to
* be done to request more memory from the OS. Additionally, not having
* to execute any instructions after this call allows the compiler to use
* the sibling call optimization. If you're considering adding code after
* this call, consider making it the responsibility of the 'alloc'
* function instead.
*/
ret = context->methods->alloc(context, size, 0);
/* We expect OOM to be handled by the alloc function */
Assert(ret != NULL);
VALGRIND_MEMPOOL_ALLOC(context, ret, size);
return ret;
}Three things in that comment deserve attention.
First, the out-of-memory check lives in the allocator, not here. The
generic wrapper does not test for NULL and call ereport(ERROR). It
cannot: an allocation that is satisfied from an existing free list never
touches the operating system, so there is no failure to report. Only when
the allocator actually has to malloc() more memory from the OS can it
fail — and that is where the check belongs:
void *
MemoryContextAllocationFailure(MemoryContext context, Size size, int flags)
{
if ((flags & MCXT_ALLOC_NO_OOM) == 0)
{
if (TopMemoryContext)
MemoryContextStats(TopMemoryContext);
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory"),
errdetail("Failed on request of size %zu in memory context \"%s\".",
size, context->name)));
}
return NULL;
}Second, the tail call. Because the comment says “not having to
execute any instructions after this call”, palloc() compiles to
something close to a jump: load context->methods->alloc, load size,
call it, return the result. The compiler is free to turn the final
return ret; into a tail jump into AllocSetAlloc(), avoiding a stack
frame in the wrapper entirely. Add an instruction after the call and
that optimisation evaporates. This is why the comment ends with a
warning to future contributors: “If you’re considering adding code after
this call, consider making it the responsibility of the ‘alloc’ function
instead.”
Third, palloc0 does not call palloc. It is a copy of the same
function with the MemSetAligned bolted on, for the same reason —
/* duplicates MemoryContextAlloc to avoid increased overhead */. The
extra call would cost a stack frame on one of the hottest paths in the
server.
The MemoryContextAlloc() variant, which takes an explicit context
rather than using CurrentMemoryContext, carries the identical comment
and the identical shape:
void *
MemoryContextAlloc(MemoryContext context, Size size)
{
void *ret;
Assert(MemoryContextIsValid(context));
AssertNotInCriticalSection(context);
context->isReset = false;
/*
* For efficiency reasons, we purposefully offload the handling of
* allocation failures to the MemoryContextMethods implementation as this
* allows these checks to be performed only when an actual malloc needs to
* be done to request more memory from the OS. Additionally, not having
* to execute any instructions after this call allows the compiler to use
* the sibling call optimization. If you're considering adding code after
* this call, consider making it the responsibility of the 'alloc'
* function instead.
*/
ret = context->methods->alloc(context, size, 0);
VALGRIND_MEMPOOL_ALLOC(context, ret, size);
return ret;
}How pfree knows where to send the pointer
The method-table dispatch works in both directions. pfree() does not
take a context argument — a chunk can be freed from any context — so it
has to find the right methods from the pointer alone. The trick is an
8-byte header stored immediately before every chunk.
From src/include/utils/memutils_memorychunk.h:
typedef struct MemoryChunk
{
#ifdef MEMORY_CONTEXT_CHECKING
Size requested_size;
#endif
/* bitfield for storing details about the chunk */
uint64 hdrmask; /* must be last */
} MemoryChunk;In a normal build MemoryChunk is exactly 8 bytes. The file’s header
comment describes how those 64 bits are carved up:
The MemoryChunk contains a uint64 field named ‘hdrmask’. This field is used to encode 4 separate pieces of information. Starting with the least significant bits of ‘hdrmask’, the bit space is reserved as follows:
- 4-bits to indicate the MemoryContextMethodID as defined by MEMORY_CONTEXT_METHODID_MASK
- 1-bit to denote an “external” chunk (see below)
- 30-bits reserved for the MemoryContext to use for anything it requires. Most MemoryContexts likely want to store the size of the chunk here.
- 30-bits for the number of bytes that must be subtracted from the chunk to obtain the address of the block that the chunk is stored on.
And the trick that makes it add up to 64 bits:
If you’re paying close attention, you’ll notice this adds up to 65 bits rather than 64 bits. This is because the highest-order bit of #3 is the same bit as the lowest-order bit of #4. We can do this as we insist that the chunk and block pointers are both MAXALIGNed, therefore the relative offset between those will always be a MAXALIGNed value which means the lowest order bit is always 0.
So one 64-bit word carries the method ID (4 bits, to dispatch pfree),
an “external” flag, 30 bits of context-specific payload, and a 30-bit
offset back to the owning block. Reading it back:
static inline void
MemoryChunkSetHdrMask(MemoryChunk *chunk, void *block,
Size value, MemoryContextMethodID methodid)
{
Size blockoffset = (char *) chunk - (char *) block;
Assert((char *) chunk >= (char *) block);
Assert((blockoffset & MEMORYCHUNK_BLOCKOFFSET_MASK) == blockoffset);
Assert(value <= MEMORYCHUNK_MAX_VALUE);
Assert((int) methodid <= MEMORY_CONTEXT_METHODID_MASK);
chunk->hdrmask = (((uint64) blockoffset) << MEMORYCHUNK_BLOCKOFFSET_BASEBIT) |
(((uint64) value) << MEMORYCHUNK_VALUE_BASEBIT) |
methodid;
}GetMemoryChunkMethodID() in mcxt.c reads the low 4 bits, and
pfree() becomes a one-line indirect call:
void
pfree(void *pointer)
{
#ifdef USE_VALGRIND
MemoryContext context = GetMemoryChunkContext(pointer);
#endif
MCXT_METHOD(pointer, free_p) (pointer);
VALGRIND_MEMPOOL_FREE(context, pointer);
}The 8-byte header is the price of admission. Every allocation pays it. For an 8-byte allocation, that means the usable chunk is 8 bytes and the header is 8 bytes — a 100% overhead. This is precisely why the other allocator types exist.
The AllocSet: power-of-two size classes
aset.c is the default allocator, and it is where all the interesting
tricks live. The file’s own header comment describes the design:
/*--------------------
* Chunk freelist k holds chunks of size 1 << (k + ALLOC_MINBITS),
* for k = 0 .. ALLOCSET_NUM_FREELISTS-1.
*
* Note that all chunks in the freelists have power-of-2 sizes. This
* improves recyclability: we may waste some space, but the wasted space
* should stay pretty constant as requests are made and released.
*
* A request too large for the last freelist is handled by allocating a
* dedicated block from malloc(). The block still has a block header and
* chunk header, but when the chunk is freed we'll return the whole block
* to malloc(), not put it on our freelists.
*
* CAUTION: ALLOC_MINBITS must be large enough so that
* 1<<ALLOC_MINBITS is at least MAXALIGN,
* or we may fail to align the smallest chunks adequately.
* 8-byte alignment is enough on all currently known machines. This 8-byte
* minimum also allows us to store a pointer to the next freelist item within
* the chunk of memory itself.
*
* With the current parameters, request sizes up to 8K are treated as chunks,
* larger requests go into dedicated blocks. Change ALLOCSET_NUM_FREELISTS
* to adjust the boundary point; and adjust ALLOCSET_SEPARATE_THRESHOLD in
* memutils.h to agree.
*--------------------
*/
#define ALLOC_MINBITS 3 /* smallest chunk size is 8 bytes */
#define ALLOCSET_NUM_FREELISTS 11
#define ALLOC_CHUNK_LIMIT (1 << (ALLOCSET_NUM_FREELISTS-1+ALLOC_MINBITS))
/* Size of largest chunk that we use a fixed size for */
#define ALLOC_CHUNK_FRACTION 4
/* We allow chunks to be at most 1/4 of maxBlockSize (less overhead) */Eleven free lists. The chunk sizes are:
| Free list index | Chunk size | Notes |
|---|---|---|
| 0 | 8 bytes | 1 << 3; also the minimum chunk |
| 1 | 16 bytes | |
| 2 | 32 bytes | |
| 3 | 64 bytes | |
| 4 | 128 bytes | |
| 5 | 256 bytes | |
| 6 | 512 bytes | |
| 7 | 1 kB | |
| 8 | 2 kB | |
| 9 | 4 kB | |
| 10 | 8 kB | ALLOC_CHUNK_LIMIT |
| — | > 8 kB | dedicated malloc() block |
A request of 100 bytes lands in free list 4 and gets a 128-byte chunk. A request of 129 bytes lands in free list 5 and gets 256 bytes. The waste is bounded by a factor of two and, crucially, constant — which is the whole point. A 100-byte allocation freed and re-requested always gets the same 128-byte chunk, forever, because it goes back on free list 4.
The context itself:
typedef struct AllocSetContext
{
MemoryContextData header; /* Standard memory-context fields */
/* Info about storage allocated in this context: */
AllocBlock blocks; /* head of list of blocks in this set */
MemoryChunk *freelist[ALLOCSET_NUM_FREELISTS]; /* free chunk lists */
/* Allocation parameters for this context: */
uint32 initBlockSize; /* initial block size */
uint32 maxBlockSize; /* maximum block size */
uint32 nextBlockSize; /* next block size to allocate */
uint32 allocChunkLimit; /* effective chunk size limit */
/* freelist this context could be put in, or -1 if not a candidate: */
int freeListIndex; /* index in context_freelists[], or -1 */
} AllocSetContext;Eleven pointers, plus four sizing parameters, plus one integer for the context recycling pool. That is the entire state of an AllocSet.
And the block:
typedef struct AllocBlockData
{
AllocSet aset; /* aset that owns this block */
AllocBlock prev; /* prev block in aset's blocks list, if any */
AllocBlock next; /* next block in aset's blocks list, if any */
char *freeptr; /* start of free space in this block */
char *endptr; /* end of space in this block */
} AllocBlockData;Blocks are obtained from malloc() and carved into chunks. freeptr
walks forward as the active block fills up.
Computing the free list index
Given a request size, the allocator has to decide which of the eleven
free lists to use. That is AllocSetFreeIndex():
static inline int
AllocSetFreeIndex(Size size)
{
int idx;
if (size > (1 << ALLOC_MINBITS))
{
/*----------
* At this point we must compute ceil(log2(size >> ALLOC_MINBITS)).
* This is the same as
* pg_leftmost_one_pos32((size - 1) >> ALLOC_MINBITS) + 1
* or equivalently
* pg_leftmost_one_pos32(size - 1) - ALLOC_MINBITS + 1
*
* However, for platforms without intrinsic support, we duplicate the
* logic here, allowing an additional optimization. It's reasonable
* to assume that ALLOC_CHUNK_LIMIT fits in 16 bits, so we can unroll
* the byte-at-a-time loop in pg_leftmost_one_pos32 and just handle
* the last two bytes.
*
* Yes, this function is enough of a hot-spot to make it worth this
* much trouble.
*----------
*/
#ifdef HAVE_BITSCAN_REVERSE
idx = pg_leftmost_one_pos32((uint32) size - 1) - ALLOC_MINBITS + 1;
#else
uint32 t,
tsize;
/* Statically assert that we only have a 16-bit input value. */
StaticAssertDecl(ALLOC_CHUNK_LIMIT < (1 << 16),
"ALLOC_CHUNK_LIMIT must be less than 64kB");
tsize = size - 1;
t = tsize >> 8;
idx = t ? pg_leftmost_one_pos[t] + 8 : pg_leftmost_one_pos[tsize];
idx -= ALLOC_MINBITS - 1;
#endif
Assert(idx < ALLOCSET_NUM_FREELISTS);
}
else
idx = 0;
return idx;
}“Yes, this function is enough of a hot-spot to make it worth this much
trouble.” That is a real PostgreSQL comment, and it explains the shape
of the code. pg_leftmost_one_pos32() is the count-leading-zeros
primitive:
static inline int
pg_leftmost_one_pos32(uint32 word)
{
#ifdef HAVE__BUILTIN_CLZ
Assert(word != 0);
return 31 - __builtin_clz(word);On x86 this compiles to a single bsr instruction (or lzcnt). One
instruction to convert “how big is this request?” into “which free list
do I use?”. The portable fallback uses a 256-entry byte table and only
looks at the top two bytes of the value, which is why the code carries
the StaticAssertDecl about ALLOC_CHUNK_LIMIT fitting in 16 bits.
The index is stored in the chunk header
Here is the part that makes pfree() cheap. When a chunk is freed, the
allocator has to know which free list to put it back on. It could
recompute it from the chunk’s size — but it doesn’t need to, because the
chunk header has 30 spare bits.
Recall the MemoryChunk layout: bits 5–34 are a 30-bit “value” field
that the context may use for whatever it likes. aset.c uses it to
store the free list index:
/* store the free list index in the value field */
MemoryChunkSetHdrMask(chunk, block, fidx, MCTX_ASET_ID);and AllocSetFree() reads it straight back out:
fidx = MemoryChunkGetValue(chunk);
Assert(FreeListIdxIsValid(fidx));
link = GetFreeListLink(chunk);No clz, no lookup table, no search. One shift and one mask. The
comment in aset.c makes the rationale explicit:
Note that all chunks in the freelists have power-of-2 sizes. This improves recyclability: we may waste some space, but the wasted space should stay pretty constant as requests are made and released.
The free list index is the size class, so storing the index is exactly equivalent to storing the size — but it is 4 bits instead of 32, and it needs no arithmetic to recover.
The free list link lives inside the chunk
A free list needs a next pointer per chunk. Where does that go?
PostgreSQL puts it in the chunk’s own user data:
/*
* Obtain a AllocFreeListLink for the given chunk. Allocation sizes are
* always at least sizeof(AllocFreeListLink), so we reuse the pointer's memory
* itself to store the freelist link.
*/
#define GetFreeListLink(chkptr) \
(AllocFreeListLink *) ((char *) (chkptr) + ALLOC_CHUNKHDRSZ)This is exactly why ALLOC_MINBITS is 3 — the smallest chunk is 8
bytes, which is exactly enough to hold a pointer. When the chunk is
live, that space is the caller’s data. When the chunk is free, it is the
list link. No separate array of freelist nodes, no extra allocation.
The allocation fast path
AllocSetAlloc() is the whole story for a typical small allocation:
void *
AllocSetAlloc(MemoryContext context, Size size, int flags)
{
AllocSet set = (AllocSet) context;
AllocBlock block;
MemoryChunk *chunk;
int fidx;
Size chunk_size;
Size availspace;
Assert(AllocSetIsValid(set));
/* due to the keeper block set->blocks should never be NULL */
Assert(set->blocks != NULL);
/*
* If requested size exceeds maximum for chunks we hand the request off to
* AllocSetAllocLarge().
*/
if (size > set->allocChunkLimit)
return AllocSetAllocLarge(context, size, flags);
/*
* Request is small enough to be treated as a chunk. Look in the
* corresponding free list to see if there is a free chunk we could reuse.
* If one is found, remove it from the free list, make it again a member
* of the alloc set and return its data address.
*
* Note that we don't attempt to ensure there's space for the sentinel
* byte here. We expect a large proportion of allocations to be for sizes
* which are already a power of 2. If we were to always make space for a
* sentinel byte in MEMORY_CONTEXT_CHECKING builds, then we'd end up
* doubling the memory requirements for such allocations.
*/
fidx = AllocSetFreeIndex(size);
chunk = set->freelist[fidx];
if (chunk != NULL)
{
AllocFreeListLink *link = GetFreeListLink(chunk);
/* Allow access to the chunk header. */
VALGRIND_MAKE_MEM_DEFINED(chunk, ALLOC_CHUNKHDRSZ);
Assert(fidx == MemoryChunkGetValue(chunk));
/* pop this chunk off the freelist */
VALGRIND_MAKE_MEM_DEFINED(link, sizeof(AllocFreeListLink));
set->freelist[fidx] = link->next;
VALGRIND_MAKE_MEM_NOACCESS(link, sizeof(AllocFreeListLink));
...
return MemoryChunkGetPointer(chunk);
}
/*
* Choose the actual chunk size to allocate.
*/
chunk_size = GetChunkSizeFromFreeListIdx(fidx);
Assert(chunk_size >= size);
block = set->blocks;
availspace = block->endptr - block->freeptr;
/*
* If there is enough room in the active allocation block, we will put the
* chunk into that block. Else must start a new one.
*/
if (unlikely(availspace < (chunk_size + ALLOC_CHUNKHDRSZ)))
return AllocSetAllocFromNewBlock(context, size, flags, fidx);
/* There's enough space on the current block, so allocate from that */
return AllocSetAllocChunkFromBlock(context, block, size, chunk_size, fidx);
}The order of operations is the whole design:
- Too big? Hand off to
AllocSetAllocLarge(), which does a dedicatedmalloc(). - Free list non-empty? Pop it. This is the common case in a
long-running loop, and it costs: one
clz, one array load, one pointer swap. Nomalloc, nofree, no copying. - Room in the active block? Bump
freeptrand write the header. - Otherwise carve the tail (below) and start a new block.
Notice the Assert(fidx == MemoryChunkGetValue(chunk)); — the assertion
that the stored index really does match the list the chunk came off.
That is the invariant that makes pfree O(1) with respect to size
classes.
Carving the tail of a dying block
Step 4 is where the interesting work happens. When the active block cannot satisfy the request, PostgreSQL does not throw away the remaining space in it. It carves the remainder into free chunks, one per size class, and puts them on the free lists.
From AllocSetAllocFromNewBlock():
/*
* The existing active (top) block does not have enough room for the
* requested allocation, but it might still have a useful amount of space
* in it. Once we push it down in the block list, we'll never try to
* allocate more space from it. So, before we do that, carve up its free
* space into chunks that we can put on the set's freelists.
*
* Because we can only get here when there's less than ALLOC_CHUNK_LIMIT
* left in the block, this loop cannot iterate more than
* ALLOCSET_NUM_FREELISTS-1 times.
*/
while (availspace >= ((1 << ALLOC_MINBITS) + ALLOC_CHUNKHDRSZ))
{
AllocFreeListLink *link;
MemoryChunk *chunk;
Size availchunk = availspace - ALLOC_CHUNKHDRSZ;
int a_fidx = AllocSetFreeIndex(availchunk);
/*
* In most cases, we'll get back the index of the next larger freelist
* than the one we need to put this chunk on. The exception is when
* availchunk is exactly a power of 2.
*/
if (availchunk != GetChunkSizeFromFreeListIdx(a_fidx))
{
a_fidx--;
Assert(a_fidx >= 0);
availchunk = GetChunkSizeFromFreeListIdx(a_fidx);
}
chunk = (MemoryChunk *) (block->freeptr);
/* Prepare to initialize the chunk header. */
VALGRIND_MAKE_MEM_UNDEFINED(chunk, ALLOC_CHUNKHDRSZ);
block->freeptr += (availchunk + ALLOC_CHUNKHDRSZ);
availspace -= (availchunk + ALLOC_CHUNKHDRSZ);
/* store the freelist index in the value field */
MemoryChunkSetHdrMask(chunk, block, a_fidx, MCTX_ASET_ID);
#ifdef MEMORY_CONTEXT_CHECKING
chunk->requested_size = InvalidAllocSize; /* mark it free */
#endif
/* push this chunk onto the free list */
link = GetFreeListLink(chunk);
VALGRIND_MAKE_MEM_DEFINED(link, sizeof(AllocFreeListLink));
link->next = set->freelist[a_fidx];
VALGRIND_MAKE_MEM_NOACCESS(link, sizeof(AllocFreeListLink));
set->freelist[a_fidx] = chunk;
}Read that loop carefully. Suppose the block has 5000 bytes left. It takes the largest chunk that fits into 5000 bytes: 4096 bytes, on free list 9, leaving 904. Then 512 bytes (free list 6), leaving 392. Then 256 (free list 5), leaving 136. Then 64 (free list 3), leaving 72. Then 32 (free list 2), leaving 0. Loop ends.
So one dying block yields five reusable chunks in five different size
classes, and nothing at all is wasted — not even the 8-byte chunk
headers, which are counted against the available space as the loop
runs. Compare that to the naive behaviour of throwing the tail away:
those 5000 bytes would have been returned to malloc() (or worse, kept
and never reused), and the next 4096-byte request would have had to
malloc() again.
Then the block size doubles:
/*
* The first such block has size initBlockSize, and we double the space in
* each succeeding block, but not more than maxBlockSize.
*/
blksize = set->nextBlockSize;
set->nextBlockSize <<= 1;
if (set->nextBlockSize > set->maxBlockSize)
set->nextBlockSize = set->maxBlockSize;So a context starts with an 8 kB block (ALLOCSET_DEFAULT_INITSIZE),
then 16 kB, then 32 kB, up to maxBlockSize (8 MB by default). The
rationale is in the header comment:
The first block allocated for an allocset has size initBlockSize. Each time we have to allocate another block, we double the block size (if possible, and without exceeding maxBlockSize), so as to reduce the bookkeeping load on malloc().
Note also the retry loop for huge blocks:
/*
* We could be asking for pretty big blocks here, so cope if malloc fails.
* But give up if there's less than 1 MB or so available...
*/
while (block == NULL && blksize > 1024 * 1024)
{
blksize >>= 1;
if (blksize < required_size)
break;
block = (AllocBlock) malloc(blksize);
}Asking for 64 MB and getting a failure is survivable; asking for 64 MB when the machine has 40 MB free is not, so PostgreSQL halves the request and tries again down to 1 MB.
Chunk limits are per-context, not global
The 8 kB ALLOC_CHUNK_LIMIT is the global maximum, but each context
computes its own effective limit:
static inline uint32
AllocSetComputeChunkLimit(Size maxBlockSize)
{
uint32 allocChunkLimit;
/*
* Compute the allocation chunk size limit for this context. It can't be
* more than ALLOC_CHUNK_LIMIT because of the fixed number of freelists.
* If maxBlockSize is small then requests exceeding the maxBlockSize, or
* even a significant fraction of it, should be treated as large chunks
* too. For the typical case of maxBlockSize a power of 2, the chunk size
* limit will be at most 1/8th maxBlockSize, so that given a stream of
* requests that are all the maximum chunk size we will waste at most
* 1/8th of the allocated space.
...
*/
allocChunkLimit = ALLOC_CHUNK_LIMIT;
while ((Size) (allocChunkLimit + ALLOC_CHUNKHDRSZ) >
(Size) ((maxBlockSize - ALLOC_BLOCKHDRSZ) / ALLOC_CHUNK_FRACTION))
allocChunkLimit >>= 1;
return allocChunkLimit;
}A context with maxBlockSize of 8 kB (the ALLOCSET_SMALL_SIZES
preset) gets a chunk limit of 1 kB or so, so that a stream of maximum-size
requests cannot waste more than a quarter of the block. Small contexts
therefore do not get 8 kB chunks they can never fill.
repalloc: sometimes it is free
Growing a chunk usually means copying. But not always. AllocSetRealloc()
first checks whether the existing chunk is already big enough:
/*
* Chunk sizes are aligned to power of 2 in AllocSetAlloc(). Maybe the
* allocated area already is >= the new size. (In particular, we will
* fall out here if the requested size is a decrease.)
*/
if (oldchksize >= size)
{
...
/* Disallow access to the chunk header. */
VALGRIND_MAKE_MEM_NOACCESS(chunk, ALLOC_CHUNKHDRSZ);
return pointer;
}oldchksize is GetChunkSizeFromFreeListIdx(fidx) — the chunk size,
not the requested size. So a 40-byte allocation lives in a 64-byte chunk;
repallocing it to 60 bytes finds 64 >= 60 and returns the same
pointer, with no copy at all. The extra 24 bytes were already there,
reserved by the power-of-two quantisation.
This is a free side effect of the quantisation scheme. The waste is already paid; handing it back to the caller costs nothing.
Growing past the chunk size does require a copy, and the comment is candid about why the obvious optimisation was rejected:
/*
* Enlarge-a-small-chunk case. We just do this by brute force, ie,
* allocate a new chunk and copy the data. Since we know the existing
* data isn't huge, this won't involve any great memcpy expense, so
* it's not worth being smarter. (At one time we tried to avoid
* memcpy when it was possible to enlarge the chunk in-place, but that
* turns out to misbehave unpleasantly for repeated cycles of
* palloc/repalloc/pfree: the eventually freed chunks go into the
* wrong freelist for the next initial palloc request, and so we leak
* memory indefinitely. See pgsql-hackers archives for 2007-08-11.)
*/“the eventually freed chunks go into the wrong freelist” — enlarging in place would change the chunk’s size class, and when the chunk is later freed it would go onto a different free list than the one it came off, which permanently removes a chunk from the list it was supposed to return to. A slow leak, per cycle, forever. Good lesson.
Internal fragmentation: the trade-off
Quantising to powers of two is not free. A 100-byte request occupies 128 bytes. Here is the full picture, ignoring the 8-byte chunk header:
| Request size | Chunk size | Wasted bytes | Overhead |
|---|---|---|---|
| 1 | 8 | 7 | 700% |
| 9 | 16 | 7 | 78% |
| 17 | 32 | 15 | 88% |
| 33 | 64 | 31 | 94% |
| 65 | 128 | 63 | 97% |
| 129 | 256 | 127 | 98% |
| 257 | 512 | 255 | 99% |
| 513 | 1 kB | 511 | 100% |
| 1025 | 2 kB | 1023 | 100% |
| 2049 | 4 kB | 2047 | 100% |
| 4097 | 8 kB | 4095 | 100% |
| 8193 | dedicated block | 0 | header only |
The “overhead” column is worst-case (a request one byte above a power of two). Averaged over a uniformly distributed range of request sizes, a request wastes roughly a third of its chunk — and, crucially, the waste is predictable, which matters more than the average, because predictable overhead can be budgeted for.
The counter-argument is in the header comment:
Note that all chunks in the freelists have power-of-2 sizes. This improves recyclability: we may waste some space, but the wasted space should stay pretty constant as requests are made and released.
A general-purpose allocator that returns exactly the requested size wastes nothing on paper, but then a 100-byte free chunk can only satisfy a 100-byte request, and the space in a 700-byte gap can satisfy nothing. Quantisation guarantees that free space is always usable, at the cost of always being a bit larger than needed.
The other side of the ledger: requests above 8 kB bypass the whole
scheme and get their own malloc() block, so large allocations (index
tuples, TOAST chunks, hash table buckets) pay no quantisation waste at
all.
Recycling whole contexts
Now the second half of the story. Contexts themselves are allocated with
malloc() and freed with free(). If a query creates a per-tuple
context and destroys it per tuple, that is a malloc/free pair per
tuple — exactly the thrashing the context system was supposed to avoid.
PostgreSQL’s answer is a pool of dead contexts. The design note is
in aset.c:
/*
* Rather than repeatedly creating and deleting memory contexts, we keep some
* freed contexts in freelists so that we can hand them out again with little
* work. Before putting a context in a freelist, we reset it so that it has
* only its initial malloc chunk and no others. To be a candidate for a
* freelist, a context must have the same minContextSize/initBlockSize as
* other contexts in the list; but its maxBlockSize is irrelevant since that
* doesn't affect the size of the initial chunk.
*
* We currently provide one freelist for ALLOCSET_DEFAULT_SIZES contexts
* and one for ALLOCSET_SMALL_SIZES contexts; the latter works for
* ALLOCSET_START_SMALL_SIZES too, since only the maxBlockSize differs.
*
* Ordinarily, we re-use freelist contexts in last-in-first-out order, in
* hopes of improving locality of reference. But if there get to be too
* many contexts in the list, we'd prefer to drop the most-recently-created
* contexts in hopes of keeping the process memory map compact.
* We approximate that by simply deleting all existing entries when the list
* overflows, on the assumption that queries that allocate a lot of contexts
* will probably free them in more or less reverse order of allocation.
*
* Contexts in a freelist are chained via their nextchild pointers.
*/
#define MAX_FREE_CONTEXTS 100 /* arbitrary limit on freelist length */“Contexts in a freelist are chained via their nextchild pointers.”
That is a wonderful piece of field-hacking: a deleted context is no
longer in the context tree, so its nextchild field (which links it to
its siblings) is dead weight. It is reused as the free-list link.
The pool itself:
typedef struct AllocSetFreeList
{
int num_free; /* current list length */
AllocSetContext *first_free; /* list header */
} AllocSetFreeList;
/* context_freelists[0] is for default params, [1] for small params */
static AllocSetFreeList context_freelists[2] =
{
{
0, NULL
},
{
0, NULL
}
};Two pools: one for “ordinary” contexts and one for “small” contexts,
matching the two size presets in memutils.h:
#define ALLOCSET_DEFAULT_MINSIZE 0
#define ALLOCSET_DEFAULT_INITSIZE (8 * 1024)
#define ALLOCSET_DEFAULT_MAXSIZE (8 * 1024 * 1024)
#define ALLOCSET_DEFAULT_SIZES \
ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE
#define ALLOCSET_SMALL_MINSIZE 0
#define ALLOCSET_SMALL_INITSIZE (1 * 1024)
#define ALLOCSET_SMALL_MAXSIZE (8 * 1024)
#define ALLOCSET_SMALL_SIZES \
ALLOCSET_SMALL_MINSIZE, ALLOCSET_SMALL_INITSIZE, ALLOCSET_SMALL_MAXSIZEDeleting a context puts it on the pool
AllocSetDelete() is where the recycling happens:
/*
* If the context is a candidate for a freelist, put it into that freelist
* instead of destroying it.
*/
if (set->freeListIndex >= 0)
{
AllocSetFreeList *freelist = &context_freelists[set->freeListIndex];
/*
* Reset the context, if it needs it, so that we aren't hanging on to
* more than the initial malloc chunk.
*/
if (!context->isReset)
MemoryContextResetOnly(context);
/*
* If the freelist is full, just discard what's already in it. See
* comments with context_freelists[].
*/
if (freelist->num_free >= MAX_FREE_CONTEXTS)
{
while (freelist->first_free != NULL)
{
AllocSetContext *oldset = freelist->first_free;
freelist->first_free = (AllocSetContext *) oldset->header.nextchild;
freelist->num_free--;
/* Destroy the context's vpool --- see notes below */
VALGRIND_DESTROY_MEMPOOL(oldset);
/* All that remains is to free the header/initial block */
free(oldset);
}
Assert(freelist->num_free == 0);
}
/* Now add the just-deleted context to the freelist. */
set->header.nextchild = (MemoryContext) freelist->first_free;
freelist->first_free = set;
freelist->num_free++;
return;
}Push at the head → LIFO. The comment explains why: “in hopes of improving locality of reference”. The most recently freed context was also the most recently allocated, so it is the one whose memory is hottest in the cache.
The pool is capped at 100 contexts. When it overflows, the entire pool is thrown away rather than just the oldest entry — because, as the comment says, “queries that allocate a lot of contexts will probably free them in more or less reverse order of allocation”, so dropping everything keeps the process memory map compact.
Creating a context takes one from the pool
The other half is AllocSetContextCreateInternal():
/*
* Check whether the parameters match either available freelist. We do
* not need to demand a match of maxBlockSize.
*/
if (minContextSize == ALLOCSET_DEFAULT_MINSIZE &&
initBlockSize == ALLOCSET_DEFAULT_INITSIZE)
freeListIndex = 0;
else if (minContextSize == ALLOCSET_SMALL_MINSIZE &&
initBlockSize == ALLOCSET_SMALL_INITSIZE)
freeListIndex = 1;
else
freeListIndex = -1;
/*
* If a suitable freelist entry exists, just recycle that context.
*/
if (freeListIndex >= 0)
{
AllocSetFreeList *freelist = &context_freelists[freeListIndex];
if (freelist->first_free != NULL)
{
/* Remove entry from freelist */
set = freelist->first_free;
freelist->first_free = (AllocSet) set->header.nextchild;
freelist->num_free--;
/* nextBlockSize should have been reset in AllocSetReset() */
Assert(set->nextBlockSize == set->initBlockSize);
if (set->maxBlockSize != maxBlockSize)
{
/*
* Update its maxBlockSize and allocChunkLimit; everything
* else should be OK.
*/
set->maxBlockSize = maxBlockSize;
set->allocChunkLimit = AllocSetComputeChunkLimit(maxBlockSize);
}
/* Reinitialize its header, installing correct name and parent */
MemoryContextCreate((MemoryContext) set,
T_AllocSetContext,
MCTX_ASET_ID,
parent,
name);
((MemoryContext) set)->mem_allocated =
KeeperBlock(set)->endptr - ((char *) set);
return (MemoryContext) set;
}
}No malloc. The context object is reused verbatim, only its header is
re-initialised and its maxBlockSize adjusted (the block size limit is
the only parameter that does not affect the initial chunk’s size, so a
context can be recycled across different maxBlockSize values).
The result: a per-tuple context costs zero system allocations once the pool is warm. Create, use, destroy, repeat, and the whole cycle is pointer arithmetic.
Reset versus delete
Two operations, and the difference matters:
MemoryContextReset()— free everything allocated in the context (and its children), but keep the context object itself. The context remains usable.MemoryContextDelete()— free the context object too. The name is gone.
For AllocSet, the two differ in one crucial detail: the keeper block.
/*
* AllocSetReset
* Frees all memory which is allocated in the given set.
*
* Actually, this routine has some discretion about what to do.
* It should mark all allocated chunks freed, but it need not necessarily
* give back all the resources the set owns. Our actual implementation is
* that we give back all but the "keeper" block (which we must keep, since
* it shares a malloc chunk with the context header). In this way, we don't
* thrash malloc() when a context is repeatedly reset after small allocations,
* which is typical behavior for per-tuple contexts.
*/The “keeper block” is the region of memory immediately following the
AllocSetContext struct, in the same malloc() chunk:
/* Obtain the keeper block for an allocation set */
#define KeeperBlock(set) \
((AllocBlock) (((char *) set) + MAXALIGN(sizeof(AllocSetContext))))
/* Check if the block is the keeper block of the given allocation set */
#define IsKeeperBlock(set, block) ((block) == (KeeperBlock(set)))So the very first malloc() a context makes contains both the context
header and its first block. AllocSetReset() keeps that block, wipes
its contents, and free()s everything else:
block = set->blocks;
/* New blocks list will be just the keeper block */
set->blocks = KeeperBlock(set);
while (block != NULL)
{
AllocBlock next = block->next;
if (IsKeeperBlock(set, block))
{
/* Reset the block, but don't return it to malloc */
char *datastart = ((char *) block) + ALLOC_BLOCKHDRSZ;
...
block->freeptr = datastart;
block->prev = NULL;
block->next = NULL;
}
else
{
/* Normal case, release the block */
context->mem_allocated -= block->endptr - ((char *) block);
...
free(block);
}
block = next;
}So a per-tuple context that allocates 200 bytes and is reset per tuple
does exactly one malloc() (at creation) and zero free()s (until it
is deleted). Compare with malloc/free per tuple, which would be two
system calls per tuple.
AllocSetDelete() is the version that gives everything back, and it is
also the version that puts the context on the recycling pool.
The other allocators
aset.c is the general-purpose one, but PostgreSQL ships four context
types, because different allocation patterns want different trade-offs.
The method table in mcxt.c lists them all:
static const MemoryContextMethods mcxt_methods[] = {
/* aset.c */
[MCTX_ASET_ID].alloc = AllocSetAlloc,
...
/* generation.c */
[MCTX_GENERATION_ID].alloc = GenerationAlloc,
...
/* slab.c */
[MCTX_SLAB_ID].alloc = SlabAlloc,
...
/* alignedalloc.c */
[MCTX_ALIGNED_REDIRECT_ID].alloc = NULL, /* not required */
...
/* bump.c */
[MCTX_BUMP_ID].alloc = BumpAlloc,
...Bump: no chunk header at all
The bump.c header comment states the design goal:
Bump is a MemoryContext implementation designed for memory usages which require allocating a large number of chunks, none of which ever need to be pfree’d or realloc’d. Chunks allocated by this context have no chunk header and operations which ordinarily require looking at the chunk header cannot be performed. For example, pfree, realloc, GetMemoryChunkSpace and GetMemoryChunkContext are all not possible with bump allocated chunks. The only way to release memory allocated by this context type is to reset or delete the context.
And the size of the header is literally zero:
/* No chunk header unless built with MEMORY_CONTEXT_CHECKING */
#ifdef MEMORY_CONTEXT_CHECKING
#define Bump_CHUNKHDRSZ sizeof(MemoryChunk)
#else
#define Bump_CHUNKHDRSZ 0
#endifNo 8-byte header per allocation. The allocation is just a bump of a pointer:
/* point the freeptr beyond this chunk */
block->freeptr += (Bump_CHUNKHDRSZ + chunk_size);
Assert(block->freeptr <= block->endptr);The cost is that the operations that need the header are refused outright:
/*
* BumpFree
* Unsupported.
*/
void
BumpFree(void *pointer)
{
elog(ERROR, "%s is not supported by the bump memory allocator", "pfree");
}Same for BumpRealloc, BumpGetChunkContext and BumpGetChunkSpace.
The MEMORY_CONTEXT_CHECKING build adds the header back precisely so
that these mistakes are caught rather than silently corrupting memory.
Where is Bump used? Wherever there are “a large number of short-lived
chunks where performance matters” — hash aggregation, tuplesort,
set operations, nodeSubplan, the TID store. The header comment
explains the shape of the win:
Because bump allocated chunks don’t have a chunk header, it can fit more chunks on each block. This means we can do more with less memory and fewer cache lines.
Slab: one size, and the free list lives in the chunk
slab.c is for the opposite extreme: many objects of exactly the same
size. The chunk size is fixed at context creation time:
MemoryContext
SlabContextCreate(MemoryContext parent,
const char *name,
Size blockSize,
Size chunkSize)and enforced at allocation time:
/*
* Make sure we only allow correct request size. This doubles as the
* MemoryContextCheckSize check.
*/
if (unlikely(size != slab->chunkSize))
SlabAllocInvalidSize(context, size);The free list is stored inside the free chunk, exactly as in
aset.c — but with an important difference. In aset.c the chunk’s
user data holds the link; in slab.c the link is written over the
start of the chunk’s data area, and the chunk size is forced to be at
least a pointer:
/*
* Ensure there's space to store the pointer to the next free chunk
* in the memory of the (otherwise) unused allocation.
*/
if (chunkSize < sizeof(MemoryChunk *))
chunkSize = sizeof(MemoryChunk *); /* push this chunk onto the head of the block's free list */
*(MemoryChunk **) pointer = block->freehead;
block->freehead = chunk;The interesting policy in slab.c is “fullest block first”. The
header comment explains the motivation:
Slab can also help reduce memory fragmentation in cases where longer-lived chunks remain stored on blocks while most of the other chunks have already been pfree’d. We give priority to putting new allocations into the “fullest” block. This help avoid having too many sparsely used blocks around and allows blocks to more easily become completely unused which allows them to be eventually free’d.
The implementation is a small array of lists, partitioned by how many free chunks a block has:
/*
* The number of partitions to divide the blocklist into based their number
* of free chunks. There must be at least 2.
*/
#define SLAB_BLOCKLIST_COUNT 3with a deliberately clever index computation:
static inline int32
SlabBlocklistIndex(SlabContext *slab, int nfree)
{
int32 index;
int32 blocklist_shift = slab->blocklist_shift;
Assert(nfree >= 0 && nfree <= slab->chunksPerBlock);
/*
* Determine the blocklist index based on the number of free chunks. We
* must ensure that 0 free chunks is dedicated to index 0. Everything
* else must be >= 1 and < SLAB_BLOCKLIST_COUNT.
*
* To make this as efficient as possible, we exploit some two's complement
* arithmetic where we reverse the sign before bit shifting. This results
* in an nfree of 0 using index 0 and anything non-zero staying non-zero.
* This is exploiting 0 and -0 being the same in two's complement. When
* we're done, we just need to flip the sign back over again for a
* positive index.
*/
index = -((-nfree) >> blocklist_shift);
if (nfree == 0)
Assert(index == 0);
else
Assert(index >= 1 && index < SLAB_BLOCKLIST_COUNT);
return index;
}-((-nfree) >> shift) — for nfree == 0 this is -0 == 0, and for
anything else it is a positive number. Two’s complement does the work.
The high-water-mark trick
slab.c has one more elegant trick. When a fresh block is allocated,
all of its chunks are free — but writing out a free list for every chunk
would mean touching the entire block at creation time. Instead, slab.c
keeps a pointer to the next never-used chunk and a count of how many
there are:
typedef struct SlabBlock
{
SlabContext *slab; /* owning context */
int32 nfree; /* number of chunks on free + unused chunks */
int32 nunused; /* number of unused chunks */
MemoryChunk *freehead; /* pointer to the first free chunk */
MemoryChunk *unused; /* pointer to the next unused chunk */
dlist_node node; /* doubly-linked list for blocklist[] */
} SlabBlock;The header comment explains it:
When we allocate a new block, technically all chunks are free, however, to avoid having to write out the entire block to set the linked list for the free chunks for every chunk in the block, we instead store a pointer to the next “unused” chunk on the block and keep track of how many of these unused chunks there are. When a new block is malloc’d, all chunks are unused. The unused pointer starts with the first chunk on the block and as chunks are allocated, the unused pointer is incremented. As chunks are pfree’d, the unused pointer never goes backwards. The unused pointer can be thought of as a high watermark for the maximum number of chunks in the block which have been in use concurrently. When a chunk is pfree’d the chunk is put onto the head of the free list and the unused pointer is not changed. We only consume more unused chunks if we run out of free chunks on the free list. This method effectively gives priority to using previously used chunks over previously unused chunks, which should perform better due to CPU caching effects.
A high-water mark for concurrency. Freed chunks are reused before virgin space is touched, so the hot parts of the block stay hot in cache.
Generation: for FIFO lifetimes
The fourth allocator, generation.c, is for the case where chunks are
freed roughly in the order they were allocated:
This memory context is based on the assumption that the chunks are freed roughly in the same order as they were allocated (FIFO), or in groups with similar lifespan (generations - hence the name of the context).
Instead of a global free list, each block tracks allocated and freed
chunks, and an empty block is recycled for future allocations before it
is handed back to malloc().
The temporary context
There is no src/backend/utils/mmgr/temp.c in this tree — the four
allocator types are aset.c, slab.c, generation.c and bump.c, and
the file listing in src/backend/utils/mmgr/ confirms it. The “temporary
context” of PostgreSQL’s design is instead a policy, described in the
README:
When creating a prepared statement, the parse and plan trees will be built in a temporary context that’s a child of MessageContext (so that it will go away automatically upon error). On success, the finished plan is copied to the prepared statement’s private context, and the temp context is released; this allows planner temporary space to be recovered before execution begins.
The word “temporary” here describes the lifetime, not the allocator
type. It is an ordinary AllocSetContext created with
AllocSetContextCreate() and destroyed with MemoryContextDelete() once
the plan has been copied out. What makes it “temporary” is that it is a
child of MessageContext and is destroyed — not reset — once the planner
is finished with it.
The executor’s per-tuple contexts work the same way, and they are where the context recycling machinery pays for itself most visibly.
Caveats
Memory is not returned to the OS promptly. pfree() on a chunk
smaller than 8 kB does not call free(); it pushes the chunk onto a
free list and moves on. The memory stays in the process. Only a context
reset or delete, or the release of a dedicated large block, actually
hands memory back — and even then, whether the OS reclaims it is
malloc’s business, not PostgreSQL’s. A backend that allocated 1 GB of
short-lived chunks will keep that 1 GB resident until the contexts
holding it are reset.
Fragmentation is real, but bounded. Within a context, the free lists keep chunks segregated by size class, so a freed 100-byte chunk cannot satisfy a 1000-byte request. That is deliberate (recyclability over density), but it means a context whose allocation size distribution changes over time will accumulate idle chunks in the wrong classes.
Reset is not delete. MemoryContextReset() keeps the context object
and its keeper block; MemoryContextDelete() frees everything and (for
an AllocSet with poolable parameters) puts the context object itself on
the recycling pool. Choosing reset where you meant delete leaves the
context alive; choosing delete where you meant reset churns the pool.
pfree on a bump chunk is an error, not a no-op. The bump allocator
has no chunk header, so it cannot find the freelist to return the chunk
to. BumpFree() raises elog(ERROR, "pfree is not supported by the bump memory allocator"). If you are using a bump context, you must let the
whole context go.
repalloc on a slab chunk is only allowed at the same size. A slab
context allocates one fixed size, and SlabRealloc() errors out for any
other size:
/* can't do actual realloc with slab, but let's try to be gentle */
if (size == slab->chunkSize)
return pointer;
elog(ERROR, "slab allocator does not support realloc()");The 8-byte header is real overhead. For an AllocSet, every
allocation pays 8 bytes of header plus up to 2× quantisation waste.
That is why Slab, Bump and Generation exist, and why
MemoryContextAllocAligned() has to allocate extra bytes to build a
“redirection” chunk for the alignment.
What it all means in practice
palloc is a size-class allocator, not a heap. If you are porting
C code that assumes malloc semantics — variable sizes, exact
accounting, memory returned promptly — some of that will not hold.
The 8 kB threshold is a cliff, not a slope. Requests of 8192 bytes
or less are quantised chunks in a shared block; requests of 8193 bytes
or more get their own malloc(). That is why
ALLOCSET_SEPARATE_THRESHOLD exists in memutils.h — “tuplesort/tuplestore
need to know it” — so they can account for the difference.
repalloc is often free, and that is not a coincidence. The
power-of-two quantisation already reserved the space. If you are writing
code that grows a buffer, repalloc within the same size class costs
nothing.
Free-list churn is the price of the free list. The pfree path
reads the index out of the chunk header and pushes the chunk onto a
list; it does not coalesce with neighbours and does not return memory.
The payoff is that the next palloc of the same size class is a single
array load and a pointer swap.
Contexts are the unit of memory lifetime, and recycling them is what
makes per-tuple contexts viable. The difference between “allocate a
per-tuple context with malloc/free” and “reuse one from the pool” is
two system calls per tuple versus none.
Do not allocate in the wrong context. The CurrentMemoryContext
pointer is the single biggest source of memory leaks in PostgreSQL
extensions. The README is blunt about it: “Only in very
circumscribed code should it ever point at a context having greater than
transaction lifespan, since doing so risks permanent memory leaks.”
Further reading
src/backend/utils/mmgr/README— the design overview: contexts, lifetimes, the tree, and why they exist.src/backend/utils/mmgr/mcxt.c— the context-type-independent layer:MemoryContextAlloc(),palloc(), the method table, and the OOM handling.src/backend/utils/mmgr/aset.c— the default allocator: size classes,AllocSetFreeIndex(), tail carving,AllocSetRealloc(), and context recycling.src/backend/utils/mmgr/bump.c— the header-less bump allocator.src/backend/utils/mmgr/slab.c— the fixed-size allocator: fullest block first, and the high-water mark.src/backend/utils/mmgr/generation.c— the FIFO/generation allocator.src/backend/utils/mmgr/alignedalloc.c— aligned allocation via a redirection chunk.src/include/utils/memutils.h— the public API and theALLOCSET_*size presets.src/include/utils/memutils_memorychunk.h— the 8-byte chunk header and its bit layout.src/include/nodes/memnodes.h—MemoryContextDataand theMemoryContextMethodsvtable.src/include/port/pg_bitutils.h—pg_leftmost_one_pos32(), the count-leading-zeros primitive behind the size-class lookup.src/include/utils/palloc.h— thepalloc/pfree/repallocAPI and its documented differences from the C library.- The
pg_backend_memory_contextssystem view for runtime inspection of every live context in the backend.