Fri, 11 Apr 2025 13:20:07 +0200
add cxMempoolTransfer() - partially resolves #640
--- a/CHANGELOG Fri Apr 11 09:15:21 2025 +0200 +++ b/CHANGELOG Fri Apr 11 13:20:07 2025 +0200 @@ -1,7 +1,8 @@ Version 3.2 - tbd ------------------------ - + * adds cxMempoolTransfer() + * changes grow strategy for the mempory pool to reduce reallocations Version 3.1 - 2025-02-11 ------------------------
--- a/docs/Writerside/topics/about.md Fri Apr 11 09:15:21 2025 +0200 +++ b/docs/Writerside/topics/about.md Fri Apr 11 13:20:07 2025 +0200 @@ -28,7 +28,8 @@ ### Version 3.2 - preview {collapsible="true"} - +* adds cxMempoolTransfer() +* changes grow strategy for the mempory pool to reduce reallocations ### Version 3.1 - 2025-02-11 {collapsible="true"}
--- a/docs/Writerside/topics/mempool.h.md Fri Apr 11 09:15:21 2025 +0200 +++ b/docs/Writerside/topics/mempool.h.md Fri Apr 11 13:20:07 2025 +0200 @@ -10,7 +10,7 @@ A memory pool can be used with all UCX features that support the use of an [allocator](allocator.h.md). For example, the UCX [string](string.h.md) functions provide several variants suffixed with `_a` for that purpose. -## Overview +## Basic Memory Management ```C #include <cx/mempool.h> @@ -29,8 +29,6 @@ cx_destructor_func fnc); ``` -## Description - A memory pool is created with the `cxMempoolCreate()` function with a default `capacity` and an optional default destructor function `fnc`. If specified, the default destructor function is registered for all freshly allocated memory within the pool, @@ -48,7 +46,7 @@ Usually this function returns zero except for platforms where memory allocations are likely to fail, in which case a non-zero value is returned. -## Order of Destruction +### Order of Destruction When you call `cxMempoolFree()` the following actions are performed: @@ -58,6 +56,34 @@ 2. The pool memory is deallocated 3. The pool structure is deallocated +## Transfer Memory + +```C +#include <cx/mempool.h> + +int cxMempoolTransfer(CxMempool *source, CxMempool *dest); +``` + +Memory managed by a pool can be transferred to another pool. + +The function `cxMempoolTransfer()` transfers all memory managed and/or registered with the `source` pool to the `dest` pool. +It also registers its allocator with the `dest` pool and creates a new allocator for the `source` pool. +That means, that all references to the allocator of the `source` pool remain valid and continue to work with the `dest` pool. +The transferred allocator will be destroyed when the `dest` pool gets destroyed. + +The function returns zero when the transfer was successful and non-zero if a necessary memory allocation was not possible, +or the `source` and `dest` pointers point to the same pool. +In case of an error, no memory is transferred and both pools are in a valid state. + +> Although the allocator from the `source` pool remains valid for the already allocated objects, +> it is **not** valid to use that allocator to allocate new objects in the `dest` pool. +> It may only be used to free or reallocate the memory of the existing objects. +> +> The reason is, that the allocator will be destroyed after destroying all objects from the `source` pool and +> _before_ destroying objects in the `dest` pool which were allocated after the transfer. +>{style="warning"} + + ## Example The following code illustrates how the contents of a CSV file are read into pooled memory.
--- a/src/cx/mempool.h Fri Apr 11 09:15:21 2025 +0200 +++ b/src/cx/mempool.h Fri Apr 11 13:20:07 2025 +0200 @@ -156,6 +156,26 @@ cx_destructor_func destr ); +/** + * Transfers all the memory managed by one pool to another. + * + * The allocator of the source pool will also be transferred and registered with the destination pool + * and stays valid, as long as the destination pool is not destroyed. + * + * The source pool will get a completely new allocator and can be reused or destroyed afterward. + * + * @param source the pool to move the memory from + * @param dest the pool where to transfer the memory to + * @retval zero success + * @retval non-zero failure + */ +cx_attr_nonnull +cx_attr_export +int cxMempoolTransfer( + CxMempool *source, + CxMempool *dest +); + #ifdef __cplusplus } // extern "C" #endif
--- a/src/mempool.c Fri Apr 11 09:15:21 2025 +0200 +++ b/src/mempool.c Fri Apr 11 13:20:07 2025 +0200 @@ -38,24 +38,34 @@ char c[]; }; +static int cx_mempool_ensure_capacity( + struct cx_mempool_s *pool, + size_t needed_capacity +) { + if (needed_capacity <= pool->capacity) return 0; + size_t newcap = pool->capacity >= 1000 ? + pool->capacity + 1000 : pool->capacity * 2; + size_t newmsize; + if (pool->capacity > newcap || cx_szmul(newcap, + sizeof(struct cx_mempool_memory_s*), &newmsize)) { + errno = EOVERFLOW; + return 1; + } + struct cx_mempool_memory_s **newdata = realloc(pool->data, newmsize); + if (newdata == NULL) return 1; + pool->data = newdata; + pool->capacity = newcap; + return 0; +} + static void *cx_mempool_malloc( void *p, size_t n ) { struct cx_mempool_s *pool = p; - if (pool->size >= pool->capacity) { - size_t newcap = pool->capacity - (pool->capacity % 16) + 16; - size_t newmsize; - if (pool->capacity > newcap || cx_szmul(newcap, - sizeof(struct cx_mempool_memory_s*), &newmsize)) { - errno = EOVERFLOW; - return NULL; - } - struct cx_mempool_memory_s **newdata = realloc(pool->data, newmsize); - if (newdata == NULL) return NULL; - pool->data = newdata; - pool->capacity = newcap; + if (cx_mempool_ensure_capacity(pool, pool->size + 1)) { + return NULL; } struct cx_mempool_memory_s *mem = malloc(sizeof(cx_destructor_func) + n); @@ -235,3 +245,41 @@ return pool; } + +int cxMempoolTransfer( + CxMempool *source, + CxMempool *dest +) { + // safety check + if (source == dest) return 1; + + // ensure enough capacity in the destination pool + if (cx_mempool_ensure_capacity(dest, dest->size + source->size + 1)) { + return 1; + } + + // allocate a replacement allocator for the source pool + CxAllocator *new_source_allocator = malloc(sizeof(CxAllocator)); + if (new_source_allocator == NULL) { // LCOV_EXCL_START + return 1; + } // LCOV_EXCL_STOP + new_source_allocator->cl = &cx_mempool_allocator_class; + new_source_allocator->data = source; + + // transfer all the data + memcpy(&dest->data[dest->size], source->data, sizeof(source->data[0])*source->size); + dest->size += source->size; + + // register the old allocator with the new pool + // we have to remove const-ness for this, but that's okay here + CxAllocator *transferred_allocator = (CxAllocator*) source->allocator; + transferred_allocator->data = dest; + cxMempoolRegister(dest, transferred_allocator, free); + + // prepare the source pool for re-use + source->allocator = new_source_allocator; + memset(source->data, 0, source->size * sizeof(source->data[0])); + source->size = 0; + + return 0; +}
--- a/tests/test_mempool.c Fri Apr 11 09:15:21 2025 +0200 +++ b/tests/test_mempool.c Fri Apr 11 13:20:07 2025 +0200 @@ -164,6 +164,50 @@ } } +CX_TEST(test_mempool_transfer) { + CxMempool *src = cxMempoolCreateSimple(4); + CxMempool *dest = cxMempoolCreateSimple(4); + CX_TEST_DO { + // check that the destructor functions are also transferred + src->auto_destr = test_mempool_destructor; + + // allocate first object + int *a = cxMalloc(src->allocator, sizeof(int)); + // allocate second object + int *b = cxMalloc(src->allocator, sizeof(int)); + // register foreign object + int *c = malloc(sizeof(int)); + cxMempoolRegister(src, c, test_mempool_destructor); + + // check source pool + CX_TEST_ASSERT(src->size == 3); + const CxAllocator *old_allocator = src->allocator; + CX_TEST_ASSERT(old_allocator->data == src); + + // perform transfer + int result = cxMempoolTransfer(src, dest); + CX_TEST_ASSERT(result == 0); + + // check transfer + CX_TEST_ASSERT(src->size == 0); + CX_TEST_ASSERT(dest->size == 4); // 3 objects + the old allocator + CX_TEST_ASSERT(src->allocator != old_allocator); + CX_TEST_ASSERT(old_allocator->data == dest); + + // verify that destroying old pool does nothing + test_mempool_destructor_called = 0; + cxMempoolFree(src); + CX_TEST_ASSERT(test_mempool_destructor_called == 0); + + // verify that destroying new pool calls the destructors + // but only three times (the old allocator has a different destructor) + cxMempoolFree(dest); + CX_TEST_ASSERT(test_mempool_destructor_called == 3); + + // free the foreign object + free(c); + } +} CxTestSuite *cx_test_suite_mempool(void) { CxTestSuite *suite = cx_test_suite_new("mempool"); @@ -175,6 +219,7 @@ cx_test_register(suite, test_mempool_free); cx_test_register(suite, test_mempool_destroy); cx_test_register(suite, test_mempool_register); + cx_test_register(suite, test_mempool_transfer); return suite; }