Skip to main content

Measuring value codec costs

ValueCodecBenchmarks measures what each value codec adds over a plain copy: framing, checksums, compression, decompression, and scratch-buffer management. Use it to choose a codec and threshold, not to predict Redis throughput.

It compares a raw copy with Brotli quality 4, Deflate Fastest, LZ4 level 0, and Zstandard level 3, through the IRespireValueCodec destination overloads. Current runs use version 2 (XxHash3) checksums. ValueCodecChecksumBenchmarks compares frame versions 1 and 2 with compression disabled, isolating the checksum cost.

Inputs and comparison​

  • Every codec receives identical already-serialized bytes. Serialization, command construction, sockets, Redis, and cache lookup are excluded.
  • The raw baseline copies the bytes into the same kind of destination without framing.
  • The matrix covers 64 B, 1 KiB, and 16 KiB values of repeated ASCII record text (high redundancy) or pseudorandom bytes from seed 527 (low redundancy). The source records a SHA-256 digest of every input.
  • Thresholds are 0 (always try compression) and the default 1 KiB. Raw cases repeat in each threshold group as a baseline.
  • Each direction has 60 cases (5 implementations × 3 sizes × 2 patterns × 2 thresholds). Encode and decode run as separate .NET 10 jobs with the default 8 MiB decoded-size ceiling.
  • Output capacity is allocated at setup and reused with ResetWrittenCount, which does not clear it. Codecs still pay to clear their own scratch buffers.
  • Setup checks array/destination equivalence, exact decoded bytes, unchanged raw output, and below-threshold framing before timing.
  • MemoryDiagnoser reports managed bytes per operation after warmup. It excludes native compression workspace, peak memory, the retained destination, and the array-returning APIs.

Reproduce and inspect​

The Value codec benchmarks workflow (.github/workflows/benchmark-value-codecs.yml) runs only this fixture. Start it with workflow_dispatch or by adding the benchmark-value-codecs label to a pull request. Each direction job can take up to 50 minutes. A job runs a Dry validation, one representative Brotli case, then the full matrix with two launches. It fails on any missing, duplicate, or incomplete measurement.

To run locally (source: benchmarks/Respire.Benchmarks/ValueCodecBenchmarks.cs):

dotnet run --project benchmarks/Respire.Benchmarks -c Release -f net10.0 -- \
--filter '*ValueCodecBenchmarks*' --allCategories Encode --job Default --launchCount 2 \
--exporters json markdown --artifacts ./codec-results

Use --job Dry --launchCount 1 first, and replace Encode with Decode for reads. Do not run the full benchmark suite.

Artifacts keep the Git revision, SDK/runtime, CPU, packages, logs, BDN reports, input digests, and joined size/time/allocation tables. In those tables:

  • MiB/s is original bytes divided by elapsed time.
  • Stored bytes include the codec frame. RESP bulk bytes add $length\r\n and the trailing \r\n.
  • Neither includes command/key bytes, Redis object overhead, replication, or TLS.

Measured trade-offs (2026-09-30)​

These results predate Zstandard context pooling, the 1 KiB cases, direct-to-destination DEFLATE, and the .NET 10-only workflow. They describe the recorded revision; DEFLATE allocations in particular may differ in current code.

Run 36702369606 measured revision cbe996aad493afc41357ee78e75380c06a602738: 40 cases per job, two Default launches each. All jobs used BenchmarkDotNet 0.15.8, Ubuntu 24.04.5 and SDK 10.0.401. Runtime and CPU differ by job, so compare codecs within a job only:

JobRuntimeCPU (2 physical / 4 logical cores exposed)
.NET 8 encode and decode8.0.31AMD EPYC 9V45, 2.60 GHz
.NET 10 encode10.0.12AMD EPYC 7763, 2.45 GHz
.NET 10 decode10.0.12Intel Xeon Platinum 8573C, 2.30 GHz

