Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 81 additions & 5 deletions ARENA.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <edsiper@gmail.com>")
set(CPACK_PACKAGE_VENDOR "Fluent Project")
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGING_INSTALL_PREFIX "/")
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
34 changes: 25 additions & 9 deletions benchmarks/arena.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -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;
}
Expand All @@ -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);
}
Expand Down
5 changes: 1 addition & 4 deletions benchmarks/variant_mutable.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions include/cfl/cfl_arena.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down
Loading
Loading