Skip to content

perf(mojo): vendor one-pass float, 4-digit SWAR, and ctz scan - #1

Closed
leo-gan wants to merge 67 commits into
ugorji:masterfrom
leo-gan:perf-spend-hour
Closed

perf(mojo): vendor one-pass float, 4-digit SWAR, and ctz scan#1
leo-gan wants to merge 67 commits into
ugorji:masterfrom
leo-gan:perf-spend-hour

Conversation

@leo-gan

@leo-gan leo-gan commented Sep 9, 2026

Copy link
Copy Markdown

Summary

  • Vendor gld-json one-pass float parse, 4-digit SWAR, Ember count_trailing_zeros SIMD scan, digit-pair itoa, and skip of the short-dtoa exact search
  • n=1 encode cap 1024 (512 + list ensure resized mid-encode)
  • Pre-size decode lists to n

Measure (official all-single 2026-09-09-160305 vs leo-gan#145)

suite n op leo-gan#145 now vs Ember
message 1 deser 406 348 1.55×
telemetry 1 deser 1678 1233 1.35×
telemetry 100 deser 167450 106745 1.48×
telemetry 1 ser 2020 1319 1.27×
telemetry 100 ser 195051 117007 1.53×
strings 1 deser 2264 2314 0.51×

Fidelity 1.0. Strings deser is still heap-bound (32 Strings). Version stays 0.2.0.

Validation

  • pixi run test in mojo/ (roundtrip ok)
  • Official ./mojo/scripts/run-benchmarks.sh all-single json stem 2026-09-09-160305

ugorji and others added 30 commits July 29, 2026 21:00
I didn't test it out, so expect minor systax issues. Basically
- honor zero-copy
- reuse encoder/decoder for io-based flow also

This should give more expected results. Thanks.
* docs(theory): first pass toward student-friendly course prose

Expand definitions and transitions across Serialization 101–401
so first-year readers can follow jargon. Links, diagrams, and
technical meaning preserved. Further sentence-level rewrites
still needed for dense original passages.

* docs(theory): rewrite course prose sentence-by-sentence for first-year readers

Rewrite Serialization 101–401 articles into clear textbook English.
Split dense multi-clause sentences, define technical terms in plain
language, and replace remaining industry jargon without changing
meaning. Preserve links, diagrams, code samples, and technical claims.

* chore: add clean-logs skill and prune available_runs to latest stems

Add a /clean-logs skill and script that keep only the latest run stem
per language. Refresh available_runs.json to match the pruned set so
the dashboard run picker stays consistent.

* docs: promote Dashboard to top-level MkDocs tab

Restructure site nav to Dashboard | Learn | Languages | Method so the
interactive dashboard is a first-class tab instead of a nested item
under Benchmarks. Point Method and theory cross-links at the new tabs.

* feat(dashboard): shared site wordmark header, keep analytics chrome

Replace the local “Serializer Analytics” header and “Back to Docs”
escape hatch with the docs product wordmark and peer links
(Dashboard · Learn · Languages · Method · GitHub). Section tabs are
Overview · Details · Compare · History. Filter bar, meta panels, and
analytics styling are unchanged.

* docs(dashboard): add Proposal B header wordmark mock artifacts

Keep the before/after HTML mock and screenshot used to approve the
shared wordmark header design.

* docs: add storefront landing page as site root

Replace the root redirect to Serialization 101 with a home storefront
that dual-CTAs into Dashboard and Learn, and links Languages and Method.
Root URL is now the project front door (Proposal D).

* docs: polish site nav, storefront, and language overview template

Make language sidebar rows short and clickable (name · Results), sort
languages alphabetically, and land the Languages tab on C. Refine the
storefront (no Home title, Serializers table matching the menu), hide
auto page titles when requested, and standardize each language overview
to intro + Benchmark runner + sorted Serializers without suite-type
sections. Dashboard site peers use a GitHub icon and link Languages to C.

* feat(analysis): clarify Results sample filter and enrich summary tables

Document the Dashboard Samples outlier policy (default IQR k=1.5) on
language Results pages. Drop Fidelity and Samples-count columns from
Summary, label Total Time columns in µs, and add an Ops/Sec Average
column matching Dashboard all@all aggregation. Regenerate published
results and violin plots.

* chore: remove temporary dashboard mock assets

Drop header-wordmark and languages-nav mock HTML/PNG used during design
review; they are not part of the published site.
#78)

* docs: role-oriented README and dashboard first-visit orientation

Expand reader roles with know/want paths, add honesty rules for reading
benchmark numbers, and surface a dismissible dashboard tip plus clearer
data-type labels. Add the improve-docs skill with Roles/Research/Style.

* docs: restore README to pre-role-oriented wording

Revert the expanded Who it is for section and How to read the numbers
block; keep the original audience table and honesty line under Statistics.

* docs: deep storefront, language explore strips, dashboard workload story

Validate site articles end-to-end: role path cards on home, jump tables on
theory levels, Method reading order and count fixes, explore strips on all
language overviews, and a clearer dashboard (primary filters, viewing strip,
scatter axis help, empty states). Capture README prefs in improve-docs skill.

* docs: drop redundant dashboard strip and language explore bars

Remove the Viewing workload story (filters already show context), remove
lang-explore strips from language overviews, and restore docs/index.md to
the pre-storefront-rewrite layout.
Lands @ugorji's #79 with required codec v1.3.2 bump for Encoder.NumBytesWritten().

Closes #79
Skip UTF-8 validation on decode for trusted generated fixtures (2–12% decode speedup). Comment cleanups from library author @fxamacker.
#83)

Add an experiments/ notebook aimed at builders and researchers: a living
plan, one experiment.yaml per study, a shared sample, and per-language
runs. Experiment 1 asks which JSON library to keep for one nested
document, reports similar/close groups instead of a single winner, and
writes a combined results.md plus results.json a dashboard can reload
from the saved CSVs.
* feat(experiments): add flat-record format comparison across nine languages

Experiment 2 asks whether an internal service should leave JSON for
MessagePack or Protocol Buffers on one small flat record (Sample B,
N=1 and N=100). Results are grouped similar/close/slower per language;
leaving JSON is language-specific on this sample.

Teach implement-experiment to start with new-task and finish by opening
a PR.

* docs(experiments): link Experiment 2 pull request in the plan
…#85)

* feat(experiments): compare one-language stores to portable formats

Experiment 3 asks what we pay to stay readable by other languages on
one flat record (Sample B). Python pickle/cloudpickle/dill, Java
serialization/Kryo/Fory/Hessian, and Go gob are slower than a portable
library on this sample. There is no speed gain here to trade for lock-in.

* docs(experiments): link Experiment 3 pull request in the plan

* chore(experiments): keep run logs local; commit results only

Do not track experiment CSVs. Saved results.md and results.json are
enough. Update implement-experiment so future labs do not add logs.
Keep results.md and results.json. Raw CSVs stay local. Teach
implement-experiment not to commit experiment logs.
* feat(experiments): measure when JSON outgrows a sensor list

Experiment 4 grows Sample C from 8 to 512 numbers and compares JSON,
CBOR, MessagePack, postcard, and Protocol Buffers in Rust and C. Size
is the first number. Trust the Rust curve: C sizes do not grow with
the list. Expand experiment.yaml settings.points lists into one runner
cell per length.

* docs(experiments): link Experiment 4 pull request in the plan
Move JsonconsCbor/Msgpack/Bson into ser_jsoncons.cpp and encode
domain structs with jsoncons member traits. The old path dumped
through nlohmann JSON, which mixed two libraries on the timed
call. BSON batches still wrap arrays because a BSON root must be
a document.

Fixes #82
* Add Experiment 12: is the difference the format, or the library?

Hold one library still (Jackson, nlohmann, ugorji, Bond) and compare
formats, then keep the format still and change the library. Ran Java,
C++, Go, C#, and JavaScript on Sample A (full). Combined page is
experiments/12-format-vs-library/results.md.

* docs(experiments): link Experiment 12 PR #89 in the plan
* Add Experiment 13: do named-JSON ranks stay put?

Same JSON libraries as Experiment 1, all nine languages, Samples A–E,
1 and 100 records, in memory. Also allow experiment.yaml sample.kind
to be a list (needed by later labs that use more than one sample).

* docs(experiments): link Experiment 13 PR #90 in the plan
Python, Java, and Go on Sample D at 1 and 100 records. Combined page
is experiments/05-event-log-formats/results.md.
Java, JavaScript, Go, Rust, and C on Sample A. BSON loses a full
write-and-read. Combined page: experiments/06-document-db-formats/results.md.
C++, C#, JavaScript, Python, and Rust on Sample A and Sample C (512
numbers). Combined page: experiments/07-write-once-read-many/results.md.
* Add Experiment 8: YAML, TOML, and XML versus JSON

