diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2017-12-13 13:55:12 -0500 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2017-12-13 13:55:16 -0500 |
commit | 9fa6f00b1308dd10da4eca2f31ccbfc7b35bb461 (patch) | |
tree | 8d90b8c780dc03890a2b638b3058c44ee26700d7 /src/backend/utils/mmgr/aset.c | |
parent | 632b03da31cbbf4d32193d35031d301bd50d2679 (diff) | |
download | postgresql-9fa6f00b1308dd10da4eca2f31ccbfc7b35bb461.tar.gz postgresql-9fa6f00b1308dd10da4eca2f31ccbfc7b35bb461.zip |
Rethink MemoryContext creation to improve performance.
This patch makes a number of interrelated changes to reduce the overhead
involved in creating/deleting memory contexts. The key ideas are:
* Include the AllocSetContext header of an aset.c context in its first
malloc request, rather than allocating it separately in TopMemoryContext.
This means that we now always create an initial or "keeper" block in an
aset, even if it never receives any allocation requests.
* Create freelists in which we can save and recycle recently-destroyed
asets (this idea is due to Robert Haas).
* In the common case where the name of a context is a constant string,
just store a pointer to it in the context header, rather than copying
the string.
The first change eliminates a palloc/pfree cycle per context, and
also avoids bloat in TopMemoryContext, at the price that creating
a context now involves a malloc/free cycle even if the context never
receives any allocations. That would be a loser for some common
usage patterns, but recycling short-lived contexts via the freelist
eliminates that pain.
Avoiding copying constant strings not only saves strlen() and strcpy()
overhead, but is an essential part of the freelist optimization because
it makes the context header size constant. Currently we make no
attempt to use the freelist for contexts with non-constant names.
(Perhaps someday we'll need to think harder about that, but in current
usage, most contexts with custom names are long-lived anyway.)
The freelist management in this initial commit is pretty simplistic,
and we might want to refine it later --- but in common workloads that
will never matter because the freelists will never get full anyway.
To create a context with a non-constant name, one is now required to
call AllocSetContextCreateExtended and specify the MEMCONTEXT_COPY_NAME
option. AllocSetContextCreate becomes a wrapper macro, and it includes
a test that will complain about non-string-literal context name
parameters on gcc and similar compilers.
An unfortunate side effect of making AllocSetContextCreate a macro is
that one is now *required* to use the size parameter abstraction macros
(ALLOCSET_DEFAULT_SIZES and friends) with it; the pre-9.6 habit of
writing out individual size parameters no longer works unless you
switch to AllocSetContextCreateExtended.
Internally to the memory-context-related modules, the context creation
APIs are simplified, removing the rather baroque original design whereby
a context-type module called mcxt.c which then called back into the
context-type module. That saved a bit of code duplication, but not much,
and it prevented context-type modules from exercising control over the
allocation of context headers.
In passing, I converted the test-and-elog validation of aset size
parameters into Asserts to save a few more cycles. The original thought
was that callers might compute size parameters on the fly, but in practice
nobody does that, so it's useless to expend cycles on checking those
numbers in production builds.
Also, mark the memory context method-pointer structs "const",
just for cleanliness.
Discussion: https://postgr.es/m/2264.1512870796@sss.pgh.pa.us
Diffstat (limited to 'src/backend/utils/mmgr/aset.c')
-rw-r--r-- | src/backend/utils/mmgr/aset.c | 370 |
1 files changed, 253 insertions, 117 deletions
diff --git a/src/backend/utils/mmgr/aset.c b/src/backend/utils/mmgr/aset.c index 1bd1c34fdef..1519da05d21 100644 --- a/src/backend/utils/mmgr/aset.c +++ b/src/backend/utils/mmgr/aset.c @@ -93,6 +93,9 @@ * * Blocks allocated to hold oversize chunks do not follow this rule, however; * they are just however big they need to be to hold that single chunk. + * + * Also, if a minContextSize is specified, the first block has that size, + * and then initBlockSize is used for the next one. *-------------------- */ @@ -113,7 +116,7 @@ typedef void *AllocPointer; * * Note: header.isReset means there is nothing for AllocSetReset to do. * This is different from the aset being physically empty (empty blocks list) - * because we may still have a keeper block. It's also different from the set + * because we will still have a keeper block. It's also different from the set * being logically empty, because we don't attempt to detect pfree'ing the * last active chunk. */ @@ -127,8 +130,11 @@ typedef struct AllocSetContext Size initBlockSize; /* initial block size */ Size maxBlockSize; /* maximum block size */ Size nextBlockSize; /* next block size to allocate */ + Size headerSize; /* allocated size of context header */ Size allocChunkLimit; /* effective chunk size limit */ - AllocBlock keeper; /* if not NULL, keep this block over resets */ + AllocBlock keeper; /* keep this block over resets */ + /* freelist this context could be put in, or -1 if not a candidate: */ + int freeListIndex; /* index in context_freelists[], or -1 */ } AllocSetContext; typedef AllocSetContext *AllocSet; @@ -216,12 +222,56 @@ typedef struct AllocChunkData ((AllocPointer)(((char *)(chk)) + ALLOC_CHUNKHDRSZ)) /* + * 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. Also, candidate contexts + * *must not* use MEMCONTEXT_COPY_NAME since that would make their header size + * variable. (We currently insist that all flags be zero, since other flags + * would likely make the contexts less interchangeable, too.) + * + * 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 */ + +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 + } +}; + +/* * These functions implement the MemoryContext API for AllocSet contexts. */ static void *AllocSetAlloc(MemoryContext context, Size size); static void AllocSetFree(MemoryContext context, void *pointer); static void *AllocSetRealloc(MemoryContext context, void *pointer, Size size); -static void AllocSetInit(MemoryContext context); static void AllocSetReset(MemoryContext context); static void AllocSetDelete(MemoryContext context); static Size AllocSetGetChunkSpace(MemoryContext context, void *pointer); @@ -236,11 +286,10 @@ static void AllocSetCheck(MemoryContext context); /* * This is the virtual function table for AllocSet contexts. */ -static MemoryContextMethods AllocSetMethods = { +static const MemoryContextMethods AllocSetMethods = { AllocSetAlloc, AllocSetFree, AllocSetRealloc, - AllocSetInit, AllocSetReset, AllocSetDelete, AllocSetGetChunkSpace, @@ -325,27 +374,35 @@ AllocSetFreeIndex(Size size) /* - * AllocSetContextCreate + * AllocSetContextCreateExtended * Create a new AllocSet context. * * parent: parent context, or NULL if top-level context * name: name of context (for debugging only, need not be unique) + * flags: bitmask of MEMCONTEXT_XXX option flags * minContextSize: minimum context size * initBlockSize: initial allocation block size * maxBlockSize: maximum allocation block size * - * Notes: the name string will be copied into context-lifespan storage. + * Notes: if flags & MEMCONTEXT_COPY_NAME, the name string will be copied into + * context-lifespan storage; otherwise, it had better be statically allocated. * Most callers should abstract the context size parameters using a macro - * such as ALLOCSET_DEFAULT_SIZES. + * such as ALLOCSET_DEFAULT_SIZES. (This is now *required* when going + * through the AllocSetContextCreate macro.) */ MemoryContext -AllocSetContextCreate(MemoryContext parent, - const char *name, - Size minContextSize, - Size initBlockSize, - Size maxBlockSize) +AllocSetContextCreateExtended(MemoryContext parent, + const char *name, + int flags, + Size minContextSize, + Size initBlockSize, + Size maxBlockSize) { + int freeListIndex; + Size headerSize; + Size firstBlockSize; AllocSet set; + AllocBlock block; /* Assert we padded AllocChunkData properly */ StaticAssertStmt(ALLOC_CHUNKHDRSZ == MAXALIGN(ALLOC_CHUNKHDRSZ), @@ -355,36 +412,125 @@ AllocSetContextCreate(MemoryContext parent, "padding calculation in AllocChunkData is wrong"); /* - * First, validate allocation parameters. (If we're going to throw an - * error, we should do so before the context is created, not after.) We - * somewhat arbitrarily enforce a minimum 1K block size. + * First, validate allocation parameters. Once these were regular runtime + * test and elog's, but in practice Asserts seem sufficient because nobody + * varies their parameters at runtime. We somewhat arbitrarily enforce a + * minimum 1K block size. */ - if (initBlockSize != MAXALIGN(initBlockSize) || - initBlockSize < 1024) - elog(ERROR, "invalid initBlockSize for memory context: %zu", - initBlockSize); - if (maxBlockSize != MAXALIGN(maxBlockSize) || - maxBlockSize < initBlockSize || - !AllocHugeSizeIsValid(maxBlockSize)) /* must be safe to double */ - elog(ERROR, "invalid maxBlockSize for memory context: %zu", - maxBlockSize); - if (minContextSize != 0 && - (minContextSize != MAXALIGN(minContextSize) || - minContextSize <= ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ)) - elog(ERROR, "invalid minContextSize for memory context: %zu", - minContextSize); - - /* Do the type-independent part of context creation */ - set = (AllocSet) MemoryContextCreate(T_AllocSetContext, - sizeof(AllocSetContext), - &AllocSetMethods, - parent, - name); - - /* Save allocation parameters */ + Assert(initBlockSize == MAXALIGN(initBlockSize) && + initBlockSize >= 1024); + Assert(maxBlockSize == MAXALIGN(maxBlockSize) && + maxBlockSize >= initBlockSize && + AllocHugeSizeIsValid(maxBlockSize)); /* must be safe to double */ + Assert(minContextSize == 0 || + (minContextSize == MAXALIGN(minContextSize) && + minContextSize >= 1024 && + minContextSize <= maxBlockSize)); + + /* + * Check whether the parameters match either available freelist. We do + * not need to demand a match of maxBlockSize. + */ + if (flags == 0 && + minContextSize == ALLOCSET_DEFAULT_MINSIZE && + initBlockSize == ALLOCSET_DEFAULT_INITSIZE) + freeListIndex = 0; + else if (flags == 0 && + 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--; + + /* Update its maxBlockSize; everything else should be OK */ + set->maxBlockSize = maxBlockSize; + + /* Reinitialize its header, installing correct name and parent */ + MemoryContextCreate((MemoryContext) set, + T_AllocSetContext, + set->headerSize, + sizeof(AllocSetContext), + &AllocSetMethods, + parent, + name, + flags); + + return (MemoryContext) set; + } + } + + /* Size of the memory context header, including name storage if needed */ + if (flags & MEMCONTEXT_COPY_NAME) + headerSize = MAXALIGN(sizeof(AllocSetContext) + strlen(name) + 1); + else + headerSize = MAXALIGN(sizeof(AllocSetContext)); + + /* Determine size of initial block */ + firstBlockSize = headerSize + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ; + if (minContextSize != 0) + firstBlockSize = Max(firstBlockSize, minContextSize); + else + firstBlockSize = Max(firstBlockSize, initBlockSize); + + /* + * Allocate the initial block. Unlike other aset.c blocks, it starts with + * the context header and its block header follows that. + */ + set = (AllocSet) malloc(firstBlockSize); + if (set == NULL) + { + if (TopMemoryContext) + MemoryContextStats(TopMemoryContext); + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("Failed while creating memory context \"%s\".", + name))); + } + + /* + * Avoid writing code that can fail between here and MemoryContextCreate; + * we'd leak the header/initial block if we ereport in this stretch. + */ + + /* Fill in the initial block's block header */ + block = (AllocBlock) (((char *) set) + headerSize); + block->aset = set; + block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ; + block->endptr = ((char *) set) + firstBlockSize; + block->prev = NULL; + block->next = NULL; + + /* Mark unallocated space NOACCESS; leave the block header alone. */ + VALGRIND_MAKE_MEM_NOACCESS(block->freeptr, block->endptr - block->freeptr); + + /* Remember block as part of block list */ + set->blocks = block; + /* Mark block as not to be released at reset time */ + set->keeper = block; + + /* Finish filling in aset-specific parts of the context header */ + MemSetAligned(set->freelist, 0, sizeof(set->freelist)); + set->initBlockSize = initBlockSize; set->maxBlockSize = maxBlockSize; set->nextBlockSize = initBlockSize; + set->headerSize = headerSize; + set->freeListIndex = freeListIndex; /* * Compute the allocation chunk size limit for this context. It can't be @@ -410,74 +556,30 @@ AllocSetContextCreate(MemoryContext parent, (Size) ((maxBlockSize - ALLOC_BLOCKHDRSZ) / ALLOC_CHUNK_FRACTION)) set->allocChunkLimit >>= 1; - /* - * Grab always-allocated space, if requested - */ - if (minContextSize > 0) - { - Size blksize = minContextSize; - AllocBlock block; - - block = (AllocBlock) malloc(blksize); - if (block == NULL) - { - MemoryContextStats(TopMemoryContext); - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), - errmsg("out of memory"), - errdetail("Failed while creating memory context \"%s\".", - name))); - } - block->aset = set; - block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ; - block->endptr = ((char *) block) + blksize; - block->prev = NULL; - block->next = set->blocks; - if (block->next) - block->next->prev = block; - set->blocks = block; - /* Mark block as not to be released at reset time */ - set->keeper = block; - - /* Mark unallocated space NOACCESS; leave the block header alone. */ - VALGRIND_MAKE_MEM_NOACCESS(block->freeptr, - blksize - ALLOC_BLOCKHDRSZ); - } + /* Finally, do the type-independent part of context creation */ + MemoryContextCreate((MemoryContext) set, + T_AllocSetContext, + headerSize, + sizeof(AllocSetContext), + &AllocSetMethods, + parent, + name, + flags); return (MemoryContext) set; } /* - * AllocSetInit - * Context-type-specific initialization routine. - * - * This is called by MemoryContextCreate() after setting up the - * generic MemoryContext fields and before linking the new context - * into the context tree. We must do whatever is needed to make the - * new context minimally valid for deletion. We must *not* risk - * failure --- thus, for example, allocating more memory is not cool. - * (AllocSetContextCreate can allocate memory when it gets control - * back, however.) - */ -static void -AllocSetInit(MemoryContext context) -{ - /* - * Since MemoryContextCreate already zeroed the context node, we don't - * have to do anything here: it's already OK. - */ -} - -/* * 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 hang onto any "keeper" block specified for the set. 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. + * 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. */ static void AllocSetReset(MemoryContext context) @@ -497,7 +599,7 @@ AllocSetReset(MemoryContext context) block = set->blocks; - /* New blocks list is either empty or just the keeper block */ + /* New blocks list will be just the keeper block */ set->blocks = set->keeper; while (block != NULL) @@ -540,7 +642,6 @@ AllocSetReset(MemoryContext context) * in preparation for deletion of the set. * * Unlike AllocSetReset, this *must* free all resources of the set. - * But note we are not responsible for deleting the context node itself. */ static void AllocSetDelete(MemoryContext context) @@ -555,11 +656,49 @@ AllocSetDelete(MemoryContext context) AllocSetCheck(context); #endif - /* Make it look empty, just in case... */ - MemSetAligned(set->freelist, 0, sizeof(set->freelist)); - set->blocks = NULL; - set->keeper = NULL; + /* + * 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--; + + /* 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; + } + /* Free all blocks, except the keeper which is part of context header */ while (block != NULL) { AllocBlock next = block->next; @@ -567,9 +706,15 @@ AllocSetDelete(MemoryContext context) #ifdef CLOBBER_FREED_MEMORY wipe_mem(block, block->freeptr - ((char *) block)); #endif - free(block); + + if (block != set->keeper) + free(block); + block = next; } + + /* Finally, free the context header, including the keeper block */ + free(set); } /* @@ -807,18 +952,6 @@ AllocSetAlloc(MemoryContext context, Size size) block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ; block->endptr = ((char *) block) + blksize; - /* - * If this is the first block of the set, make it the "keeper" block. - * Formerly, a keeper block could only be created during context - * creation, but allowing it to happen here lets us have fast reset - * cycling even for contexts created with minContextSize = 0; that way - * we don't have to force space to be allocated in contexts that might - * never need any space. Don't mark an oversize block as a keeper, - * however. - */ - if (set->keeper == NULL && blksize == set->initBlockSize) - set->keeper = block; - /* Mark unallocated space NOACCESS. */ VALGRIND_MAKE_MEM_NOACCESS(block->freeptr, blksize - ALLOC_BLOCKHDRSZ); @@ -1205,11 +1338,14 @@ AllocSetStats(MemoryContext context, int level, bool print, AllocSet set = (AllocSet) context; Size nblocks = 0; Size freechunks = 0; - Size totalspace = 0; + Size totalspace; Size freespace = 0; AllocBlock block; int fidx; + /* Include context header in totalspace */ + totalspace = set->headerSize; + for (block = set->blocks; block != NULL; block = block->next) { nblocks++; @@ -1264,7 +1400,7 @@ static void AllocSetCheck(MemoryContext context) { AllocSet set = (AllocSet) context; - char *name = set->header.name; + const char *name = set->header.name; AllocBlock prevblock; AllocBlock block; |