From e3578ba62160dfe88e6b1dfa4774d1df07ab0d86 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sat, 11 Jul 2026 18:07:35 -0600 Subject: [PATCH 1/4] build: set package maintainer contact Signed-off-by: Eduardo Silva --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5484ff8..96188e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,7 +158,7 @@ endif() set(CPACK_PACKAGE_VERSION ${CFL_VERSION_STR}) set(CPACK_PACKAGE_NAME "cfl") set(CPACK_PACKAGE_RELEASE 1) -set(CPACK_PACKAGE_CONTACT "CFL Authors") +set(CPACK_PACKAGE_CONTACT "Eduardo Silva ") set(CPACK_PACKAGE_VENDOR "Fluent Project") set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") set(CPACK_PACKAGING_INSTALL_PREFIX "/") From 4ae70749ed5b139a7324b1b585ffa31ada907be4 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sat, 11 Jul 2026 18:17:06 -0600 Subject: [PATCH 2/4] arena: add public allocation interface Signed-off-by: Eduardo Silva --- include/cfl/cfl_arena.h | 31 +++++ src/cfl_arena.c | 163 ++++++++++++++++++++++++--- src/cfl_arena_internal.h | 2 - src/cfl_array.c | 4 +- src/cfl_kvlist.c | 2 +- src/cfl_sds.c | 2 +- tests/arena.c | 194 ++++++++++++++++++++++++++++++++ tests/installed_consumer/main.c | 15 +++ 8 files changed, 392 insertions(+), 21 deletions(-) diff --git a/include/cfl/cfl_arena.h b/include/cfl/cfl_arena.h index ba8eb21..2134df2 100644 --- a/include/cfl/cfl_arena.h +++ b/include/cfl/cfl_arena.h @@ -7,6 +7,19 @@ struct cfl_arena; +typedef void *(*cfl_arena_malloc_fn)(void *context, size_t size); +typedef void (*cfl_arena_free_fn)(void *context, void *pointer); + +struct cfl_arena_options { + size_t struct_size; + size_t chunk_size; + size_t maximum_chunk_size; + size_t large_object_threshold; + cfl_arena_malloc_fn malloc_fn; + cfl_arena_free_fn free_fn; + void *allocator_context; +}; + /* * Arena-created objects remain valid until the arena is reset or destroyed. * Reset and destroy invalidate every pointer allocated from the arena. @@ -20,8 +33,26 @@ struct cfl_arena; struct cfl_arena *cfl_arena_create(size_t chunk_size); struct cfl_arena *cfl_arena_create_ex(size_t chunk_size, size_t large_object_threshold); +void cfl_arena_options_init(struct cfl_arena_options *options); +struct cfl_arena *cfl_arena_create_with_options( + const struct cfl_arena_options *options); void cfl_arena_destroy(struct cfl_arena *arena); void cfl_arena_reset(struct cfl_arena *arena); + +/* + * Raw allocations are aligned for CFL-supported fundamental C types. They + * cannot be freed individually and remain valid until reset or destruction. + * A zero-sized or overflowing request returns NULL. Allocation failure leaves + * the arena valid and does not define errno. + */ +void *cfl_arena_malloc(struct cfl_arena *arena, size_t size); +void *cfl_arena_calloc(struct cfl_arena *arena, + size_t count, size_t size); +void *cfl_arena_memdup(struct cfl_arena *arena, + const void *source, size_t size); +char *cfl_arena_strndup(struct cfl_arena *arena, + const char *source, size_t length); + size_t cfl_arena_bytes_reserved(struct cfl_arena *arena); size_t cfl_arena_bytes_used(struct cfl_arena *arena); size_t cfl_arena_large_object_threshold(struct cfl_arena *arena); diff --git a/src/cfl_arena.c b/src/cfl_arena.c index bcdf058..62fa165 100644 --- a/src/cfl_arena.c +++ b/src/cfl_arena.c @@ -50,14 +50,31 @@ struct cfl_arena { size_t external_cache_bytes; size_t external_cache_limit; size_t chunk_size; + size_t maximum_chunk_size; + size_t next_chunk_size; size_t bytes_reserved; size_t bytes_used; size_t large_object_threshold; void *free_variants; void *free_kvpairs; void *free_sds[CFL_ARENA_SDS_CLASS_COUNT]; + cfl_arena_malloc_fn malloc_fn; + cfl_arena_free_fn free_fn; + void *allocator_context; }; +static void *arena_default_malloc(void *context, size_t size) +{ + (void) context; + return malloc(size); +} + +static void arena_default_free(void *context, void *pointer) +{ + (void) context; + free(pointer); +} + static void arena_chunks_destroy(struct cfl_arena *arena) { struct cfl_arena_chunk *chunk; @@ -66,7 +83,7 @@ static void arena_chunks_destroy(struct cfl_arena *arena) chunk = arena->head; while (chunk != NULL) { next = chunk->next; - free(chunk); + arena->free_fn(arena->allocator_context, chunk); chunk = next; } @@ -88,7 +105,7 @@ static void arena_external_destroy(struct cfl_arena *arena) arena->bytes_reserved -= allocation->size + sizeof(struct cfl_arena_external); arena->bytes_used -= allocation->size; - free(allocation); + arena->free_fn(arena->allocator_context, allocation); allocation = next; } arena->external = NULL; @@ -99,7 +116,7 @@ static void arena_external_destroy(struct cfl_arena *arena) next = allocation->next; arena->bytes_reserved -= allocation->size + sizeof(struct cfl_arena_external); - free(allocation); + arena->free_fn(arena->allocator_context, allocation); allocation = next; } arena->external_cache[index] = NULL; @@ -112,7 +129,7 @@ static void arena_external_destroy(struct cfl_arena *arena) next = allocation->next; arena->bytes_reserved -= allocation->size + sizeof(struct cfl_arena_external); - free(allocation); + arena->free_fn(arena->allocator_context, allocation); allocation = next; } arena->external_exact_cache = NULL; @@ -125,19 +142,82 @@ struct cfl_arena *cfl_arena_create(size_t chunk_size) struct cfl_arena *cfl_arena_create_ex(size_t chunk_size, size_t large_object_threshold) +{ + struct cfl_arena_options options; + + cfl_arena_options_init(&options); + options.chunk_size = chunk_size; + options.large_object_threshold = large_object_threshold; + + return cfl_arena_create_with_options(&options); +} + +void cfl_arena_options_init(struct cfl_arena_options *options) +{ + if (options == NULL) { + return; + } + + memset(options, 0, sizeof(struct cfl_arena_options)); + options->struct_size = sizeof(struct cfl_arena_options); +} + +struct cfl_arena *cfl_arena_create_with_options( + const struct cfl_arena_options *options) { struct cfl_arena *arena; + size_t chunk_size; + size_t maximum_chunk_size; + size_t large_object_threshold; + cfl_arena_malloc_fn malloc_fn; + cfl_arena_free_fn free_fn; + void *allocator_context; + + if (options == NULL || + options->struct_size < + offsetof(struct cfl_arena_options, allocator_context) + + sizeof(options->allocator_context)) { + return NULL; + } + + if ((options->malloc_fn == NULL) != (options->free_fn == NULL)) { + return NULL; + } + + chunk_size = options->chunk_size; + maximum_chunk_size = options->maximum_chunk_size; + large_object_threshold = options->large_object_threshold; + malloc_fn = options->malloc_fn; + free_fn = options->free_fn; + allocator_context = options->allocator_context; if (chunk_size == 0) { chunk_size = CFL_ARENA_DEFAULT_CHUNK_SIZE; } + if (maximum_chunk_size == 0) { + maximum_chunk_size = chunk_size; + } + if (maximum_chunk_size < chunk_size) { + return NULL; + } + if (malloc_fn == NULL) { + malloc_fn = arena_default_malloc; + free_fn = arena_default_free; + allocator_context = NULL; + } - arena = calloc(1, sizeof(struct cfl_arena)); + arena = malloc_fn(allocator_context, sizeof(struct cfl_arena)); if (arena == NULL) { return NULL; } + memset(arena, 0, sizeof(struct cfl_arena)); arena->chunk_size = chunk_size; + arena->maximum_chunk_size = maximum_chunk_size; + arena->next_chunk_size = chunk_size; + arena->malloc_fn = malloc_fn; + arena->free_fn = free_fn; + arena->allocator_context = allocator_context; if (large_object_threshold == 0) { large_object_threshold = chunk_size / 2; if (large_object_threshold == 0) { @@ -162,7 +242,7 @@ void cfl_arena_destroy(struct cfl_arena *arena) arena_external_destroy(arena); arena_chunks_destroy(arena); - free(arena); + arena->free_fn(arena->allocator_context, arena); } void cfl_arena_reset(struct cfl_arena *arena) @@ -190,12 +270,13 @@ void cfl_arena_reset(struct cfl_arena *arena) arena->bytes_used = 0; arena->current = arena->head; + arena->next_chunk_size = arena->chunk_size; arena->free_variants = NULL; arena->free_kvpairs = NULL; memset(arena->free_sds, 0, sizeof(arena->free_sds)); } -void *cfl_arena_alloc(struct cfl_arena *arena, size_t size) +void *cfl_arena_malloc(struct cfl_arena *arena, size_t size) { struct cfl_arena_chunk *chunk; size_t alignment; @@ -229,7 +310,7 @@ void *cfl_arena_alloc(struct cfl_arena *arena, size_t size) chunk = chunk->next; } - capacity = arena->chunk_size; + capacity = arena->next_chunk_size; if (capacity < size) { capacity = size; } @@ -237,7 +318,8 @@ void *cfl_arena_alloc(struct cfl_arena *arena, size_t size) return NULL; } - chunk = malloc(sizeof(struct cfl_arena_chunk) + capacity); + chunk = arena->malloc_fn(arena->allocator_context, + sizeof(struct cfl_arena_chunk) + capacity); if (chunk == NULL) { return NULL; } @@ -251,9 +333,24 @@ void *cfl_arena_alloc(struct cfl_arena *arena, size_t size) sizeof(struct cfl_arena_chunk); arena->bytes_used += size; + if (capacity == arena->next_chunk_size && + arena->next_chunk_size < arena->maximum_chunk_size) { + if (arena->next_chunk_size > arena->maximum_chunk_size / 2) { + arena->next_chunk_size = arena->maximum_chunk_size; + } + else { + arena->next_chunk_size *= 2; + } + } + return chunk->data; } +void *cfl_arena_alloc(struct cfl_arena *arena, size_t size) +{ + return cfl_arena_malloc(arena, size); +} + void *cfl_arena_calloc(struct cfl_arena *arena, size_t count, size_t size) { @@ -265,7 +362,7 @@ void *cfl_arena_calloc(struct cfl_arena *arena, } total = count * size; - result = cfl_arena_alloc(arena, total); + result = cfl_arena_malloc(arena, total); if (result != NULL) { memset(result, 0, total); } @@ -273,6 +370,41 @@ void *cfl_arena_calloc(struct cfl_arena *arena, return result; } +void *cfl_arena_memdup(struct cfl_arena *arena, + const void *source, size_t size) +{ + void *result; + + if (source == NULL || size == 0) { + return NULL; + } + + result = cfl_arena_malloc(arena, size); + if (result != NULL) { + memcpy(result, source, size); + } + + return result; +} + +char *cfl_arena_strndup(struct cfl_arena *arena, + const char *source, size_t length) +{ + char *result; + + if (source == NULL || length == SIZE_MAX) { + return NULL; + } + + result = cfl_arena_malloc(arena, length + 1); + if (result != NULL) { + memcpy(result, source, length); + result[length] = '\0'; + } + + return result; +} + size_t cfl_arena_bytes_reserved(struct cfl_arena *arena) { return arena == NULL ? 0 : arena->bytes_reserved; @@ -310,7 +442,7 @@ void cfl_arena_external_cache_limit_set(struct cfl_arena *arena, arena->external_cache_bytes -= allocation->size; arena->bytes_reserved -= allocation->size + sizeof(struct cfl_arena_external); - free(allocation); + arena->free_fn(arena->allocator_context, allocation); } } @@ -321,7 +453,7 @@ void cfl_arena_external_cache_limit_set(struct cfl_arena *arena, arena->external_cache_bytes -= allocation->size; arena->bytes_reserved -= allocation->size + sizeof(struct cfl_arena_external); - free(allocation); + arena->free_fn(arena->allocator_context, allocation); } } @@ -401,8 +533,9 @@ void *cfl_arena_alloc_external(struct cfl_arena *arena, } if (allocation == NULL) { - allocation = malloc(sizeof(struct cfl_arena_external) + - allocation_size); + allocation = arena->malloc_fn( + arena->allocator_context, + sizeof(struct cfl_arena_external) + allocation_size); if (allocation == NULL) { return NULL; } @@ -466,7 +599,7 @@ void cfl_arena_free_external(struct cfl_arena *arena, else { arena->bytes_reserved -= allocation->size + sizeof(struct cfl_arena_external); - free(allocation); + arena->free_fn(arena->allocator_context, allocation); } } diff --git a/src/cfl_arena_internal.h b/src/cfl_arena_internal.h index df6e011..e41fe3a 100644 --- a/src/cfl_arena_internal.h +++ b/src/cfl_arena_internal.h @@ -6,8 +6,6 @@ #include void *cfl_arena_alloc(struct cfl_arena *arena, size_t size); -void *cfl_arena_calloc(struct cfl_arena *arena, - size_t count, size_t size); void *cfl_arena_alloc_external(struct cfl_arena *arena, size_t size); void cfl_arena_free_external(struct cfl_arena *arena, diff --git a/src/cfl_array.c b/src/cfl_array.c index b327c91..bac55ab 100644 --- a/src/cfl_array.c +++ b/src/cfl_array.c @@ -49,7 +49,7 @@ struct cfl_array *cfl_array_create_in(struct cfl_arena *arena, array = malloc(sizeof(struct cfl_array)); } else { - array = cfl_arena_alloc(arena, sizeof(struct cfl_array)); + array = cfl_arena_malloc(arena, sizeof(struct cfl_array)); } if (array == NULL) { cfl_errno(); @@ -222,7 +222,7 @@ int cfl_array_append(struct cfl_array *array, tmp = realloc(array->entries, new_size); } else { - tmp = cfl_arena_alloc(array->arena, new_size); + tmp = cfl_arena_malloc(array->arena, new_size); if (tmp != NULL) { memcpy(tmp, array->entries, array->entry_count * sizeof(void *)); diff --git a/src/cfl_kvlist.c b/src/cfl_kvlist.c index d853eb7..204ab77 100644 --- a/src/cfl_kvlist.c +++ b/src/cfl_kvlist.c @@ -99,7 +99,7 @@ struct cfl_kvlist *cfl_kvlist_create_in(struct cfl_arena *arena) list = malloc(sizeof(struct cfl_kvlist)); } else { - list = cfl_arena_alloc(arena, sizeof(struct cfl_kvlist)); + list = cfl_arena_malloc(arena, sizeof(struct cfl_kvlist)); } if (list == NULL) { cfl_report_runtime_error(); diff --git a/src/cfl_sds.c b/src/cfl_sds.c index 304e412..afe89c2 100644 --- a/src/cfl_sds.c +++ b/src/cfl_sds.c @@ -99,7 +99,7 @@ static cfl_sds_t sds_alloc(struct cfl_arena *arena, size_t size) buf = cfl_arena_alloc_external(arena, allocation_size); } else { - buf = cfl_arena_alloc(arena, allocation_size); + buf = cfl_arena_malloc(arena, allocation_size); } } if (!buf) { diff --git a/tests/arena.c b/tests/arena.c index 05cb7de..494fa63 100644 --- a/tests/arena.c +++ b/tests/arena.c @@ -17,6 +17,193 @@ struct test_alignment_probe { union test_max_align value; }; +struct test_allocator_context { + size_t allocation_count; + size_t free_count; + size_t fail_after; + size_t allocation_sizes[8]; +}; + +static void *test_allocator_malloc(void *data, size_t size) +{ + void *result; + struct test_allocator_context *context; + + context = data; + if (context->allocation_count >= context->fail_after) { + return NULL; + } + + result = malloc(size); + if (result != NULL) { + if (context->allocation_count < 8) { + context->allocation_sizes[context->allocation_count] = size; + } + context->allocation_count++; + } + + return result; +} + +static void test_allocator_free(void *data, void *pointer) +{ + struct test_allocator_context *context; + + context = data; + context->free_count++; + free(pointer); +} + +static void public_raw_allocations(void) +{ + unsigned char *zeroed; + unsigned char source[] = {0x01, 0x02, 0x03, 0x04}; + unsigned char *copy; + char *string; + void *pointer; + size_t alignment; + size_t index; + struct cfl_arena *arena; + + arena = cfl_arena_create(128); + TEST_CHECK(arena != NULL); + alignment = offsetof(struct test_alignment_probe, value); + + for (index = 1; index <= 64; index++) { + pointer = cfl_arena_malloc(arena, index); + TEST_CHECK(pointer != NULL); + TEST_CHECK((uintptr_t) pointer % alignment == 0); + } + + zeroed = cfl_arena_calloc(arena, 8, sizeof(unsigned char)); + TEST_CHECK(zeroed != NULL); + for (index = 0; index < 8; index++) { + TEST_CHECK(zeroed[index] == 0); + } + + copy = cfl_arena_memdup(arena, source, sizeof(source)); + TEST_CHECK(copy != NULL); + TEST_CHECK(memcmp(copy, source, sizeof(source)) == 0); + + string = cfl_arena_strndup(arena, "arena-data", 5); + TEST_CHECK(string != NULL); + TEST_CHECK(strcmp(string, "arena") == 0); + string = cfl_arena_strndup(arena, "", 0); + TEST_CHECK(string != NULL); + TEST_CHECK(string[0] == '\0'); + + cfl_arena_destroy(arena); +} + +static void public_raw_allocation_failures(void) +{ + size_t used; + struct cfl_arena *arena; + + arena = cfl_arena_create(128); + TEST_CHECK(arena != NULL); + used = cfl_arena_bytes_used(arena); + + TEST_CHECK(cfl_arena_malloc(NULL, 1) == NULL); + TEST_CHECK(cfl_arena_malloc(arena, 0) == NULL); + TEST_CHECK(cfl_arena_malloc(arena, SIZE_MAX) == NULL); + TEST_CHECK(cfl_arena_calloc(arena, SIZE_MAX, 2) == NULL); + TEST_CHECK(cfl_arena_calloc(arena, 0, 1) == NULL); + TEST_CHECK(cfl_arena_memdup(arena, NULL, 1) == NULL); + TEST_CHECK(cfl_arena_memdup(arena, "x", 0) == NULL); + TEST_CHECK(cfl_arena_strndup(arena, NULL, 0) == NULL); + TEST_CHECK(cfl_arena_strndup(arena, "x", SIZE_MAX) == NULL); + TEST_CHECK(cfl_arena_bytes_used(arena) == used); + + cfl_arena_destroy(arena); +} + +static void options_growth_and_callbacks(void) +{ + char payload[2048]; + cfl_sds_t value; + void *pointer; + struct cfl_arena *arena; + struct cfl_arena_options options; + struct test_allocator_context context; + + memset(&context, 0, sizeof(context)); + context.fail_after = SIZE_MAX; + cfl_arena_options_init(&options); + options.chunk_size = 64; + options.maximum_chunk_size = 256; + options.malloc_fn = test_allocator_malloc; + options.free_fn = test_allocator_free; + options.allocator_context = &context; + + arena = cfl_arena_create_with_options(&options); + TEST_CHECK(arena != NULL); + TEST_CHECK(context.allocation_count == 1); + + pointer = cfl_arena_malloc(arena, 48); + TEST_CHECK(pointer != NULL); + pointer = cfl_arena_malloc(arena, 48); + TEST_CHECK(pointer != NULL); + pointer = cfl_arena_malloc(arena, 96); + TEST_CHECK(pointer != NULL); + TEST_CHECK(context.allocation_count == 4); + TEST_CHECK(context.allocation_sizes[1] < context.allocation_sizes[2]); + TEST_CHECK(context.allocation_sizes[2] < context.allocation_sizes[3]); + + cfl_arena_reset(arena); + TEST_CHECK(cfl_arena_bytes_used(arena) == 0); + pointer = cfl_arena_malloc(arena, 48); + TEST_CHECK(pointer != NULL); + + memset(payload, 'x', sizeof(payload)); + value = cfl_sds_create_len_in(arena, payload, (int) sizeof(payload)); + TEST_CHECK(value != NULL); + cfl_sds_destroy(value); + + cfl_arena_destroy(arena); + TEST_CHECK(context.free_count == context.allocation_count); + + options.maximum_chunk_size = 32; + TEST_CHECK(cfl_arena_create_with_options(&options) == NULL); + options.maximum_chunk_size = 64; + options.free_fn = NULL; + TEST_CHECK(cfl_arena_create_with_options(&options) == NULL); + TEST_CHECK(cfl_arena_create_with_options(NULL) == NULL); + options.free_fn = test_allocator_free; + options.struct_size = offsetof(struct cfl_arena_options, + allocator_context); + TEST_CHECK(cfl_arena_create_with_options(&options) == NULL); +} + +static void callback_allocation_failure(void) +{ + void *pointer; + struct cfl_arena *arena; + struct cfl_arena_options options; + struct test_allocator_context context; + + memset(&context, 0, sizeof(context)); + context.fail_after = 0; + cfl_arena_options_init(&options); + options.chunk_size = 64; + options.malloc_fn = test_allocator_malloc; + options.free_fn = test_allocator_free; + options.allocator_context = &context; + TEST_CHECK(cfl_arena_create_with_options(&options) == NULL); + + context.fail_after = 1; + arena = cfl_arena_create_with_options(&options); + TEST_CHECK(arena != NULL); + TEST_CHECK(cfl_arena_malloc(arena, 1) == NULL); + TEST_CHECK(cfl_arena_bytes_used(arena) == 0); + + context.fail_after = SIZE_MAX; + pointer = cfl_arena_malloc(arena, 1); + TEST_CHECK(pointer != NULL); + cfl_arena_destroy(arena); + TEST_CHECK(context.free_count == context.allocation_count); +} + static void arena_build_and_destroy(void) { struct cfl_arena *arena; @@ -290,6 +477,9 @@ static void bound_external_rounding_and_cache(void) payload_size = 1024 * 1024; payload = malloc(payload_size); TEST_CHECK(payload != NULL); + if (payload == NULL) { + return; + } memset(payload, 'x', payload_size); arena = cfl_arena_create(8192); @@ -340,6 +530,10 @@ static void reclaim_failed_variant_construction(void) } TEST_LIST = { + {"public_raw_allocations", public_raw_allocations}, + {"public_raw_allocation_failures", public_raw_allocation_failures}, + {"options_growth_and_callbacks", options_growth_and_callbacks}, + {"callback_allocation_failure", callback_allocation_failure}, {"arena_build_and_destroy", arena_build_and_destroy}, {"arena_reset", arena_reset}, {"reject_cross_arena_values", reject_cross_arena_values}, diff --git a/tests/installed_consumer/main.c b/tests/installed_consumer/main.c index a65589a..1c806b6 100644 --- a/tests/installed_consumer/main.c +++ b/tests/installed_consumer/main.c @@ -6,7 +6,9 @@ int main(void) { int result; + char *request_name; uint64_t hash; + struct cfl_arena *arena; struct cfl_array *array; result = cfl_init(); @@ -36,5 +38,18 @@ int main(void) return 1; } + arena = cfl_arena_create(0); + if (arena == NULL) { + return 1; + } + + request_name = cfl_arena_strndup(arena, "installed", 9); + if (request_name == NULL || strcmp(request_name, "installed") != 0) { + cfl_arena_destroy(arena); + return 1; + } + + cfl_arena_destroy(arena); + return 0; } From 423afc5108bacd96f5305ea8508b9a99db84de97 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sat, 11 Jul 2026 18:17:11 -0600 Subject: [PATCH 3/4] benchmarks: compare arena growth policies Signed-off-by: Eduardo Silva --- benchmarks/README.md | 12 ++++++++++++ benchmarks/arena.c | 34 +++++++++++++++++++++++++--------- benchmarks/variant_mutable.c | 5 +---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/benchmarks/README.md b/benchmarks/README.md index 021641c..d4cb58f 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -10,6 +10,18 @@ build-bench/benchmarks/cfl-benchmark-variant-arena heap 1000 1000 build-bench/benchmarks/cfl-benchmark-variant-arena arena 1000 1000 8192 ``` +Compare fixed 4 KiB chunks with optional 4-to-64 KiB geometric growth: + +```sh +build-bench/benchmarks/cfl-benchmark-variant-arena arena 1000 1000 4096 +build-bench/benchmarks/cfl-benchmark-variant-arena arena-grow 1000 1000 4096 65536 +``` + +The geometric mode exercises the public arena options used by request-lifetime +encoder workloads. Compare elapsed time, peak RSS, reserved bytes, used bytes, +and slack with a representative graph; fewer chunk allocations can trade CPU +time for retained capacity. + The tool reports elapsed time, peak RSS, glibc heap usage, and arena reserved/used bytes. Use `perf stat` for CPU and allocator-independent memory events: diff --git a/benchmarks/arena.c b/benchmarks/arena.c index 0814097..f217378 100644 --- a/benchmarks/arena.c +++ b/benchmarks/arena.c @@ -18,10 +18,7 @@ static uint64_t monotonic_nanoseconds(void) { - struct timespec now; - - timespec_get(&now, TIME_UTC); - return ((uint64_t) now.tv_sec * 1000000000ULL) + now.tv_nsec; + return cfl_time_now(); } static int build_heap(size_t entries) @@ -46,14 +43,24 @@ static int build_heap(size_t entries) } static int build_arena(size_t entries, size_t chunk_size, + size_t maximum_chunk_size, size_t *reserved, size_t *used) { struct cfl_arena *arena; + struct cfl_arena_options options; struct cfl_kvlist *list; size_t index; char key[32]; - arena = cfl_arena_create(chunk_size); + if (maximum_chunk_size == 0) { + arena = cfl_arena_create(chunk_size); + } + else { + cfl_arena_options_init(&options); + options.chunk_size = chunk_size; + options.maximum_chunk_size = maximum_chunk_size; + arena = cfl_arena_create_with_options(&options); + } if (arena == NULL) { return -1; } @@ -81,6 +88,7 @@ int main(int argc, char **argv) size_t iterations; size_t entries; size_t chunk_size; + size_t maximum_chunk_size; size_t iteration; size_t reserved; size_t used; @@ -97,13 +105,18 @@ int main(int argc, char **argv) iterations = argc > 2 ? strtoull(argv[2], NULL, 10) : 1000; entries = argc > 3 ? strtoull(argv[3], NULL, 10) : 1000; chunk_size = argc > 4 ? strtoull(argv[4], NULL, 10) : 8192; + maximum_chunk_size = argc > 5 ? strtoull(argv[5], NULL, 10) : 65536; reserved = 0; used = 0; start = monotonic_nanoseconds(); for (iteration = 0; iteration < iterations; iteration++) { - if (strcmp(mode, "arena") == 0) { - if (build_arena(entries, chunk_size, &reserved, &used) != 0) { + if (strcmp(mode, "arena") == 0 || + strcmp(mode, "arena-grow") == 0) { + if (build_arena(entries, chunk_size, + strcmp(mode, "arena-grow") == 0 ? + maximum_chunk_size : 0, + &reserved, &used) != 0) { return EXIT_FAILURE; } } @@ -113,7 +126,9 @@ int main(int argc, char **argv) } } else { - fprintf(stderr, "usage: %s heap|arena [iterations] [entries] [chunk-size]\n", + fprintf(stderr, + "usage: %s heap|arena|arena-grow [iterations] " + "[entries] [chunk-size] [maximum-chunk-size]\n", argv[0]); return EXIT_FAILURE; } @@ -132,7 +147,8 @@ int main(int argc, char **argv) printf(" heap_in_use=%zu heap_free=%zu", (size_t) memory.uordblks, (size_t) memory.fordblks); #endif - if (strcmp(mode, "arena") == 0) { + if (strcmp(mode, "arena") == 0 || + strcmp(mode, "arena-grow") == 0) { printf(" arena_reserved=%zu arena_used=%zu arena_slack=%zu", reserved, used, reserved - used); } diff --git a/benchmarks/variant_mutable.c b/benchmarks/variant_mutable.c index efe84a4..62a9868 100644 --- a/benchmarks/variant_mutable.c +++ b/benchmarks/variant_mutable.c @@ -63,10 +63,7 @@ static size_t record_payload_size(const char *distribution, static uint64_t wall_nanoseconds(void) { - struct timespec now; - - timespec_get(&now, TIME_UTC); - return ((uint64_t) now.tv_sec * 1000000000ULL) + now.tv_nsec; + return cfl_time_now(); } static struct cfl_kvlist *create_record(struct cfl_arena *arena, From 4f4ec53617fed904005e8ee7a0367f70787f9d8d Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sat, 11 Jul 2026 18:17:11 -0600 Subject: [PATCH 4/4] docs: document arena allocation interface Signed-off-by: Eduardo Silva --- ARENA.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++--- CHANGELOG.md | 5 +++ README.md | 3 +- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/ARENA.md b/ARENA.md index 6ee2156..3409f98 100644 --- a/ARENA.md +++ b/ARENA.md @@ -1,8 +1,9 @@ # CFL arena allocator `cfl_arena` is an optional allocator for CFL variants, arrays, key/value lists, -kvpairs, and owned SDS strings. It reduces allocator traffic when an application -constructs, mutates, and discards a complete object graph as one unit. +kvpairs, owned SDS strings, and arbitrary request-lifetime objects. It reduces +allocator traffic when an application constructs, mutates, and discards a +complete object graph as one unit. The normal CFL constructors remain heap-backed. Arena use is explicit and does not change existing callers. @@ -40,9 +41,20 @@ struct cfl_arena; struct cfl_arena *cfl_arena_create(size_t chunk_size); struct cfl_arena *cfl_arena_create_ex(size_t chunk_size, size_t large_object_threshold); +void cfl_arena_options_init(struct cfl_arena_options *options); +struct cfl_arena *cfl_arena_create_with_options( + const struct cfl_arena_options *options); void cfl_arena_destroy(struct cfl_arena *arena); void cfl_arena_reset(struct cfl_arena *arena); +void *cfl_arena_malloc(struct cfl_arena *arena, size_t size); +void *cfl_arena_calloc(struct cfl_arena *arena, + size_t count, size_t size); +void *cfl_arena_memdup(struct cfl_arena *arena, + const void *source, size_t size); +char *cfl_arena_strndup(struct cfl_arena *arena, + const char *source, size_t length); + size_t cfl_arena_bytes_reserved(struct cfl_arena *arena); size_t cfl_arena_bytes_used(struct cfl_arena *arena); size_t cfl_arena_large_object_threshold(struct cfl_arena *arena); @@ -57,6 +69,69 @@ Passing zero as `chunk_size` selects the default chunk size. With `cfl_arena_create_ex()`, a zero large-object threshold selects the default policy derived from the chunk size. +## Raw request-lifetime allocation + +Raw allocation supports objects that do not have CFL-specific constructors, +including temporary encoder trees: + +```c +struct request_state *state; +char *name; + +state = cfl_arena_calloc(arena, 1, sizeof(*state)); +name = cfl_arena_strndup(arena, input_name, input_name_length); +if (state == NULL || name == NULL) { + /* The arena remains valid and can still be reset or destroyed. */ +} +``` + +Raw pointers cannot be freed individually. They remain valid until the arena +is reset or destroyed. Returned pointers are aligned for CFL-supported +fundamental C types, including `long double`, pointers, and 64-bit integers. + +The raw allocation rules are: + +- `cfl_arena_malloc()` returns uninitialized storage. +- `cfl_arena_calloc()` checks multiplication overflow and zeroes the result. +- `cfl_arena_memdup()` copies an exact number of bytes. +- `cfl_arena_strndup()` appends a null terminator to the requested prefix. +- Zero-sized `malloc`, `calloc`, and `memdup` requests return `NULL`. +- `strndup` accepts a zero length and returns an allocated empty string. +- A null source, arithmetic overflow, invalid arena, or allocation failure + returns `NULL`. +- Failure leaves the arena usable and does not define `errno`. + +## Growth and allocator options + +Existing constructors retain fixed-size chunks. Optional geometric growth and +allocator callbacks are configured through an initialized options structure: + +```c +struct cfl_arena_options options; + +cfl_arena_options_init(&options); +options.chunk_size = 4096; +options.maximum_chunk_size = 65536; +options.malloc_fn = application_malloc; +options.free_fn = application_free; +options.allocator_context = application_context; + +arena = cfl_arena_create_with_options(&options); +``` + +The first normal chunk uses `chunk_size`. Later chunks double in size until +`maximum_chunk_size`; a request larger than the current chunk size receives a +dedicated chunk large enough for that request. A zero maximum selects fixed +growth, making the maximum equal to the initial chunk size. A maximum smaller +than the initial size is invalid. + +The callback pair is optional, but callers must provide both callbacks or +neither. Callbacks allocate and release the arena context, normal chunks, +external allocations, and cached external allocations. The allocation callback +must return storage with normal `malloc` alignment. CFL implements zeroing and +does not require `calloc` or `realloc` callbacks. `struct_size` must be set by +`cfl_arena_options_init()` so future CFL versions can extend the structure. + ## Arena-aware constructors The following constructors associate new values with an arena: @@ -168,9 +243,10 @@ allocation. ## Large values and external caching -Small and medium allocations come from arena chunks. Allocations at or above -the large-object threshold use separately tracked external storage so unusually -large values do not consume the remainder of a normal chunk. +Raw allocations and small CFL objects come from arena chunks. Raw requests +larger than the active chunk receive a dedicated chunk. The large-object +threshold applies to arena-aware owned SDS values, which use separately tracked +external storage so unusually large strings do not consume normal chunks. Reusable external buffers may remain cached after reset. Configure the maximum cached capacity with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e72a65..37b1f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ This file records the notable changes in each CFL release. +## Unreleased + +- Added public request-lifetime arena allocation, duplication helpers, + allocator callbacks, and optional bounded geometric chunk growth. + ## 1.0.0 - 2026-07-11 The first stable CFL release establishes the variant, container, utility, and diff --git a/README.md b/README.md index d7c452d..c73578d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ smaller interface. - Intrusive lists and lightweight string key/value entries. - Portable 64-bit atomics and time helpers. - xxHash wrappers and CRC32C checksums. -- Optional arenas for allocation-heavy, bounded object graphs. +- Optional arenas for allocation-heavy, bounded object graphs and arbitrary + request-lifetime objects. - CMake support for embedding, installation, tests, and benchmarks. ## Core data structures