C++
C++ serialization spans header-only JSON (nlohmann, RapidJSON, ArduinoJson, glaze), SIMD parse (simdjson), C libraries callable from C++ (yyjson), schemaless binary (MessagePack, cereal, bitsery, zpp_bits, CBOR/BSON via jsoncons), and schema / zero-copy families (official libprotobuf, in-tree Protobuf wire, FlatBuffers, FlexBuffers).
Runtime
What it is
C++ compiles to native machine code. There is no virtual machine. This runner requires C++20. Memory is mostly handled by RAII (Resource Acquisition Is Initialization): an object frees its resources in its destructor when it goes out of scope. That is still not a garbage collector. Leaks and extra copies remain the programmer’s problem, and the library’s.
| This suite | |
|---|---|
| Language | C++20 (required) |
| Build | CMake 3.16 or newer, Release configuration, g++ or clang++ |
| Dependencies | CMake FetchContent downloads into cpp/third_party/ (network needed once) |
| Prepare | A C++20 compiler and cmake. Check with ./scripts/check-host-requirements.sh cpp. |
| Run | cpp/scripts/run-benchmarks.sh |
| Memory | RAII and the heap. No garbage collector. |
What this suite runs
The first CMake configure downloads the pinned third-party libraries. Official protobuf is a separate sysroot created by cpp/scripts/setup-protobuf-sysroot.sh. It is not the system libprotobuf. glaze is pinned to v2.9.5 because glaze v3 and later require C++23.
What changes the numbers
The compiler’s optimization level changes the numbers more than almost anything else. Header-only JSON (nlohmann) and SIMD parse (simdjson) are different designs. They belong in the same ranking only when you stay inside one family.
A C library called from C++, such as yyjson, is a C++ call path. It is not a second measurement of the C suite. See C vs C++.
Suite-specific gotchas
simdjson is optimized for parse. Serialize in this suite is prepared minified JSON, so that row does not claim a SIMD encoder.
These times cannot be ranked against C, C#, or Python as one contest.
Where to go next
The steps to install the toolchain and run the benchmark are in cpp/README.md. The language reference is C++ on cppreference.
Benchmark runner
- Directory:
cpp/(repository root) - Output: monorepo
logs/cpp/YYYY-MM-DD-HHMMSS.csv(Language=cpp, times in nanoseconds) - Runner:
cpp/scripts/run-benchmarks.sh {smoke|all-single|full|research} - Build: CMake C++20, deps via
FetchContent→cpp/third_party/(pins incpp/third_party/VERSIONS.md) - Official Protobuf:
cpp/scripts/setup-protobuf-sysroot.sh(libprotobuf 3.12 + protoc, no root install) - Registration:
cpp/src/register.cpp
Serializers
| Serializer | Category | Library | Optimal call path | Notes |
|---|---|---|---|---|
| arduinojson | JSON | ArduinoJson | serializeJson / deserializeJson (bytes + stream) |
Embedded/IoT; native stream |
| avro | Schema | suite avro-binary | zigzag/varint + array blocks | Avro binary encoding |
| avro_c | Schema | avro-c | cached iface + value_write/read | Real Avro C lib from C++; stream adapted |
| bitsery | Binary | bitsery | serializer object/container |
Explicit schema |
| boost_serialization | Binary | Boost.Serialization | binary_o/iarchive (bytes + stream) | Optional (system lib); native stream |
| capnproto | Schema | Cap'n Proto | messageToFlatArray / writeMessage of a prepared MallocMessageBuilder; decode is FlatArrayMessageReader / InputStreamMessageReader |
Domain fill is prepare; field walk is to_domain. native stream |
| cereal | Binary | cereal | BinaryOutput/InputArchive on ostream/istream |
C++-native archives; native stream |
| cista | Binary | Cista++ | cista::serialize / deserialize |
Offset graphs; convert in prepare |
| custom_binary | Binary | harness | length-prefixed fields | Baseline; stream adapted |
| flatbuffers | Schema | flatbuffers | FlatBufferBuilder |
C++ primary; C uses flatcc |
| glaze | JSON | stephenberry/glaze | glz::write_json / glz::read_json on domain structs |
Direct-to-memory JSON; C++20 pin v2.9.5 (v3+ needs C++23); stream adapted |
| flexbuffers | Schema | flatbuffers | flexbuffers::Builder / GetRoot |
Schemaless FB family |
| jsoncons_bson | Binary | jsoncons | bson::encode/decode on domain structs |
BSON document; native stream |
| jsoncons_cbor | Binary | jsoncons | cbor::encode/decode on domain structs |
CBOR; native stream |
| jsoncons_msgpack | Binary | jsoncons | msgpack::encode/decode on domain structs |
MessagePack; native stream |
| msgpack | Binary | msgpack-c (C++ API) | packer + sbuffer / unpack; stream packer + unpacker |
Official C++ API; native stream |
| nlohmann_bson | Binary | nlohmann/json | to_bson / from_bson (+ ostream/istream) |
BSON (object root); native stream |
| nlohmann_cbor | Binary | nlohmann/json | to_cbor / from_cbor (+ ostream/istream) |
IETF CBOR; native stream |
| nlohmann_json | JSON | nlohmann/json | dump / parse; stream << / parse(istream) |
De-facto C++ JSON; native stream |
| nlohmann_msgpack | Binary | nlohmann/json | to_msgpack / from_msgpack (+ ostream/istream) |
Multi-format nlohmann; native stream |
| nlohmann_ubjson | Binary | nlohmann/json | to_ubjson / from_ubjson (+ ostream/istream) |
UBJSON; native stream |
| protobuf | Schema | libprotobuf (Google) | SerializeToArray / ParseFromArray on prepared messages |
Official C++ runtime; sysroot via setup script |
| protobuf-wire | Schema | suite wire | proto3 field tags | In-tree codec; same field numbers as shared .proto |
| rapidjson | JSON | Tencent/rapidjson | Writer + Document::Parse; stream O/IStreamWrapper |
SAX/DOM hot path; native stream |
| simdjson | JSON | simdjson | dom::parser::parse |
Ser = prepared minified JSON; stream adapted |
| thrift | Schema | suite TBinaryProtocol | field type+id + STOP | Apache Thrift binary; stream adapted |
| yas | Binary | niXman/yas | yas::save/load mem\|binary |
Top-tier microbench staple |
| yyjson | JSON | yyjson | yyjson_mut_write / yyjson_read |
Also in C suite; stream adapted |
| zpp_bits | Binary | zpp_bits | zpp::bits::out / in |
Compile-time binary |
Call-path contract
prepare(fixture) # untimed: DOM/maps, buffers, domain convert
for rep:
serialize_bytes / stream # timed
deserialize_bytes / stream # timed (codec only)
to_domain (if needed) # untimed
fidelity(expected, actual) # untimed
C vs C++ — clear separation
| Concern | C benchmark runner (c/) |
C++ benchmark runner (cpp/) |
|---|---|---|
| CBOR | tinycbor, libcbor, QCBOR, zcbor | jsoncons CBOR |
| FlatBuffers | flatcc (C) | google/flatbuffers (C++) |
| JSON focus | cJSON, yyjson, jansson, parson, json-c | nlohmann, RapidJSON, simdjson, arduinojson, yyjson, glaze |
| Language id | c |
cpp |
| MessagePack | mpack, msgpack-c C API | msgpack-c C++ API (msgpack.hpp) |
| Object model | C structs + function pointers | C++20 structs + virtual ISerializer |
| Protobuf | Google libprotobuf (protobuf), plus nanopb / protobuf-c / protobuf-wire (shared suite wire helper) |
official libprotobuf + in-tree protobuf-wire |
Libraries that work for both C and C++
Some projects are C libraries with a pure C API. They are valid from C++ via extern "C" includes. The suite registers them carefully:
- yyjson (registered in both benchmark runners)
- Why: Written in C, ships
yyjson.hwith C linkage; C++ can call it without a separate C++ port. - How: C++ includes
yyjson.hand usesyyjson_read/yyjson_mut_write(same recommended APIs as the C benchmark runner). -
Example:
vs C benchmark runner#include <yyjson.h> yyjson_doc* doc = yyjson_read(ptr, len, 0); char* out = yyjson_write(doc, 0, &out_len);ser_yyjson.cwith the same calls. -
msgpack-c (related but not the same registration)
- Why: One repository provides two APIs: C (
msgpack.h) and C++ (msgpack.hpp). - How: C suite uses pack/unpack C functions; C++ suite uses
msgpack::packer/msgpack::unpack. -
Wire format: Compatible MessagePack; call path and type mapping differ.
-
Protobuf family (shared schema, different runtimes)
- Why: The suite
.protois language-agnostic; C and C++ use different encoders for the same field numbers. - How: Both benchmark runners register official libprotobuf (
protobufrow, sysroot viasetup-protobuf-sysroot.sh) plus an in-tree protobuf-wire baseline. C also keeps log namesnanopb/protobuf-cthat currently time the sharedfixture_pb_v2wire helper (see C overview caveats)—not full generated nanopb/protoc-gen-c stacks. All field numbers align withschemas/v2/protobuf/benchmark_v2.proto. -
Example field:
Message.f_int32 = 2is wire tag(2<<3)|0in both. -
FlatBuffers family (shared idea, different codegens)
- Why: Google FlatBuffers is C++-first; flatcc is the maintained C implementation.
- How: C benchmark runner → flatcc builder/reader; C++ benchmark runner →
flatbuffers::FlatBufferBuilder(+ FlexBuffers). -
Not interchangeable binaries without matching schema/codegen.
-
Avro family
- Why: Same Avro binary encoding (zigzag ints, length-prefixed strings, array blocks).
- How: C benchmark runner → avro-c; C++ benchmark runner → in-tree Avro binary codec for suite types (Apache avro-cpp is heavy to FetchContent; wire follows Avro 1.x binary).
-
Example:
string= zigzag/longlength + bytes; arrays end with a zero count block. -
Not dual-registered (C-only or C++-only by design)
- C-only in suite: cJSON, jansson, parson, json-c, mpack, tinycbor, QCBOR, libbson, nanopb/protobuf-c log rows, flatcc, avro-c, zcbor.
- C++-only in suite: nlohmann, RapidJSON, simdjson, arduinojson, glaze, cereal, bitsery, zpp_bits, jsoncons, google flatbuffers C++ API.
Rule of thumb: If a library is pure C and already measured under Language=c, re-registering under C++ only makes sense when the C++ call path is a first-class usage mode (yyjson) or when the API surface differs (msgpack C vs C++). Do not treat C and C++ rows as interchangeable runtimes for ranking.
Caveats
- glaze is pinned to v2.9.5, the last release that builds as C++20. Glaze v3+ requires C++23 (GCC 12+ / Clang 15+). This pin measures JSON via
write_json/read_jsonon suite structs; CBOR is not registered (it landed after the C++20 line). Stream is adapted. - simdjson is optimized for parse; serialize is prepared minified JSON (same honesty as Rust/JS suite entries).
- protobuf is official libprotobuf + protoc-generated stubs from
schemas/v2/protobuf/benchmark_v2.proto(requirescpp/scripts/setup-protobuf-sysroot.sh). Domain→Message conversion is untimed (prepare/to_domain). - capnproto follows the same split:
preparefills a reusedMallocMessageBuilder; the timer coversmessageToFlatArray/writeMessageand reader setup;to_domainwalks fields into suite structs. That matches libprotobuf and the timing contract. - protobuf-wire is the previous in-tree proto3 field-tag codec (no libprotobuf); kept for comparison when the sysroot is absent or for wire-only baselines.
- flatbuffers blob-root path embeds suite payload via
FlatBufferBuilder(typed tables generated whenflatcruns). - Stream mode is native where the library exposes streams/buffers and the benchmark runner uses them (
VecOutStream/VecInStream, Cap’n ProtowriteMessage, msgpack packer/unpacker, etc.); others are adapted (stream path = bytes path). - First CMake configure downloads pinned deps into
cpp/third_party/(network required once).
Also: cpp/README.md. Serialization Categories.
Numbers
Measured numbers for this language live on the Dashboard (pre-filtered). Claim level is L1 (one machine, one session) — see Claims and replication.
Design choices
- Prepare outside the loop — DOM trees, packers, flexbuffers builders, domain→wire convert.
- Optimal APIs — library-recommended encode/decode; no pretty-print JSON.
- Dual mode —
bytesandstreamwithStreamModemetadata. - C++20 — ArduinoJson v7 / zpp_bits / modern
std::variantfixtures.