FFI wire-format codec benchmark
bench_codec.nim is a single-process Nim microbenchmark comparing the two FFI
wire-format codecs head-to-head on identical payloads:
- cbor —
cborEncode/cborDecode, self-describing bytes overseq[byte]. The codec thecborABI uses on every boundary crossing. - c (cwire) —
cwirePack/cwireUnpack/cwireFree, flat C-struct shared-memory packing. The codec thecABI uses, emitted for every{.ffi: "abi = c".}type as its<T>_CWirecompanion.
Both paths run in the same process on the same values, so the numbers isolate codec cost only — no thread hop, no callback dispatch, no chronos work. The full FFI round-trip (thread channel + callback) is identical for both ABIs, so the codec is where the ABI difference actually lives.
Running
nimble bench_codec
# or directly, with the size sweep extended to 1 MiB:
nim c -r --mm:orc -d:danger tests/bench/bench_codec.nim --include-1mib
Build with -d:danger (the nimble task does) so the figures reflect optimized
codegen rather than a debug build.
Payload shapes covered
| Type | Shape |
|---|---|
EchoRequest |
1 string + 1 int (small struct) |
EchoResponse |
2 strings |
ComplexRequest |
seq[EchoRequest], seq[string], Option[...] |
BytesPayload |
seq[byte], swept 100 B → 150 KiB (--include-1mib adds 1 MiB) |
Interpreting
Small structs are dominated by CBOR's per-field tag/length framing, so cwire
wins by a large factor. As payloads grow into big seq[byte] blobs, both codecs
become memcpy-bound and the ratio converges toward ~1×. The byte-blob sweep
also reports throughput (MiB/s) for each codec.
Note: this benchmark exercises the
cABI codec (the cwire companions on the Nim side). Wiring thecABI through the full proc-dispatch path and the foreign (C++/Rust) generators is tracked separately; onlycborcurrently generates working end-to-end bindings.