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
15 changes: 13 additions & 2 deletions ARENA.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,19 @@ Setting the limit to zero disables external-buffer caching. Reducing the limit
immediately trims the cache. A larger limit can improve throughput for repeated
large payloads but may increase retained memory and RSS.

The arena also maintains size classes for commonly-sized owned SDS values.
Callers do not need to select a class.
The arena also maintains size classes for owned SDS values up to 1 KiB.
Callers do not need to select a class. Classes below 128 bytes grow in
alignment-sized steps of the complete allocation, so short strings such as map
keys use at most one alignment unit more than they need; larger classes grow
by about 25 percent. A released string is recycled for the next string of the
same class. `cfl_sds_alloc()` reports the class capacity, which can exceed the
requested length.

Variants, kvpairs, key/value lists, arrays, and SDS headers are aligned for the
pointers, sizes, 64-bit integers, and doubles they contain. This can be less
strict than the raw allocation alignment, which also covers `long double`, so
CFL objects pack more densely in arena chunks. Raw allocations keep the public
alignment guarantee.

## Memory statistics

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ This file records the notable changes in each CFL release.

- Added public request-lifetime arena allocation, duplication helpers,
allocator callbacks, and optional bounded geometric chunk growth.
- Reduced arena memory for CFL object graphs: objects use their natural
alignment instead of the raw allocation alignment, and owned SDS strings use
finer size classes. Decoded log records use 27 to 31 percent less arena
memory. Owned SDS blocks are no longer zeroed before initialization.
- Added a bump-allocation fast path for the current arena chunk.
- Fixed the alignment of external arena allocations, which returned storage
at a misaligned offset for large owned SDS strings.
- Added the `cfl-benchmark-arena-records` benchmark for decoded record
batches.

## 1.0.0 - 2026-07-11

Expand Down
3 changes: 3 additions & 0 deletions benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ target_link_libraries(cfl-benchmark-variant-arena cfl-static)

add_executable(cfl-benchmark-variant-mutable variant_mutable.c)
target_link_libraries(cfl-benchmark-variant-mutable cfl-static)

add_executable(cfl-benchmark-arena-records records.c)
target_link_libraries(cfl-benchmark-arena-records cfl-static)
17 changes: 17 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,20 @@ document is still alive. Compare them with `arena_initial_used` and
`arena_used`. `max_rss_kb` is process-level high-water RSS and can remain the
same for small workloads because it is page-granular and includes executable,
library, and allocator-retained pages.

## Decoded record batches

The records benchmark builds batches of small log records, each with short
keys, a nested map, an array, and string, integer, and double values, as
produced by decoding a buffer of telemetry records. Arena batches are reset
between iterations; heap batches destroy every record:

```sh
build-bench/benchmarks/cfl-benchmark-arena-records arena 512 3000
build-bench/benchmarks/cfl-benchmark-arena-records heap 512 3000
```

Optional arguments select `referenced` or `owned` values, enable record edits
(`1` removes, inserts, and renames keys), and set the arena chunk size. The
tool reports nanoseconds per record and, for arenas, used and reserved bytes
per record.
195 changes: 195 additions & 0 deletions benchmarks/records.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*
* Decoded telemetry batch benchmark: every batch holds many small records
* with short keys, a nested map and an array, as produced by decoding a
* buffer of log records. Values can reference the source buffer or be owned
* copies, and records can be edited after construction.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#if !defined(CFL_SYSTEM_WINDOWS)
#include <sys/resource.h>
#endif

#include <cfl/cfl.h>

struct batch_options {
size_t records;
int referenced;
int mutate;
};

static int insert_string(struct cfl_kvlist *list, char *key, char *value,
int referenced)
{
return cfl_kvlist_insert_string_s(list, key, strlen(key), value,
strlen(value), referenced);
}

static struct cfl_variant *build_record(struct cfl_arena *arena, size_t index,
struct batch_options *options)
{
char message[64];
struct cfl_kvlist *record;
struct cfl_kvlist *http;
struct cfl_array *tags;
struct cfl_variant *root;

snprintf(message, sizeof(message), "request %zu completed", index);

record = cfl_kvlist_create_in(arena);
http = cfl_kvlist_create_in(arena);
tags = cfl_array_create_in(arena, 2);
if (record == NULL || http == NULL || tags == NULL) {
return NULL;
}

/* referenced values must outlive the record, keep them static */
if (insert_string(record, "msg", options->referenced ?
"request completed" : message,
options->referenced) != 0 ||
insert_string(record, "level", "info", options->referenced) != 0 ||
insert_string(record, "user", "u-1234", options->referenced) != 0 ||
insert_string(record, "password", "secret", options->referenced) != 0 ||
insert_string(http, "path", "/api/v1/items", options->referenced) != 0 ||
insert_string(http, "method", "GET", options->referenced) != 0 ||
cfl_kvlist_insert_int64(http, "status", 200) != 0 ||
cfl_kvlist_insert_kvlist(record, "http", http) != 0 ||
cfl_kvlist_insert_double(record, "duration_ms", 12.5) != 0 ||
cfl_array_append_string_s(tags, "edge", 4, options->referenced) != 0 ||
cfl_array_append_string_s(tags, "eu-west-1", 9,
options->referenced) != 0 ||
cfl_kvlist_insert_array(record, "tags", tags) != 0) {
return NULL;
}

if (options->mutate) {
if (cfl_kvlist_remove(record, "password") != CFL_TRUE ||
cfl_kvlist_insert_int64(record, "x", 1) != 0 ||
cfl_kvlist_rename_s(record, "user", 4, "user.id", 7) != 0) {
return NULL;
}
}

root = cfl_variant_create_from_kvlist_in(arena, record);
return root;
}

static int run_batch(struct cfl_arena *arena, struct cfl_variant **roots,
struct batch_options *options)
{
size_t index;
int result;

result = 0;
for (index = 0; index < options->records; index++) {
roots[index] = build_record(arena, index, options);
if (roots[index] == NULL) {
result = -1;
break;
}
}

/* arena records are released by the reset, heap ones one by one */
if (arena == NULL) {
for (index = 0; index < options->records; index++) {
if (roots[index] != NULL) {
cfl_variant_destroy(roots[index]);
}
}
}

return result;
}

int main(int argc, char **argv)
{
const char *mode;
size_t batches;
size_t batch;
size_t chunk_size;
size_t reserved;
size_t used;
uint64_t start;
uint64_t elapsed;
struct cfl_arena *arena;
struct cfl_variant **roots;
struct batch_options options;
#if !defined(CFL_SYSTEM_WINDOWS)
struct rusage usage;
#endif

mode = argc > 1 ? argv[1] : "arena";
options.records = argc > 2 ? strtoull(argv[2], NULL, 10) : 512;
batches = argc > 3 ? strtoull(argv[3], NULL, 10) : 2000;
options.referenced = argc > 4 ? strcmp(argv[4], "owned") != 0 : CFL_TRUE;
options.mutate = argc > 5 ? atoi(argv[5]) : 0;
chunk_size = argc > 6 ? strtoull(argv[6], NULL, 10) : 16384;

if ((strcmp(mode, "arena") != 0 && strcmp(mode, "heap") != 0) ||
options.records == 0) {
fprintf(stderr,
"usage: %s arena|heap [records] [batches] "
"[referenced|owned] [mutate] [chunk-size]\n", argv[0]);
return EXIT_FAILURE;
}

roots = calloc(options.records, sizeof(struct cfl_variant *));
if (roots == NULL) {
return EXIT_FAILURE;
}

arena = NULL;
if (strcmp(mode, "arena") == 0) {
arena = cfl_arena_create(chunk_size);
if (arena == NULL) {
free(roots);
return EXIT_FAILURE;
}
}

reserved = 0;
used = 0;
start = cfl_time_now();
for (batch = 0; batch < batches; batch++) {
if (run_batch(arena, roots, &options) != 0) {
fprintf(stderr, "batch construction failed\n");
cfl_arena_destroy(arena);
free(roots);
return EXIT_FAILURE;
}
if (arena != NULL) {
reserved = cfl_arena_bytes_reserved(arena);
used = cfl_arena_bytes_used(arena);
cfl_arena_reset(arena);
}
}
elapsed = cfl_time_now() - start;

printf("mode=%s records=%zu batches=%zu values=%s mutate=%d "
"elapsed_ns=%llu ns_per_record=%.2f",
mode, options.records, batches,
options.referenced ? "referenced" : "owned", options.mutate,
(unsigned long long) elapsed,
(double) elapsed / (double) (batches * options.records));
if (arena != NULL) {
printf(" arena_reserved=%zu arena_used=%zu used_per_record=%.1f "
"reserved_per_record=%.1f", reserved, used,
(double) used / (double) options.records,
(double) reserved / (double) options.records);
}
#if !defined(CFL_SYSTEM_WINDOWS)
getrusage(RUSAGE_SELF, &usage);
printf(" max_rss_kb=%ld", usage.ru_maxrss);
#endif
putchar('\n');

cfl_arena_destroy(arena);
free(roots);
return EXIT_SUCCESS;
}
Loading
Loading