add cxMempoolTransfer() - partially resolves #640

Fri, 11 Apr 2025 13:20:07 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 11 Apr 2025 13:20:07 +0200
changeset 1281
45746a08c59e
parent 1280
60123b3db06e
child 1282
0c8077f67e54

add cxMempoolTransfer() - partially resolves #640

CHANGELOG file | annotate | diff | comparison | revisions
docs/Writerside/topics/about.md file | annotate | diff | comparison | revisions
docs/Writerside/topics/mempool.h.md file | annotate | diff | comparison | revisions
src/cx/mempool.h file | annotate | diff | comparison | revisions
src/mempool.c file | annotate | diff | comparison | revisions
tests/test_mempool.c file | annotate | diff | comparison | revisions
--- 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;
 }

mercurial