From 2b89d993b0294d5c0fe0a8333bccc555337ac979 Mon Sep 17 00:00:00 2001 From: David Rowley Date: Wed, 12 Oct 2022 09:30:24 +1300 Subject: [PATCH v3] WIP: slab performance. Author: Andres Freund Discussion: https://postgr.es/m/20210717194333.mr5io3zup3kxahfm@alap3.anarazel.de --- src/backend/utils/mmgr/slab.c | 449 ++++++++++++++++++++++------------ 1 file changed, 294 insertions(+), 155 deletions(-) diff --git a/src/backend/utils/mmgr/slab.c b/src/backend/utils/mmgr/slab.c index 1a0b28f9ea..58b8e2d67c 100644 --- a/src/backend/utils/mmgr/slab.c +++ b/src/backend/utils/mmgr/slab.c @@ -23,15 +23,18 @@ * global (context) level. This is possible as the chunk size (and thus also * the number of chunks per block) is fixed. * - * On each block, free chunks are tracked in a simple linked list. Contents - * of free chunks is replaced with an index of the next free chunk, forming - * a very simple linked list. Each block also contains a counter of free - * chunks. Combined with the local block-level freelist, it makes it trivial - * to eventually free the whole block. + * On each block, never allocated chunks are tracked by a simple offset, and + * free chunks are tracked in a simple linked list. The offset approach + * avoids needing to iterate over all chunks when allocating a new block, + * which would cause page faults and cache pollution. Contents of free chunks + * is replaced with a the offset (in bytes) from the block pointer to the + * next free chunk, forming a very simple linked list. Each block also + * contains a counter of free chunks. Combined with the local block-level + * freelist, it makes it trivial to eventually free the whole block. * * At the context level, we use 'freelist' to track blocks ordered by number * of free chunks, starting with blocks having a single allocated chunk, and - * with completely full blocks on the tail. + * with completely full blocks on the tail. XXX * * This also allows various optimizations - for example when searching for * free chunk, the allocator reuses space from the fullest blocks first, in @@ -44,7 +47,7 @@ * case this performs as if the pointer was not maintained. * * We cache the freelist index for the blocks with the fewest free chunks - * (minFreeChunks), so that we don't have to search the freelist on every + * (minFreeChunkIndex), so that we don't have to search the freelist on every * SlabAlloc() call, which is quite expensive. * *------------------------------------------------------------------------- @@ -60,6 +63,17 @@ #define Slab_BLOCKHDRSZ MAXALIGN(sizeof(SlabBlock)) +struct SlabBlock; +struct SlabChunk; + +/* + * Number of actual freelists + 1, for full blocks. Full blocks are always at + * offset 0. + */ +#define SLAB_FREELIST_COUNT 9 + +#define SLAB_RETAIN_EMPTY_BLOCK_COUNT 10 + /* * SlabContext is a specialized implementation of MemoryContext. */ @@ -72,13 +86,16 @@ typedef struct SlabContext Size blockSize; /* block size */ Size headerSize; /* allocated size of context header */ int chunksPerBlock; /* number of chunks per block */ - int minFreeChunks; /* min number of free chunks in any block */ + int minFreeChunksIndex; /* min number of free chunks in any block XXX */ int nblocks; /* number of blocks allocated */ #ifdef MEMORY_CONTEXT_CHECKING bool *freechunks; /* bitmap of free chunks in a block */ #endif + int nfreeblocks; + dlist_head freeblocks; + int freelist_shift; /* blocks with free space, grouped by number of free chunks: */ - dlist_head freelist[FLEXIBLE_ARRAY_MEMBER]; + dlist_head freelist[SLAB_FREELIST_COUNT]; } SlabContext; /* @@ -92,8 +109,11 @@ typedef struct SlabContext typedef struct SlabBlock { dlist_node node; /* doubly-linked list */ - int nfree; /* number of free chunks */ - int firstFreeChunk; /* index of the first free chunk in the block */ + int nfree; /* number of chunks on freelist + unused */ + int nunused; /* number of unused chunks */ + int32 firstFreeOffset; /* offset of bytes from block to the first + * free chunk in the block. */ + MemoryChunk *unused; /* */ SlabContext *slab; /* owning context */ } SlabBlock; @@ -125,6 +145,34 @@ typedef struct SlabBlock #define SlabBlockIsValid(block) \ (PointerIsValid(block) && SlabIsValid((block)->slab)) +static inline uint8 +SlabFreelistIndex(SlabContext *slab, int freecount) +{ + uint8 index; + + Assert(freecount <= slab->chunksPerBlock); + + /* + * Ensure, without a branch, that index 0 is only used for blocks entirely + * without free chunks. + * XXX: There probably is a cheaper way to do this. Needing to shift twice + * by slab->freelist_shift isn't great. + */ + index = (freecount + (1 << slab->freelist_shift) - 1) >> slab->freelist_shift; + + if (freecount == 0) + Assert(index == 0); + else + Assert(index > 0 && index < (SLAB_FREELIST_COUNT)); + + return index; +} + +static inline dlist_head* +SlabFreelist(SlabContext *slab, int freecount) +{ + return &slab->freelist[SlabFreelistIndex(slab, freecount)]; +} /* * SlabContextCreate @@ -145,7 +193,6 @@ SlabContextCreate(MemoryContext parent, { int chunksPerBlock; Size fullChunkSize; - Size freelistSize; Size headerSize; SlabContext *slab; int i; @@ -155,9 +202,12 @@ SlabContextCreate(MemoryContext parent, "sizeof(MemoryChunk) is not maxaligned"); Assert(MAXALIGN(chunkSize) <= MEMORYCHUNK_MAX_VALUE); - /* Make sure the linked list node fits inside a freed chunk */ - if (chunkSize < sizeof(int)) - chunkSize = sizeof(int); + /* + * Ensure there's enough space to store the offset to the next free chunk + * in the memory of free'd chunks. + */ + if (chunkSize < sizeof(uint32)) + chunkSize = sizeof(uint32); /* chunk, including SLAB header (both addresses nicely aligned) */ #ifdef MEMORY_CONTEXT_CHECKING @@ -175,16 +225,14 @@ SlabContextCreate(MemoryContext parent, /* Compute maximum number of chunks per block */ chunksPerBlock = (blockSize - Slab_BLOCKHDRSZ) / fullChunkSize; - /* The freelist starts with 0, ends with chunksPerBlock. */ - freelistSize = sizeof(dlist_head) * (chunksPerBlock + 1); - /* * Allocate the context header. Unlike aset.c, we never try to combine * this with the first regular block; not worth the extra complication. + * XXX: What's the evidence for that? */ /* Size of the memory context header */ - headerSize = offsetof(SlabContext, freelist) + freelistSize; + headerSize = sizeof(SlabContext); #ifdef MEMORY_CONTEXT_CHECKING @@ -218,17 +266,34 @@ SlabContextCreate(MemoryContext parent, slab->blockSize = blockSize; slab->headerSize = headerSize; slab->chunksPerBlock = chunksPerBlock; - slab->minFreeChunks = 0; + slab->minFreeChunksIndex = 0; slab->nblocks = 0; + slab->nfreeblocks = 0; + + /* + * Compute a shift that guarantees that shifting chunksPerBlock with it + * yields is smaller than SLAB_FREELIST_COUNT - 1 (one freelist is used for full blocks). + */ + slab->freelist_shift = 0; + while ((slab->chunksPerBlock >> slab->freelist_shift) >= (SLAB_FREELIST_COUNT - 1)) + slab->freelist_shift++; + +#if 0 + elog(LOG, "freelist shift for %d chunks of size %zu is %d, block size %zu", + slab->chunksPerBlock, slab->fullChunkSize, slab->freelist_shift, + slab->blockSize); +#endif + + dlist_init(&slab->freeblocks); /* initialize the freelist slots */ - for (i = 0; i < (slab->chunksPerBlock + 1); i++) + for (i = 0; i < SLAB_FREELIST_COUNT; i++) dlist_init(&slab->freelist[i]); #ifdef MEMORY_CONTEXT_CHECKING /* set the freechunks pointer right after the freelists array */ slab->freechunks - = (bool *) slab + offsetof(SlabContext, freelist) + freelistSize; + = (bool *) slab + sizeof(SlabContext); #endif /* Finally, do the type-independent part of context creation */ @@ -261,8 +326,27 @@ SlabReset(MemoryContext context) SlabCheck(context); #endif + /* release retained empty blocks */ + { + dlist_mutable_iter miter; + + dlist_foreach_modify(miter, &slab->freeblocks) + { + SlabBlock *block = dlist_container(SlabBlock, node, miter.cur); + + dlist_delete(miter.cur); + +#ifdef CLOBBER_FREED_MEMORY + wipe_mem(block, slab->blockSize); +#endif + free(block); + slab->nblocks--; + context->mem_allocated -= slab->blockSize; + } + } + /* walk over freelists and free the blocks */ - for (i = 0; i <= slab->chunksPerBlock; i++) + for (i = 0; i < SLAB_FREELIST_COUNT; i++) { dlist_mutable_iter miter; @@ -281,7 +365,7 @@ SlabReset(MemoryContext context) } } - slab->minFreeChunks = 0; + slab->minFreeChunksIndex = 0; Assert(slab->nblocks == 0); Assert(context->mem_allocated == 0); @@ -311,12 +395,11 @@ SlabAlloc(MemoryContext context, Size size) SlabContext *slab = (SlabContext *) context; SlabBlock *block; MemoryChunk *chunk; - int idx; AssertArg(SlabIsValid(slab)); - Assert((slab->minFreeChunks >= 0) && - (slab->minFreeChunks < slab->chunksPerBlock)); + Assert((slab->minFreeChunksIndex >= 0) && + (slab->minFreeChunksIndex < SLAB_FREELIST_COUNT)); /* make sure we only allow correct request size */ if (size != slab->chunkSize) @@ -327,62 +410,93 @@ SlabAlloc(MemoryContext context, Size size) * If there are no free chunks in any existing block, create a new block * and put it to the last freelist bucket. * - * slab->minFreeChunks == 0 means there are no blocks with free chunks, - * thanks to how minFreeChunks is updated at the end of SlabAlloc(). + * slab->minFreeChunksIndex == 0 means there are no blocks with free + * chunks, thanks to how minFreeChunks is updated at the end of + * SlabAlloc(). */ - if (slab->minFreeChunks == 0) + if (unlikely(slab->minFreeChunksIndex == 0)) { - block = (SlabBlock *) malloc(slab->blockSize); + if (slab->nfreeblocks > 0) + { + dlist_node *node; - if (block == NULL) - return NULL; + node = dlist_pop_head_node(&slab->freeblocks); + block = dlist_container(SlabBlock, node, node); + slab->nfreeblocks--; + } + else + { + block = (SlabBlock *) malloc(slab->blockSize); - block->nfree = slab->chunksPerBlock; - block->firstFreeChunk = 0; - block->slab = slab; + if (unlikely(block == NULL)) + return NULL; - /* - * Put all the chunks on a freelist. Walk the chunks and point each - * one to the next one. - */ - for (idx = 0; idx < slab->chunksPerBlock; idx++) - { - chunk = SlabBlockGetChunk(slab, block, idx); - *(int32 *) MemoryChunkGetPointer(chunk) = (idx + 1); + slab->nblocks += 1; + block->slab = slab; + context->mem_allocated += slab->blockSize; } + block->nfree = slab->chunksPerBlock; + block->firstFreeOffset = -1; + block->nunused = slab->chunksPerBlock; + block->unused = (MemoryChunk *) SlabBlockStart(block); + + slab->minFreeChunksIndex = SlabFreelistIndex(slab, slab->chunksPerBlock); + /* * And add it to the last freelist with all chunks empty. * * We know there are no blocks in the freelist, otherwise we wouldn't * need a new block. */ - Assert(dlist_is_empty(&slab->freelist[slab->chunksPerBlock])); + Assert(dlist_is_empty(SlabFreelist(slab, slab->chunksPerBlock))); - dlist_push_head(&slab->freelist[slab->chunksPerBlock], &block->node); + dlist_push_head(SlabFreelist(slab, slab->chunksPerBlock), + &block->node); - slab->minFreeChunks = slab->chunksPerBlock; - slab->nblocks += 1; - context->mem_allocated += slab->blockSize; + chunk = block->unused; + block->unused = (MemoryChunk *) (((char *) block->unused) + slab->fullChunkSize); + block->nunused--; } + else + { + /* grab the block from the freelist */ + block = dlist_head_element(SlabBlock, node, + &slab->freelist[slab->minFreeChunksIndex]); - /* grab the block from the freelist (even the new block is there) */ - block = dlist_head_element(SlabBlock, node, - &slab->freelist[slab->minFreeChunks]); + /* make sure we actually got a valid block, with matching nfree */ + Assert(block != NULL); + Assert(slab->minFreeChunksIndex == SlabFreelistIndex(slab, block->nfree)); + Assert(block->nfree > 0); - /* make sure we actually got a valid block, with matching nfree */ - Assert(block != NULL); - Assert(slab->minFreeChunks == block->nfree); - Assert(block->nfree > 0); + /* we know index of the first free chunk in the block */ + if (block->nunused > 0) + { + chunk = block->unused; + block->unused = (MemoryChunk *) (((char *) block->unused) + slab->fullChunkSize); + block->nunused--; + } + else + { + chunk = (MemoryChunk *) ((char *) block + block->firstFreeOffset); - /* we know index of the first free chunk in the block */ - idx = block->firstFreeChunk; + /* + * Remove the chunk from the freelist head. The index of the next free + * chunk is stored in the chunk itself. + */ + VALGRIND_MAKE_MEM_DEFINED(SlabChunkGetPointer(chunk), sizeof(void*)); + block->firstFreeOffset = *(int32 *) SlabChunkGetPointer(chunk); + } + } - /* make sure the chunk index is valid, and that it's marked as empty */ - Assert((idx >= 0) && (idx < slab->chunksPerBlock)); + /* make sure the chunk is in the block and that it's marked as empty (XXX?) */ + Assert((char *) chunk >= SlabBlockStart(block)); + Assert((char *) chunk < (((char *)block) + slab->blockSize)); - /* compute the chunk location block start (after the block header) */ - chunk = SlabBlockGetChunk(slab, block, idx); + Assert(block->firstFreeChunk == NULL || ( + (block->firstFreeChunk >= (SlabChunk *) SlabBlockStart(block)) && + block->firstFreeChunk <= (SlabChunk *) (((char *)block) + slab->blockSize)) + ); /* * Update the block nfree count, and also the minFreeChunks as we've @@ -390,48 +504,6 @@ SlabAlloc(MemoryContext context, Size size) * (because that's how we chose the block). */ block->nfree--; - slab->minFreeChunks = block->nfree; - - /* - * Remove the chunk from the freelist head. The index of the next free - * chunk is stored in the chunk itself. - */ - VALGRIND_MAKE_MEM_DEFINED(MemoryChunkGetPointer(chunk), sizeof(int32)); - block->firstFreeChunk = *(int32 *) MemoryChunkGetPointer(chunk); - - Assert(block->firstFreeChunk >= 0); - Assert(block->firstFreeChunk <= slab->chunksPerBlock); - - Assert((block->nfree != 0 && - block->firstFreeChunk < slab->chunksPerBlock) || - (block->nfree == 0 && - block->firstFreeChunk == slab->chunksPerBlock)); - - /* move the whole block to the right place in the freelist */ - dlist_delete(&block->node); - dlist_push_head(&slab->freelist[block->nfree], &block->node); - - /* - * And finally update minFreeChunks, i.e. the index to the block with the - * lowest number of free chunks. We only need to do that when the block - * got full (otherwise we know the current block is the right one). We'll - * simply walk the freelist until we find a non-empty entry. - */ - if (slab->minFreeChunks == 0) - { - for (idx = 1; idx <= slab->chunksPerBlock; idx++) - { - if (dlist_is_empty(&slab->freelist[idx])) - continue; - - /* found a non-empty freelist */ - slab->minFreeChunks = idx; - break; - } - } - - if (slab->minFreeChunks == slab->chunksPerBlock) - slab->minFreeChunks = 0; /* Prepare to initialize the chunk header. */ VALGRIND_MAKE_MEM_UNDEFINED(chunk, Slab_CHUNKHDRSZ); @@ -458,6 +530,60 @@ SlabAlloc(MemoryContext context, Size size) return MemoryChunkGetPointer(chunk); } +static void pg_noinline +SlabFreeSlow(SlabContext *slab, SlabBlock *block) +{ + dlist_delete(&block->node); + + /* + * See if we need to update the minFreeChunks field for the slab - we only + * need to do that if the block had that number of free chunks + * before we freed one. In that case, we check if there still are blocks + * in the original freelist and we either keep the current value (if there + * still are blocks) or increment it by one (the new block is still the + * one with minimum free chunks). + * + * The one exception is when the block will get completely free - in that + * case we will free it, se we can't use it for minFreeChunks. It however + * means there are no more blocks with free chunks. + */ + if (slab->minFreeChunksIndex == SlabFreelistIndex(slab, block->nfree - 1)) + { + /* Have we removed the last chunk from the freelist? */ + if (dlist_is_empty(&slab->freelist[slab->minFreeChunksIndex])) + { + /* but if we made the block entirely free, we'll free it */ + if (block->nfree == slab->chunksPerBlock) + slab->minFreeChunksIndex = 0; + else + slab->minFreeChunksIndex++; + } + } + + /* If the block is now completely empty, free it (in a way). */ + if (block->nfree == slab->chunksPerBlock) + { + /* + * To avoid constantly freeing/allocating blocks in bursty patterns + * (on most crucially cases of repeatedly allocating and freeing a + * single chunk), retain a small number of blocks. + */ + if (slab->nfreeblocks < SLAB_RETAIN_EMPTY_BLOCK_COUNT) + { + dlist_push_head(&slab->freeblocks, &block->node); + slab->nfreeblocks++; + } + else + { + slab->nblocks--; + slab->header.mem_allocated -= slab->blockSize; + free(block); + } + } + else + dlist_push_head(SlabFreelist(slab, block->nfree), &block->node); +} + /* * SlabFree * Frees allocated memory; memory is removed from the slab. @@ -468,7 +594,6 @@ SlabFree(void *pointer) MemoryChunk *chunk = PointerGetMemoryChunk(pointer); SlabBlock *block = MemoryChunkGetBlock(chunk); SlabContext *slab; - int idx; /* * For speed reasons we just Assert that the referenced block is good. @@ -478,6 +603,45 @@ SlabFree(void *pointer) AssertArg(SlabBlockIsValid(block)); slab = block->slab; + Assert(slab->minFreeChunksIndex == SlabFreelistIndex(slab, block->nfree + 1)); + + /* move the whole block to the right place in the freelist */ + if (unlikely(SlabFreelistIndex(slab, block->nfree) != slab->minFreeChunksIndex)) + { + slab->minFreeChunksIndex = SlabFreelistIndex(slab, block->nfree); + dlist_delete(&block->node); + dlist_push_head(SlabFreelist(slab, block->nfree), &block->node); + + + /* + * And finally update minFreeChunks, i.e. the index to the block with the + * lowest number of free chunks. We only need to do that when the block + * got full (otherwise we know the current block is the right one). We'll + * simply walk the freelist until we find a non-empty entry. + */ + if (slab->minFreeChunksIndex == 0) + { + for (int idx = 1; idx < SLAB_FREELIST_COUNT; idx++) + { + if (dlist_is_empty(&slab->freelist[idx])) + continue; + + /* found a non-empty freelist */ + slab->minFreeChunksIndex = idx; + break; + } + } + } + +#if 0 + /* + * FIXME: I don't understand what this ever did? It should be unreachable + * I think? + */ + if (slab->minFreeChunks == slab->chunksPerBlock) + slab->minFreeChunks = 0; +#endif + #ifdef MEMORY_CONTEXT_CHECKING /* Test for someone scribbling on unused space in chunk */ Assert(slab->chunkSize < (slab->fullChunkSize - Slab_CHUNKHDRSZ)); @@ -486,60 +650,22 @@ SlabFree(void *pointer) slab->header.name, chunk); #endif - /* compute index of the chunk with respect to block start */ - idx = SlabChunkIndex(slab, block, chunk); - /* add chunk to freelist, and update block nfree count */ - *(int32 *) pointer = block->firstFreeChunk; - block->firstFreeChunk = idx; + *(MemoryChunk **) pointer = (MemoryChunk *) ((char *) block + block->firstFreeOffset); + block->firstFreeOffset = (char *) chunk - (char *) block; block->nfree++; Assert(block->nfree > 0); Assert(block->nfree <= slab->chunksPerBlock); #ifdef CLOBBER_FREED_MEMORY - /* XXX don't wipe the int32 index, used for block-level freelist */ + /* XXX don't wipe the int32 offset, used for block-level freelist */ wipe_mem((char *) pointer + sizeof(int32), slab->chunkSize - sizeof(int32)); #endif - /* remove the block from a freelist */ - dlist_delete(&block->node); - - /* - * See if we need to update the minFreeChunks field for the slab - we only - * need to do that if there the block had that number of free chunks - * before we freed one. In that case, we check if there still are blocks - * in the original freelist and we either keep the current value (if there - * still are blocks) or increment it by one (the new block is still the - * one with minimum free chunks). - * - * The one exception is when the block will get completely free - in that - * case we will free it, se we can't use it for minFreeChunks. It however - * means there are no more blocks with free chunks. - */ - if (slab->minFreeChunks == (block->nfree - 1)) - { - /* Have we removed the last chunk from the freelist? */ - if (dlist_is_empty(&slab->freelist[slab->minFreeChunks])) - { - /* but if we made the block entirely free, we'll free it */ - if (block->nfree == slab->chunksPerBlock) - slab->minFreeChunks = 0; - else - slab->minFreeChunks++; - } - } - - /* If the block is now completely empty, free it. */ - if (block->nfree == slab->chunksPerBlock) - { - free(block); - slab->nblocks--; - slab->header.mem_allocated -= slab->blockSize; - } - else - dlist_push_head(&slab->freelist[block->nfree], &block->node); + if (SlabFreelistIndex(slab, block->nfree) != SlabFreelistIndex(slab, block->nfree - 1)) + SlabFreeSlow(slab, block); Assert(slab->nblocks >= 0); Assert(slab->nblocks * slab->blockSize == slab->header.mem_allocated); @@ -656,7 +782,7 @@ SlabStats(MemoryContext context, /* Include context header in totalspace */ totalspace = slab->headerSize; - for (i = 0; i <= slab->chunksPerBlock; i++) + for (i = 0; i < SLAB_FREELIST_COUNT; i++) { dlist_iter iter; @@ -713,7 +839,7 @@ SlabCheck(MemoryContext context) Assert(slab->chunksPerBlock > 0); /* walk all the freelists */ - for (i = 0; i <= slab->chunksPerBlock; i++) + for (i = 0; i < SLAB_FREELIST_COUNT; i++) { int j, nfree; @@ -722,14 +848,14 @@ SlabCheck(MemoryContext context) /* walk all blocks on this freelist */ dlist_foreach(iter, &slab->freelist[i]) { - int idx; SlabBlock *block = dlist_container(SlabBlock, node, iter.cur); + MemoryChunk *cur_chunk; /* * Make sure the number of free chunks (in the block header) * matches position in the freelist. */ - if (block->nfree != i) + if (SlabFreelistIndex(slab, block->nfree) != i) elog(WARNING, "problem in slab %s: number of free chunks %d in block %p does not match freelist %d", name, block->nfree, block, i); @@ -740,7 +866,6 @@ SlabCheck(MemoryContext context) /* reset the bitmap of free chunks for this block */ memset(slab->freechunks, 0, (slab->chunksPerBlock * sizeof(bool))); - idx = block->firstFreeChunk; /* * Now walk through the chunks, count the free ones and also @@ -749,10 +874,12 @@ SlabCheck(MemoryContext context) * walk through the chunks and construct our own bitmap. */ + cur_chunk = (MemoryChunk *) ((char *) block) + block->firstChunkOffset); nfree = 0; - while (idx < slab->chunksPerBlock) + while (cur_chunk != NULL) { MemoryChunk *chunk; + int idx = SlabChunkIndex(slab, block, cur_chunk); /* count the chunk as free, add it to the bitmap */ nfree++; @@ -761,9 +888,21 @@ SlabCheck(MemoryContext context) /* read index of the next free chunk */ chunk = SlabBlockGetChunk(slab, block, idx); VALGRIND_MAKE_MEM_DEFINED(MemoryChunkGetPointer(chunk), sizeof(int32)); - idx = *(int32 *) MemoryChunkGetPointer(chunk); + cur_chunk = (char *) block + (int32) SlabChunkGetPointer(cur_chunk); } + cur_chunk = block->unused; + for (j = 0; j < block->nunused; j++) + { + int idx = SlabChunkIndex(slab, block, cur_chunk); + + /* count the chunk as free, add it to the bitmap */ + nfree++; + slab->freechunks[idx] = true; + + cur_chunk = (MemoryChunk *) (((char *) cur_chunk) + slab->fullChunkSize); + } + for (j = 0; j < slab->chunksPerBlock; j++) { /* non-zero bit in the bitmap means chunk the chunk is used */ -- 2.35.1.windows.2