zig-pkl
How fast does each library decode pkl-binary? Apple M5, median of 31 rounds, longer is faster

Dynamic tree, configuration fixture

pkl-zig206.7 MiB/s
zig-pkl669.7 MiB/s

Dynamic tree, mixed value kinds

pkl-zig241.0 MiB/s
zig-pkl554.2 MiB/s

Zig structs, configuration fixture

pkl-zig190.3 MiB/s
zig-pkl531.3 MiB/s
How long does a request take? Apple M5, Pkl 0.32.2, shorter is faster

Start the server, create an evaluator, evaluate, shut down

pkl-zig8.41 ms
zig-pkl7.76 ms

Evaluate on a running evaluator

pkl-zig31.42 µs
zig-pkl25.48 µs

Same-run comparison with pkl-zig

bench/compare.sh builds zig-pkl with Zig 0.17.0 and pkl-zig 6a113ab with Zig 0.16.0, runs both against the same Pkl executable, and applies the release gates in baseline.md. pkl-zig is used only through its public API.

ZIG_0_16=/path/to/zig-0.16.0/zig PKL_EXEC=/path/to/pkl bench/compare.sh 21

Method

  • Both programs, bench/zig-pkl.zig and bench/pkl-zig/src/main.zig, run the same cases with the same iteration counts, backing allocator (std.heap.smp_allocator), timer, and output format.
  • Codec cases decode the pkl-binary fixtures in bench/fixtures 2000 times after 200 unmeasured decodes. decode_config and decode_mixed build each library's dynamic value tree; typed_config decodes into the same Zig structs and frees them.
  • Allocations count calls that reach the backing allocator, so each arena chunk counts once.
  • The cold case starts pkl server, creates an evaluator, evaluates (1 + 2) * 14, and shuts down, 10 times per round after one unmeasured cycle. Each round reports the median cycle and the median of each phase. The warm case evaluates the same expression 200 times on a running evaluator after 20 unmeasured evaluations.
  • Each library runs once unrecorded, then the rounds alternate which library goes first. Reported figures are medians over the rounds.
  • The fixtures are evaluated through modulepath: URIs, so their bytes do not depend on the checkout path. bench/fixtures/*.pkl regenerate them with pkl eval --module-path bench/fixtures -f pkl-binary -o bench/fixtures/<name>.pklbin modulepath:/<name>.pkl.

CI runs the comparison on Linux x86_64 in the bench job, writes the summary to the job page, and uploads the raw results.

Results on macOS arm64

Apple M5, Pkl 0.32.2 native, 31 rounds. Raw data: results/macos-arm64.

Casepkl-zigzig-pklSpeeduppkl-zig allocationszig-pkl allocations
decode_config206.7 MiB/s669.7 MiB/s3.24x19628
decode_mixed241.0 MiB/s554.2 MiB/s2.30x18218
typed_config190.3 MiB/s531.3 MiB/s2.79x2284330
cold evaluator8.41 ms7.76 ms1.08x
warm evaluate31.42 µs25.48 µs1.23x
Cold phasepkl-zigzig-pkl
start and create evaluator7.34 ms6.64 ms
evaluate0.38 ms0.35 ms
close and shut down0.76 ms0.80 ms
GateRequirementResultVerdict
Codec speedgeometric mean at least 1.25x2.75xpass
Allocationsat least 25% fewer in every codec case86% to 99.6% fewerpass
Cold evaluatorat most 5% slower7.7% fasterpass
Warm evaluateat most 5% slower18.9% fasterpass

Codec throughput on a laptop varies by about 20% between runs; the ratios are measured in the same run.

Results on Linux x86_64

GitHub-hosted ubuntu-24.04 runner with 2 vCPUs, Pkl 0.32.2 native, 15 rounds, from the CI bench job. Each CI run publishes its own summary and raw data.

Casepkl-zigzig-pklSpeedup
decode_config110.7 MiB/s263.0 MiB/s2.38x
decode_mixed122.8 MiB/s215.2 MiB/s1.75x
typed_config102.4 MiB/s217.8 MiB/s2.13x
cold evaluator7.67 ms7.66 ms1.00x
warm evaluate109.18 µs91.17 µs1.20x
Cold phasepkl-zigzig-pkl
start and create evaluator5.27 ms5.28 ms
evaluate0.50 ms0.48 ms
close and shut down1.79 ms1.88 ms

Cold start

The cold case is bounded by work inside Pkl that both libraries pay: starting the server and, after stdin closes, the server's own exit, which takes about 2.1 ms on the Linux runner. The remaining shutdown difference on Linux is the thread that keeps the tail of the server's stderr for error reports; pkl-zig does not capture stderr.

Measured choices, on the Linux runner unless noted:

ChoiceEffect on the cold case
Run pkl --version before starting the server, as pkl-go doesadds a full pkl --version, about 5.8 ms on macOS
Run pkl --version while the server starts17% slower than pkl-zig: the second Pkl process competes with the server
Run pkl --version only for options that need a release newer than 0.25.3no cost for other options
Start all reader workers with the managerabout 0.18 ms per thread at start and 0.25 ms to join them at shutdown
Start reader workers when callbacks arriveno cost without module or resource readers
Poll for server exit every 100 µswakes up to 100 µs late; shutdown about 0.1 ms slower on macOS, no measurable difference on Linux
Wait on a pidfd (Linux) or kqueue (macOS)wakes when the server exits; polling remains the fallback
Kill the server instead of waiting for it to exitabout 0.6 ms faster on macOS, but external readers and loggers lose their close notification; rejected