C
C serialization is fragmented: each library owns its own object model (DOM trees, streams, generated structs).
Runtime
What it is
C has no virtual machine. gcc or clang compiles C11 to a native binary. Memory is manual: each library allocates and frees, or the benchmark runner does. There is no just-in-time compiler to warm up, and no garbage collector to pause the process. A C microsecond is therefore not the same kind of number as a C# or Python microsecond.
| This suite | |
|---|---|
| Language | C11 through CMake. C++17 is used only for Google libprotobuf. |
| Build | CMake in the Release configuration |
| Prepare | cmake, curl, and c/scripts/fetch-and-build-deps.sh |
| Run | c/scripts/run-benchmarks.sh |
| Memory | Manual allocation. No garbage collector. |
What this suite runs
Third-party libraries are downloaded and built as static dependencies the first time you run the tree. Official Google protobuf uses the shared C++ sysroot created by cpp/scripts/setup-protobuf-sysroot.sh. A serializer is registered only when CMake actually linked it. The configure log prints serializer: … REAL for those rows.
What changes the numbers
The choice of compiler and its optimization flags (-O) changes the numbers more than almost anything else. A CMake Debug build is not comparable to the Dashboard. Each library has its own object model: a DOM tree, a stream, or a generated struct. The suite visitor in v2_codec.c walks the fields so wrappers do not hard-code the V2 graph.
Suite-specific gotchas
Stream mode is adapted for every row. The timed path writes or reads a full buffer through fmemopen, which is an in-memory FILE*. It is not each library’s own incremental stream API.
nanopb, protobuf-c, and protobuf-wire currently time a shared in-tree proto3 helper. They are not full code-generated stacks for those libraries. See caveats.
C rows and C++ rows are not the same runtime. See C vs C++.
Where to go next
The steps to install the toolchain and run the benchmark are in c/README.md. The language reference is C on cppreference.
Benchmark runner
c/(repository root)- Logs:
logs/c/YYYY-MM-DD-HHMMSS.csv - Build: CMake, C11 (+ C++17 for Google libprotobuf)
- Deps:
c/scripts/fetch-and-build-deps.sh; Google protobuf:cpp/scripts/setup-protobuf-sysroot.sh - Registration:
c/src/register_serializers.c - Domain shape: map-style codecs use a single visitor in
c/src/v2_codec.c(v2_write_fixture/v2_read_fixture). Wrappers implement library ops only—they do not hard-code V2 field graphs.
Serializers
| Name | Category | Timed path (what the row measures) |
|---|---|---|
| cJSON, yyjson, jansson, parson, json-c | JSON | Library DOM build + print / parse via visitor ops |
| custom-binary | Binary | Suite length-prefixed V2 baseline (bin_write_fixture / bin_read_fixture) |
| flatcc, avro-c | Schema | Real flatcc builder / avro-c iface write-read wrapping V2 payload bytes |
| libbson | Binary | bson_append_* / bson_iter_* via visitor ops |
| mpack, msgpack-c | Binary | Fixed-buffer map pack + tree/object unpack via visitor ops |
| nanopb, protobuf-c, protobuf-wire | Schema | Shared in-tree proto3 wire for V2 (fixture_pb_v2.h, same field tags as schemas/v2/protobuf/benchmark_v2.proto). Log names stay separate for historical comparison; not full nanopb stream codegen, protoc-gen-c descriptors, or Google upb. |
| protobuf | Schema | Google libprotobuf SerializeToArray / ParseFromArray on generated benchmark_v2.proto messages |
| tinycbor, libcbor, libcbor-stream, qcbor, zcbor | Binary/schema | Native CBOR map encode via visitor ops (libcbor = DOM API, libcbor-stream = streaming cbor_encode_*); decode via each library's native walker (tinycbor buffer walker, libcbor cbor_load). Do not read libcbor-stream deserialize as a streaming decoder. |
| ubj | Binary | In-tree UBJSON markers around suite V2 binary payload (bin_*) |
| libyaml | Text | Official C YAML 1.1 emitter/parser (yaml_emitter_* / yaml_parser_*) |
Specifics
Why each library exists, what problem it was written to solve, and how. Names link to the source repository (or the stdlib / in-tree path this suite times). A version after the name is the last measured SerializerVersion from this suite's latest bench.
cJSON · 1.7.19
cJSON was written as an ultralightweight JSON parser in ANSI C for embedded and application code that could not afford a heavy toolkit. The problem was that existing C JSON stacks pulled large dependencies or a C++ runtime. cJSON solves that with a single-file DOM: parse to a tree of cJSON nodes, mutate, and print.
yyjson · 0.10.0
yyjson was written for high-performance JSON in ANSI C: fast parse and print without giving up a usable DOM. The problem was that lightweight C parsers were slow, and fast parsers were often C++ or SAX-only. yyjson solves that with a compact C implementation and mutable/immutable document APIs.
jansson · 2.15.1
Jansson was created to give C a complete, documented JSON library with a stable API — encode, decode, and manipulate values — not just a minimal parser. The problem was that many C JSON snippets were incomplete or awkward to embed. Jansson solves it with a reference-counted value type and an API aimed at RFC 8259.
parson · 1.5.3
Parson was written as a small, single-file JSON library in C that is easy to drop into a project. The problem was boilerplate-heavy C JSON stacks. Parson solves it with a compact DOM around json.org JSON.
json-c · 0.15
json-c is a long-running C implementation of JSON intended to be the practical library for Unix/C programs. The problem was the lack of a maintained, RFC-oriented JSON C library for system software. It solves that with a C API that aims at RFC 8259.
custom-binary · v2-1.0
This is the suite's length-prefixed V2 baseline, not a published format. It exists so every language has a simple binary control point: write fields with explicit lengths, read them back, no schema compiler.
flatcc · 0.6.3
flatcc is the maintained C implementation of FlatBuffers. FlatBuffers was created at Google so games and other clients could read serialized data without a parsing/unpacking step. flatcc solves the C side with a schema compiler and a C builder/reader.
avro-c · 1.11.3
Apache Avro was created for Hadoop-era data: a compact binary encoding with the schema stored out of band so field names are not repeated. avro-c is the official C implementation of that encoding.
libbson · 1.27.5
libbson is MongoDB's C library for building and iterating BSON documents. BSON exists so MongoDB can store JSON-like documents with a binary, traversable layout. libbson solves that with bson_append_* / bson_iter_*.
mpack · 1.1.1
MPack is a C encoder/decoder for MessagePack, aimed at correctness and a clean buffer/stream API. The problem was that C MessagePack options were either incomplete or awkward to embed. MPack solves it with a documented pack/unpack implementation of the MessagePack spec.
msgpack-c · 6.0.1
msgpack-c is the official C/C++ implementation of MessagePack. MessagePack was created to be as small and fast as a binary format while staying as simple as JSON. The C library solves that with pack/unpack APIs (and a separate C++ API in the same repository).
nanopb · 0.4.9.2
nanopb was written so Protocol Buffers could run on microcontrollers. The problem was that Google's C++ protobuf runtime is far too large for tiny devices. nanopb solves it with a small C implementation and a generator aimed at static allocation.
protobuf-c · 1.5.2
protobuf-c provides C bindings for Google Protocol Buffers. The problem was that official protobuf was C++-first. protobuf-c solves it with protoc-gen-c and a C runtime for the same wire format.
protobuf-wire · wire-v2
This row is the suite's in-tree proto3 tag reader/writer. It exists to measure the published Protocol Buffers encoding itself, without a particular vendor runtime. Field numbers match schemas/v2/protobuf/benchmark_v2.proto.
protobuf · 3.12.4
Protocol Buffers were created at Google so many languages could share a compact, evolving binary contract without hand-written parsers. The problem was ad-hoc binary formats and verbose XML. Protobuf solves it with an IDL, generated code, and a documented tag/length wire format.
tinycbor · 0.6.0
Intel TinyCBOR was written so constrained and systems software could speak IETF CBOR (RFC 7049 / 8949). The problem was that CBOR needed a small, well-specified C encoder/decoder. TinyCBOR solves it with a buffer-oriented C API.
libcbor · 0.11.0
libcbor is a CBOR protocol implementation for C targeting RFC 7049 and RFC 8949. The problem was the need for a full-featured C CBOR DOM and streaming encoder. It solves that with cbor_load for documents and cbor_encode_* for streaming.
libcbor-stream · 0.11.0
libcbor is a CBOR protocol implementation for C targeting RFC 7049 and RFC 8949. The problem was the need for a full-featured C CBOR DOM and streaming encoder. It solves that with cbor_load for documents and cbor_encode_* for streaming. This row times libcbor's streaming cbor_encode_* API, not the DOM cbor_load decoder.
qcbor · 1.6.1
QCBOR was written as a comprehensive, safety-oriented CBOR implementation for professional and embedded use (RFC 8949). The problem was that CBOR stacks were either incomplete or hard to audit. QCBOR solves it with a carefully bounded C encoder/decoder.
zcbor · 0.9
Nordic zcbor was created to generate C from CDDL and to encode/decode CBOR on constrained devices. The problem was writing CBOR by hand against a schema. zcbor solves it with a CBOR codec plus a CDDL code generator.
ubj · 1.0-min
This row is the suite's in-tree UBJSON marker codec around the V2 binary payload. UBJSON was created as a binary cousin of JSON with explicit types. The harness implements the markers so C has a UBJSON size/speed point without a third-party dependency.
libyaml · 0.2.5
libyaml is the official C library for YAML 1.1, written so other languages (PyYAML, Yams, ext-yaml) can share one parser/emitter. YAML exists as a human-friendly config language. This row times the C library directly.
Caveats
- Visitor (map codecs): JSON / MessagePack / CBOR / BSON serializers only implement library primitives; field layout lives in
v2_codec.c. - Protobuf family honesty: the official Google row is
protobuf(libprotobuf + sysroot).nanopb/protobuf-c/protobuf-wirecurrently time the sharedfixture_pb_v2wire codec (domain encode/decode), not each library’s full generated-message stack. Do not read those three as “full library codegen benchmarks.” - Payload-wrapped:
ubj,flatcc, andavro-ckeep kind + binary payload (or builder vector) without full multi-type schema codegen. - Symbol prefixing:
parsonandtinycborare linked with renamed symbols so they co-exist withjanssonandlibcbor. - A serializer is registered only when its library is linked (CMake configure log
serializer: … REAL).
Stream honesty
Stream mode uses an in-memory FILE* (fmemopen) wrapper around full encode/decode buffers — StreamMode=adapted for every stream row. It is not a per-library incremental stream API. See Modes — stream honesty.
Numbers
Measured numbers for this language live on the Dashboard (pre-filtered). Claim level is L1 (one machine, one session) — see Claims and replication.