16 KiB repeated text, default 1 KiB threshold. Times are mean +/- standard deviation in microseconds.

JobCodecMean +/- StdDev (us)MiB/sManaged B/opStored BRESP bulk B
net8.0-EncodeRaw0.114 +/- 0.00213756201638416394
net8.0-EncodeBrotli9.024 +/- 0.205173107279
net8.0-EncodeDeflate5.407 +/- 0.0612890760198206
net8.0-EncodeLz41.664 +/- 0.03493890142150
net8.0-EncodeZstd3.479 +/- 0.0794491648794
net8.0-DecodeRaw0.115 +/- 0.00213641501638416394
net8.0-DecodeBrotli9.854 +/- 0.1511586327279
net8.0-DecodeDeflate3.129 +/- 0.0334993528198206
net8.0-DecodeLz41.722 +/- 0.01790760142150
net8.0-DecodeZstd2.038 +/- 0.0147667568794
net10.0-EncodeRaw0.199 +/- 0.00027836801638416394
net10.0-EncodeBrotli19.905 +/- 0.20178507279
net10.0-EncodeDeflate6.100 +/- 0.1072561816259267
net10.0-EncodeLz43.153 +/- 0.00649550142150
net10.0-EncodeZstd6.356 +/- 0.2982458648794
net10.0-DecodeRaw0.082 +/- 0.00118995201638416394
net10.0-DecodeBrotli14.978 +/- 0.1811043327279
net10.0-DecodeDeflate2.277 +/- 0.0106862584259267
net10.0-DecodeLz42.431 +/- 0.00864270142150
net10.0-DecodeZstd2.940 +/- 0.0115315568794
  • Brotli stores the smallest frame (72 B).
  • LZ4 encodes fastest with 0 managed B/op.
  • Zstandard stores 87 B with small per-call managed allocations.
  • Deflate sizes differ by runtime (198 B versus 259 B), so its size is not a format guarantee.

16 KiB seeded random data. Every codec falls back to a 16,402 B frame (16,412 B RESP), versus raw 16,384/16,394 B. The failed compression still costs CPU:

RuntimeCodecEncode mean (us)Managed B/op
net8.0Raw0.1140
net8.0Brotli76.9000
net8.0Deflate71.66874104
net8.0Lz410.9280
net8.0Zstd16.86264
net10.0Raw0.2670
net10.0Brotli136.3830
net10.0Deflate121.81875000
net10.0Lz415.4140
net10.0Zstd26.38664

The destination API is not allocation-free for every codec: Deflate allocates about 74-75 KB/op on this input.

64 B repeated text. With the 1 KiB threshold, every codec skips compression and emits an 82 B frame (89 B RESP), versus raw 64/71 B. Encoding takes 0.486-0.501 us on .NET 8 and 0.610-0.660 us on .NET 10, with 0 managed B/op. With threshold 0:

RuntimeCodecEncode mean (us)Managed B/opStored B
net8.0Raw0.007064
net8.0Brotli5.198069
net8.0Deflate3.07263273
net8.0Lz41.035078
net8.0Zstd1.1796482
net10.0Raw0.006064
net10.0Brotli9.147069
net10.0Deflate4.35462472
net10.0Lz41.739078
net10.0Zstd1.5756482

Even the smallest compressed 64 B frame is larger than the raw value. Random 64 B input also yields 82 B frames.

The complete joined results include every case with means, deviations, allocations, payload hashes, and environments. These are single-run, descriptive measurements; they do not establish Redis throughput or a regression threshold.

Interpreting the trade-off​

  • Below-threshold values still pay framing and checksum costs and add 18 bytes.
  • A lower threshold saves storage only when compression saves more than the frame overhead, and it always costs CPU.
  • Low-redundancy values may pay for a failed compression attempt.
  • Decode cost depends on whether the stored frame is compressed, so compare algorithm and size with timing.
  • Runner contention affects timings. Test your real payloads and concurrency before changing the threshold or level.