Go, Swift, and C# on Sample A and Sample E. YAML stays a file.
Combined page: experiments/08-human-files/results.md.

* Add Experiment 9: size after gzip or zstd

Python only — other language runners do not record compressed size.
Combined page: experiments/09-compression-size/results.md.

* Add Experiments 10 and 11, plus the not-covered list

Experiment 10: 1 vs 100 records (JSON / MessagePack / protobuf) on
Samples B and D, all nine languages.

Experiment 11: memory vs real stream on Sample A in Go, Java, and C++.

PLAN.md now has a Not covered table: what we skipped, why, and how to fix it.
Order: C points (wrapper) then re-run Experiment 4; then experiment-only
Avro/BSON/FlatBuffers rows; then gzip size columns; then small C-steps.
The C runner always built 32 sensor numbers and clamped the list at 64,
so Experiment 4 could not answer the size curve. Read points (and the
other type_config knobs) from the resolved cell and raise V2_MAX_POINTS
to 512.
C sizes now grow with the sensor list and match the Rust curve.
JSON 226→9371 bytes from 8 to 512 numbers.
…iment 5 (#98)

C#, Rust, JavaScript, C, C++, and Swift now sit beside Python, Java, and
Go on Sample D. Combined page lists similar/close sets per language.
Experiment 6: SwiftBSON vs JSON and MessagePack on Sample A.
Experiment 7: C flatcc and Swift FlatBuffers/CapnProto vs protobuf.
One-shot gzip(6) of written bytes after the clock, matching the Python
runner. zstd(3) is filled where an encoder is already available (Go,
Rust, and C/C++/JS when libzstd or Node zstd is present). Java, C#, and
Swift leave SizeZstd empty.

The analysis parser already reads those columns. Experiment 9 is not
re-run here (B4).
…es (#101)

After the runner SizeGzip columns (#100), run JSON vs MessagePack vs
Protocol Buffers on samples E, C, and B in Go, Java, JavaScript, Rust,
C, C++, C#, and Swift. Python numbers are unchanged. Combined page
leads with size after gzip, not write-time ranks.
Default to archive.ubuntu.com and copy jammy debs from the repo root
when the mirror 404s. After this script, CMake reports
serializer: protobuf (libprotobuf) REAL via sysroot.
After the sysroot script (#102), CMake registers protobuf 3.12.4.
On Sample A it writes the same 164 bytes as the in-tree wire helper.
On 512 numbers it is smaller and faster than protobuf-wire.
Python/C# BSON need new harness clients. Sequential vs shuffle needs
a runner flag and a full Experiment 13 re-run. Not small enough to
finish cleanly in this pass.
…Sharp (#106)

Research matched official libraries already used elsewhere in the suite:

- Java FlatBuffers (flatbuffers-java + generated tables from benchmark.fbs)
- Java Cap'n Proto (org.capnproto:runtime + generated BenchmarkCapnp)
- YAML: PyYAML (already a dep), jackson-dataformat-yaml, js-yaml,
  serde_yaml, libyaml, yaml-cpp
- C# MessagePack-CSharp with ContractlessStandardResolver
- C yyjson native FILE* stream path (other C/Swift codecs stay copied)

Experiment tables are not re-run here.
#107)

Re-ran Experiment 7 (Java), Experiment 8 (six YAML languages), and
Experiments 2 and 10 (C# MessagePack-CSharp). Logs stay local.
…reams (#108)

Record correspondent libraries and the D1–D7 order. Do not start this
work until a new task picks it up.
leo-gan and others added 28 commits August 24, 2026 17:24
Keep the one-line run meta; add L1 badge and a Run configuration panel.
* dashboard: surface L1 run identity on Overview

Keep the one-line run meta; add L1 badge and a Run configuration panel.

* dashboard: show stream-honesty counts and Details column

Add Mode chip and Honesty column (visible in stream mode).

* fix: address review feedback for dashboard: show stream-honesty counts and Details column
* dashboard: surface L1 run identity on Overview

Keep the one-line run meta; add L1 badge and a Run configuration panel.

* dashboard: show stream-honesty counts and Details column

Add Mode chip and Honesty column (visible in stream mode).

* fix: address review feedback for dashboard: show stream-honesty counts and Details column

* dashboard: canonicalize mode=bytes|stream; persist policy and ser=

Alias C# string/Stream; add policy and ser query params.
* dashboard: surface L1 run identity on Overview

Keep the one-line run meta; add L1 badge and a Run configuration panel.

* dashboard: show stream-honesty counts and Details column

Add Mode chip and Honesty column (visible in stream mode).

* fix: address review feedback for dashboard: show stream-honesty counts and Details column

* dashboard: canonicalize mode=bytes|stream; persist policy and ser=

Alias C# string/Stream; add policy and ser query params.

* analysis: write language reports under reports/, not docs/

Default unpublished markdown; violins opt-in; leave site Results pages alone.
…at Dashboard

* dashboard: surface L1 run identity on Overview

Keep the one-line run meta; add L1 badge and a Run configuration panel.

* dashboard: show stream-honesty counts and Details column

Add Mode chip and Honesty column (visible in stream mode).

* fix: address review feedback for dashboard: show stream-honesty counts and Details column

* dashboard: canonicalize mode=bytes|stream; persist policy and ser=

Alias C# string/Stream; add policy and ser query params.

* analysis: write language reports under reports/, not docs/

Default unpublished markdown; violins opt-in; leave site Results pages alone.

* docs: move Rust fidelity/category rule to index; point all languages at Dashboard

Add Numbers pointers; keep Results pages live.
* docs: unpublish language Results; Dashboard is the L1 artifact

Delete results pages and violin PNGs; rewrite inbound links; inject Dashboard in Languages nav.

* fix: address review feedback for docs: unpublish language Results
* Add missing forward declarations in bench.h

Newer clang (Apple clang 21+) promotes implicit-function-declaration to
an error under C11+. Declare bench_now_ns and bench_fidelity_cell in
bench.h so run_v2.c can call them without the harness failing to build
from a fresh clone.

Definitions are unchanged (runner.c and batch_cell.c respectively).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* libcbor: streaming wrapper row, DOM tuning, native decoder

Follow-up to the call-path / fairness review request at
PJK/libcbor#433.

Rework the libcbor row so libcbor is benchmarked with its own idiomatic
APIs and no cross-library helper. Net published-benchmark impact: DOM
encode ~1.7–2.2× faster, size on 'document' 25% smaller (matching
tinycbor byte-for-byte), and a new libcbor-stream row that is ~3.5–5.8×
faster still on encode.

Changes in this commit:

* Rename the existing row from "cbor-encode" to "libcbor" so it lines
  up with the vendored library's actual name.

* Drop the tinycbor cross-dependency from the libcbor rows. Add
  ser_libcbor_common.c with bench_libcbor_de, a libcbor DOM-based
  decoder using cbor_load + cbor_map_handle / cbor_array_handle. Both
  rows now use it in place of bench_tinycbor_de.

* DOM wrapper (ser_libcbor.c): use narrowest-width integer builders
  (cbor_build_uint8/16/32/64 and negint equivalents) instead of always
  emitting uint64/negint64. This is the RFC 8949 §4.2.1 preferred
  serialization; it removes the 25% size regression on 'document' and
  ~9% on 'message' that the row was showing on the published chart.

* DOM wrapper: install a bump arena via cbor_set_allocs and reset it
  at the start of every ser() and de() call. This is the documented
  libcbor pattern for hot loops; it removes most of the per-item malloc
  cost that dominated the DOM path's time. Combined with the narrowest-
  width change, DOM encode drops from ~3.5–7× slower than tinycbor to
  competitive.

* New row: libcbor-stream, wrapping libcbor's streaming encoder
  (cbor_encode_map_start / array_start / uint / negint / bool / double /
  string_start in src/cbor/encoding.h). Zero cbor_item_t allocations,
  no refcount traffic, no second serialization walk. This is the fair
  apples-to-apples peer of tinycbor / qcbor / zcbor streaming encoders.
  Output is interoperable CBOR and matches tinycbor's bytes exactly on
  every fixture in the suite.

All 111 roundtrip checks in c_serializer_tests pass for both rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* libcbor-stream: address review — header + bool + catalog

Follow-up on PR feedback:

* ser_libcbor_stream.c: rewrite the stale file-header comment. It
  claimed decode reused the tinycbor visitor reader and that the
  wrapper still kept an indef stack; both were true of an earlier
  version and neither is true now. Also guard w_begin_map / w_begin_array
  against a negative count: v2_write_fixture never passes one today,
  but a future -1 would silently wrap to (size_t)-1 and emit garbage.

* ser_libcbor_stream.c: emit booleans via cbor_encode_bool instead of
  poking 0xF4 / 0xF5 directly, so the row stays "only libcbor ops".
  Same two bytes, no measurable perf impact.

* config/benchmark_config.yaml, c/third_party/VERSIONS.md,
  docs/c/index.md: update from the old "cbor-encode" name to
  "libcbor" and "libcbor-stream" so the catalog matches the row names
  the C runner logs. Splits the libcbor entry into two rows in the
  YAML catalog.

All 111 c_serializer_tests roundtrip checks still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: leo-gan <leo.gan.57@gmail.com>
* docs: rename Method tab to Benchmarks and fix Benchmarks links

Reorder top tabs to Learn, Languages, Benchmarks, Dashboard,
Experiments. Put Adding a Serializer and Adding a Language last
under Benchmarks. Point Test Data and methodology config links at
GitHub so they work on the published site.

* docs: point language-index source links at GitHub

Registration, README, and runner files live outside the MkDocs tree,
so relative ../../lang/... links 404 on the site. Use blob/tree URLs
on master, matching the Benchmarks Test Data table fix.
Move jspbEncode onto the timed path so the three JavaScript
Protocol Buffers rows measure the same work. Rewrite the
Serialization 401 articles in plain textbook English and
refresh the JavaScript Dashboard slice.
* Document the timing contract and stop caching simdjson encode

Add Timing honesty as the suite rule for prepare vs serialize.
Move simdjson encode to nlohmann dump on the clock, and clone
avsc payloads in prepare so timed serialize is only toBuffer.

* Apply the timing contract to remaining dishonest wrappers

Move JS remaps to untimed toDomain, convert Rust prost messages
in prepare_many, walk C++ JSON DOMs instead of parsing twice,
return C# JSON as text, and label Python buffer-then-write
streams as adapted.

* Time Java and Swift 401 pairs as suite value to suite value

protobuf-java and SwiftProtobuf now convert on the timed path so
each pair matches Protostuff and FlatBuffers: Document in, bytes,
Document out.

* Add prepare-and-approve-pr skill

Land a feature branch: run prepare-pr, approve, fix failed checks,
squash-merge, delete the PR branch, and sync local master.

* Simplify dataclass comparison in the Python comparer

Extract field reading so _compare_dataclass no longer branches on
actual type. Compare only when expected is a dataclass, and drop
dead missing-expected-field checks.

* Drop stale DateTime TODOs and test Python log aggregation

Suite payloads use integer timestamps, so the leftover DateTime
TODOs on FsPickler and ServiceStack were misleading. Restore
NativeKind/StreamMode on LogStorage.read_all and cover write/read,
averages, and error dedup.

* Size Java Cap'n Proto write buffer off the clock

The strings N=100 cell overflowed the fixed 64 KiB ArrayOutputStream.
Measure the needed size in prepare and refresh dashboard payloads
from the full re-bench of the languages this branch changed.

* Rename C# protobuf map alias to avoid CS0576 on CI

The hosted SDK treats using Pb = Benchmark.V2 as a conflict with
the generated pb alias. Use Wire instead.
Replace slang and clipped phrases on all thirteen experiment cards
with complete sentences for a high-school reader. The Dashboard
catalog picks up the same copy. Record the writing rule in STYLE.
* Add Kotlin as a tenth benchmark language with 26 serializers.

New kotlin/ Gradle harness (JDK 21) follows the Java prepare/timed/fidelity
contract. Roster covers the kotlinx.serialization format family, JVM JSON
and binary stacks, schema codecs (protobuf-java, protobuf-kotlin, FlatBuffers,
Cap'n Proto, Avro/Avro4k, Thrift), and kaml YAML. Register the runner in
config, CI, dashboard, docs, and analysis fallbacks.

* Fix Kotlin kbson and tomlkt batch encoding.

BSON cannot have an array at the document root, so N>1 cells are wrapped
as { batch: [...] }. tomlkt also uses that wrap key so nested Document.items
does not collide with the batch field and trip the parser.

* Publish Kotlin dashboard data and a 401 comparison page.

Pack the all-single Kotlin run into dashboard/public/data so the language
selector has stats. Sort the catalog alphabetically. Register Kotlin on
experiment 1 and add a 401 article that pairs Protostuff with the two
official protobuf rows on the same 155-byte document.

* Add Kotlin to experiments 2–13 and two more 401 pages.

Wire a Kotlin language block and run.sh into every remaining lab experiment, and register the Kotlin runner in each parent run.sh (including Experiment 1). Add 401 articles for kotlinx-json vs Moshi and FlatBuffers vs protobuf.
* Publish Experiment 1 Kotlin results on the Dashboard.

Experiment 1 already listed Kotlin in experiment.yaml, but the Dashboard tabs come from results.json. Run the Kotlin bakeoff, keep other language summaries when no CSV is on disk, and sync the experiment catalog.

* Time Kotlin on all experiments and require that in prepare-pr.

The Experiments Dashboard tab only lists languages with summarized results. Run experiments 2–13 for Kotlin, keep other language pages intact, and add prepare-pr step 7 so changed languages are timed on every experiment that enables them.
* Add PHP as an eleventh benchmark language

Register PHP in every closed-set consumer and add a Java-shaped CLI
harness (PHP 8.3) that times official codec APIs only. Live rows are
stdlib json, serialize, rybakit-msgpack, google/protobuf, Symfony JSON
and XML, JMS JSON, CBOR, Avro, and Symfony YAML. PECL codecs register
when the extension is loaded.

Time the full 100-rep matrix (stem 2026-08-28-113418, 20k rows, no
error-CSV regressions) and all 13 lab experiments. Pack Dashboard
php_latest / stats_php_latest plus the experiment catalog. Add a 401
article comparing json_encode with official protobuf on document n=1.

Update ADDING_A_LANGUAGE so a new runner is registered everywhere,
experiments are timed, and dashboard gzip of other languages is not
churned.

* Add PHP to the site home Languages table

docs/index.md still said ten runners and omitted PHP. Insert the
15-serializer row in alphabetical order and list this table in the
add-a-language checklist.

* Bump README language count to 11

Match the PHP roster: badge 11 and the lead sentence already
says 200+ libraries across 11 languages.
* Add Zig as a twelfth benchmark language

Native Zig 0.16 runner with the mixed candidate list: official std.json
(two APIs) and std.zon, in-tree comptime-bin, serde.zig across JSON /
MessagePack / YAML / TOML / ZON / XML, zig-msgpack, msgpack.zig, zbor,
and s2s. Schema codecs (protobuf, FlatBuffers, Cap'n Proto) stay out
until their 0.16 codegen paths exist.

* Add Zig protobuf, FlatBuffers, and Cap’n Proto from shared schemas

Generate suite types from the same .proto / .fbs / .capnp files other
languages already compile. Wire honest prepare/encode/decode rows and
drop the intake-list selection table from the Zig overview.

* Publish Zig full-run dashboard and experiment results

Full Zig bench (17 serializers, stem 2026-08-28-173322, 0 error rows)
plus timed Zig rows for experiments 1–13.

* Link Cap’n Proto against libstdc++ for CI

Official libkj.a is built with GCC. Zig’s -lc++ does not provide
std::exception_ptr, so zig build test failed on GitHub Actions.

* Pass -lstdc++ (not Darwin -needed-lstdc++) for Cap’n Proto

GitHub Actions still missed GCC exception_ptr symbols after the
first link tweak. Emit a normal Linux stdc++ / gcc_s link line.

* Build Cap’n Proto with system c++ into libzigcapnp.so

Zig’s LLD cannot resolve GCC exception_ptr from libkj.a. The system
compiler now produces a shared C ABI; Zig links only that .so.
The Zig squash also edited schemas/v2/README.md. dorny treated schemas/**
as a hit for every language, so the master push ran all smokes. Match
only the catalog and .proto contracts, same prose rule as detect-changed-langs.
…obs? (#135)

* feat(experiments): add a starter-kit experiment across twelve languages

Experiment 14 times a short list per language for typical jobs: public
JSON, compact internal MessagePack, and a shared Protocol Buffers file.
It is a place to begin, not a prize. Run logs stay local.

* docs(experiments): link Experiment 14 pull request in the plan
Explain what each language platform is, what this suite actually
runs, and what changes the numbers, so newcomers can read a language
page without already knowing that runtime.
* fix(cpp): time Cap'n Proto encode/decode like libprotobuf

Building the MessageBuilder and walking fields back to suite
structs was on the clock, while protobuf did that work in
untimed prepare/to_domain. Move fill to prepare, keep a reused
builder, time messageToFlatArray/writeMessage and reader setup,
and convert in to_domain.

Addresses capnproto/capnproto#2730.

* chore(cpp): refresh full bench, experiments, and dashboard

Re-bench C++ after the Cap'n Proto prepare/to_domain split
(stem 2026-09-04-111531), retime experiments that enable cpp,
and publish cpp_latest dashboard payloads.
* Add Mojo as a thirteenth benchmark language

Wire a pixi/Mojo 1.0 harness (EmberJson, ehsanmok-json, mojo-cbor,
mojo-protobuf, mojo-avro, mojo-toml) through config, analysis, CI,
experiments, and docs. CBOR/Protobuf are vendored under renamed
packages to avoid conda module collisions; Avro and EmberJson stay
conda packages.

* Publish Mojo full-run dashboard and experiment results

Sync mojo_latest from stem 2026-09-08-155242 and time experiments 01–14
for the new language so the dashboard catalog lists Mojo as ok.
* fix(zig): decode into the trial arena instead of a nested one

The Zig runner already gives every trial a fresh ArenaAllocator and frees it
after the measurement, but three adapters call the arena-wrapping entry point
of their library, which heap-allocates and initialises a *second* arena on
every call:

  std.json          parseFromSlice
  std.json.scanner  parseFromTokenSource
  msgpack.zig       decodeFromSlice

The result is dropped immediately -- only .value is kept -- so the nested arena
buys nothing and is pure per-call overhead. It also leaks in the general case,
since the returned Parsed is never deinit'd; here the trial arena happens to
reclaim it.

Each of these libraries has a *Leaky variant meant for exactly this situation.
std.json documents it on parseFromSlice: "If you are using a
std.heap.ArenaAllocator or similar, consider calling parseFromSliceLeaky
instead." The adapters that take an allocator directly -- zbor, s2s, std.zon,
zig-msgpack -- never paid this and are unchanged.

.allocate = .alloc_always is kept on the std.json calls, so string ownership is
unchanged; this only removes the redundant arena.

Measured with all-single on this machine (Zig 0.16.0, ReleaseFast, median
TimeDeser over reps 1..9, warmup rep dropped as the analysis does):

  std.json           2-19% faster
  std.json.scanner   4-19% faster
  msgpack.zig        8-40% faster

The spread is expected: the nested arena is a roughly fixed per-call cost, so
it is a larger fraction of a faster decoder. It is also why the effect is
largest at data_type_instance_count=100, where deserializeCell calls the
adapter once per item.

Two cells change sign against protobuf: event/N=100 goes 1.25x -> 0.76x and
message/N=100 goes 1.23x -> 0.97x.

protobuf, zbor and s2s are untouched and move only 1.3-2.8% median between
runs, so the changes above are well outside run-to-run noise. FidelityScore
stays 1.0 on every row and no errors file is produced.

* build(zig): pin msgpack.zig to the v0.9.0 tag

The dependency pointed at refs/heads/master, which GitHub redirects to main, so
the suite tracked whatever was on the branch while reporting version 0.7.0 in
the results. Two runs months apart could measure different code under the same
label.

v0.9.0 is tagged, so pin the tag and correct the version the registry reports.
…nt (#140)

Adds a second benchmark row for shamaton/msgpack using its struct-as-array
encoding (MarshalAsArray/UnmarshalAsArray, MarshalWriteAsArray/UnmarshalReadAsArray).
This drops field-name keys entirely, giving a more compact wire shape closer
to schema-based formats (avro/protobuf), and is worth comparing separately
from the default map-based shamaton/msgpack entry.
Vendor gld-json as gldjson_* so it can share a pixi env with mojo-avro.
Wire expected-order encode/decode into the runner and roundtrip tests,
register the row in config and experiments, and refresh Mojo dashboard
payloads from full run 2026-09-09-131928 (fidelity 1.0).
Re-vendor gld-json #6/#7. Official client uses word key compares,
here() value reads, empty Fixture lists, and the no-alloc 9-decimal
float path. Message n=1 deser 1565→484 ns (beats Ember 565).
Official n=1 document/strings/telemetry were 411–491 bytes written
into a 256-byte WireWriter. Vendor gld-json #8 string/list path.
Re-vendor gld-json #9. Telemetry n=1 deser 2490→1678 ns.
Vendor gld-json one-pass number parse, 4-digit SWAR, Ember ctz SIMD
scan, digit-pair itoa, and skip of the short-dtoa exact search. n=1
encode cap is 1024 so list ensure does not resize. Decode lists are
pre-sized to n.

Official 2026-09-09-160305 vs #145: telemetry n=1 deser 1678→1233
(1.35× Ember), n=100 167450→106745 (1.48×). Telemetry n=1 ser
2020→1319 (1.27×), n=100 195051→117007 (1.53×). Message n=1 deser
406→348 (1.55× Ember). Fidelity 1.0.

Version stays 0.2.0.
@leo-gan

leo-gan commented Sep 9, 2026

Copy link
Copy Markdown
Author

Wrong target: previous mojo-json speed PRs land on leo-gan/GLD.SerializerBenchmark (origin), not this fork-upstream PR.

@leo-gan leo-gan closed this Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants