Skip to content

fix(serialize): strict validation in __unserialize() rejection paths - #225

Merged
orieg merged 1 commit into
mainfrom
fix/unserialize-strict-validation
Oct 8, 2026
Merged

orieg merged 1 commit into
mainfrom
fix/unserialize-strict-validation

Conversation

@orieg

@orieg orieg commented Oct 8, 2026

Copy link
Copy Markdown
Owner

What

Judy::__unserialize() now validates its payload strictly, rejecting anything __serialize() cannot produce:

  • Unknown top-level keys throw (including integer keys): "Invalid serialization data for Judy array".
  • optimizeIteration must be a literal bool, not merely truthy.
  • Out-of-range types are rejected before JTYPE, so the constructor-facing E_WARNING in the JTYPE macro no longer leaks out of unserialize().
  • optimizeIteration=true on a type that cannot honour it throws "Invalid Judy type in serialized data".

The optimizeIteration/type check runs before judy_free_array_internal(), so a rejected payload leaves an already-populated object fully intact rather than half-freed and empty. It reuses judy_type_can_mirror(), so the accepted set cannot drift from the constructor's own rule.

Why round-trips are safe

__serialize() only emits optimizeIteration when intern->mirror_payload is set, and judy_set_optimize_iteration() only sets that for the two mirror-capable types (STRING_TO_INT_HASH, STRING_TO_INT_ADAPTIVE). A payload produced by serialize() can therefore never trip these checks — they only fire on hand-crafted, corrupt, or foreign payloads.

Fixes over the draft

Three defects in the draft were corrected before landing:

  1. A duplicated Z_TYPE_P(ztype) != IS_LONG clause (copy-paste) removed.
  2. The type bound was hardcoded > 12, but the enum ends at TYPE_STRING_TO_ENTRY = 11; now enum-driven (type < TYPE_BITSET || type > TYPE_STRING_TO_ENTRY), so type 12 is correctly rejected.
  3. The optimizeIteration-honour check was placed after judy_free_array_internal(), destroying a populated object's contents on rejection; moved before it.

Integer top-level keys are also rejected (the draft skipped them), for consistent strictness.

Tests

tests/serialize_error_001.phpt is extended in place (same --TEST-- title, so the test-floor junit identity ratchet stays intact — no test added or dropped). Added cases cover: unknown key, integer key, non-bool optimizeIteration, unhonourable optimizeIteration, the type-12/type-99 warning-free boundary, object-intact-on-rejection, and the honoured optimizeIteration roundtrip.

Verification

  • Full suite: 258/258 PASS, zero compiler warnings from extension sources (the two ld/LTO warnings are pre-existing environment noise, confirmed on the unpatched tree).
  • Direct CLI check confirms invalid-type paths throw with no E_WARNING, and a rejected payload leaves a populated object intact (count=1 k=7 opt=true).
  • Discipline gates: PASS (0 findings).

Harden __unserialize() against hand-crafted or corrupt payloads. A payload
is now rejected unless it is exactly what __serialize() can emit:

- Unknown top-level keys (including integer keys) throw
  "Invalid serialization data for Judy array".
- optimizeIteration must be a literal bool, not merely truthy.
- Out-of-range types are rejected before JTYPE, so the constructor-facing
  E_WARNING in the JTYPE macro no longer leaks out of unserialize().
- optimizeIteration=true on a type that cannot honour it throws
  "Invalid Judy type in serialized data".

The optimizeIteration/type check runs before judy_free_array_internal(), so a
rejected payload leaves an already-populated object fully intact rather than
half-freed. It reuses judy_type_can_mirror() so the accepted set cannot drift
from the constructor's own rule.

Round-trips are unaffected: __serialize() only emits optimizeIteration when
mirror_payload is set, which judy_set_optimize_iteration() only does for the
two mirror-capable types.

tests/serialize_error_001.phpt is extended in place (same test identity) with
regression cases for each rejection path, the type-12/type-99 warning-free
boundary, the intact-object-on-rejection guarantee, and the honoured
optimizeIteration roundtrip.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

discipline: passed

Base main (origin/main) (merge base 5b9c8da99f) · 27 gate(s) passed · 2277 item(s) examined · 0 warning(s) · 0 override(s)

discipline explain <gate> prints what a gate checks and how a finding is lifted. This comment is updated on every run; the check's status is the verdict.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Test Results

PHP Platform Arch TS Tests Pass Fail Skip Duration
✅ 8.1 Linux x64 - 258 258 0 0 9.2s
✅ 8.2 Linux x64 - 258 258 0 0 10.2s
✅ 8.3 Linux x64 - 258 258 0 0 8.8s
✅ 8.4 Linux x64 - 258 258 0 0 8.7s
✅ 8.5 Linux x64 - 258 258 0 0 10.6s
✅ 🧪 8.6 Linux x64 - 258 258 0 0 10.5s
✅ 8.1 Linux (Expanse) x64 - 258 258 0 0 9.2s
✅ 8.2 Linux (Expanse) x64 - 258 258 0 0 11.1s
✅ 8.3 Linux (Expanse) x64 - 258 258 0 0 9.3s
✅ 8.4 Linux (Expanse) x64 - 258 258 0 0 11.2s
✅ 8.5 Linux (Expanse) x64 - 258 258 0 0 9.2s
✅ 🧪 8.6 Linux (Expanse) x64 - 258 258 0 0 9.2s
✅ 8.1 Windows x64 nts 258 258 0 0 13.1s
✅ 8.2 Windows x64 nts 258 258 0 0 14.3s
✅ 8.3 Windows x64 nts 258 258 0 0 14.7s
✅ 8.4 Windows x64 nts 258 258 0 0 15.2s
✅ 8.5 Windows x64 nts 258 258 0 0 14.5s
✅ 🧪 8.6 Windows x64 nts 258 258 0 0 10.7s
✅ 8.1 Windows (Expanse) x64 nts 258 258 0 0 16.7s
✅ 8.2 Windows (Expanse) x64 nts 258 258 0 0 18.6s
✅ 8.3 Windows (Expanse) x64 nts 258 258 0 0 15.2s
✅ 8.4 Windows (Expanse) x64 nts 258 258 0 0 15.7s
✅ 8.5 Windows (Expanse) x64 nts 258 258 0 0 15.3s
✅ 🧪 8.6 Windows (Expanse) x64 nts 258 258 0 0 14.5s
✅ Total 6192 6192 0 0

Benchmark Results

500,000 elements, 7 iterations (median), Judy 2.7.1

Run Status

Platform PHP 8.1 PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5 PHP 8.6
Linux ✅ ✅ ✅ ✅ ✅ ✅
Windows ✅ ✅ ✅ ✅ ✅ ✅

Memory (Linux, PHP 8.6)

Peak RSS of a child process building one structure, n = 500,000, minus an empty-process floor. Judy's memory advantage is type- and scale-dependent — see BENCHMARK.md for the curve across sizes on a dedicated host.

Type Judy PHP array Judy vs PHP array
Bitset < 296.0 KB 8.0 MB > 28x smaller
Int to Int 4.3 MB 8.0 MB 1.8x smaller
Int to Mixed 40.2 MB 38.1 MB 1.05x larger
Int to Packed 16.2 MB 38.0 MB 2.3x smaller
String to Int 10.2 MB 38.0 MB 3.7x smaller
String to Mixed 46.2 MB 67.9 MB 1.5x smaller
String to Int (Hash) 31.4 MB 38.0 MB 1.2x smaller
String to Mixed (Hash) 67.4 MB 68.0 MB about the same
String to Int (Adaptive) 31.4 MB 38.1 MB 1.2x smaller
String to Mixed (Adaptive) 67.4 MB 67.9 MB about the same

< marks a structure smaller than this instrument can resolve at n = 500,000: run-to-run movement in the empty-process floor puts the resolution at 296.0 KB. The bound is real, the exact figure is not available at this size — BENCHMARK.md measures the same types up to n = 8M, where they are well clear of the floor.

API Speedups (Linux, PHP 8.6)

Speedup = PHP time / Judy time. Bold = Judy faster.

Operation Judy PHP Speedup
putAll() 17.5 ms 24.6 ms 1.4x
fromArray() 17.5 ms 24.6 ms 1.4x
toArray() 20.9 ms 30.9 ms 1.5x
getAll() 1.1 ms 1.9 ms 1.7x
keys() 9.9 ms 19.5 ms 2.0x
values() 10.1 ms 19.5 ms 1.9x
sumValues() 6.0 ms 16.3 ms 2.7x
populationCount() 0.00 ms 17.2 ms O(1) vs O(n)
deleteRange() 32.5 ms 47.6 ms 1.5x
equals() 10.0 ms 42.4 ms 4.2x
union() Bitset 26.5 ms 4.6 ms 0.2x
intersect() Bitset 12.8 ms 5.1 ms 0.4x
diff() Bitset 12.6 ms 3.1 ms 0.2x
xor() Bitset 25.3 ms 10.5 ms 0.4x
mergeWith() Int→Int 42.0 ms 54.0 ms 1.3x
mergeWith() Str→Int 200.5 ms 215.3 ms 1.1x
forEach() Int→Int 25.9 ms 18.4 ms 0.7x
filter() Int→Int 36.9 ms 30.6 ms 0.8x
map() Int→Int 39.7 ms 37.0 ms 0.9x
forEach() Str→Int 62.0 ms 56.3 ms 0.9x
filter() Str→Int 116.1 ms 108.9 ms 0.9x
map() Str→Int 158.5 ms 154.2 ms ~1.0x

O(1) vs O(n) marks a row whose ratio is decided by the complexity classes rather than measured: populationCount() reads libJudy's cached population, the PHP arm counts every element. It scales with n and says nothing about this change. judy-bench.php declines the same comparison for size($lo, $hi) for the same reason.

Core Types (Linux, PHP 8.6)

Speedup = PHP time / Judy time. Bold = Judy faster.

Type Op Judy PHP array Speedup
Bitset Write 14.9 ms 5.8 ms 0.4x
Bitset Read 9.7 ms 3.1 ms 0.3x
Bitset Iter 15.9 ms 2.4 ms 0.2x
Bitset Free 0.00 ms 0.32 ms 74.4x
Int to Int Write 20.1 ms 6.0 ms 0.3x
Int to Int Read 10.6 ms 3.1 ms 0.3x
Int to Int Iter 17.0 ms 2.4 ms 0.1x
Int to Int Free 0.19 ms 0.36 ms 1.9x
Int to Mixed Write 28.5 ms 8.7 ms 0.3x
Int to Mixed Read 11.3 ms 4.5 ms 0.4x
Int to Mixed Iter 17.0 ms 2.7 ms 0.2x
Int to Mixed Free 12.0 ms 3.3 ms 0.3x
String to Int Write 87.1 ms 15.8 ms 0.2x
String to Int Read 35.7 ms 5.8 ms 0.2x
String to Int Iter 55.7 ms 3.7 ms 0.1x
String to Int Free 22.5 ms 1.4 ms 0.1x
String to Int (Adaptive) Write 143.5 ms 15.5 ms 0.1x
String to Int (Adaptive) Read 19.5 ms 5.8 ms 0.3x
String to Int (Adaptive) Iter 67.2 ms 3.7 ms 0.1x
String to Int (Adaptive) Free 36.0 ms 1.3 ms 0.0x
All types detail

Core Types (Linux, PHP 8.6) — All

Speedup = PHP time / Judy time. Bold = Judy faster.

Type Op Judy PHP array Speedup
Bitset Write 14.9 ms 5.8 ms 0.4x
Bitset Read 9.7 ms 3.1 ms 0.3x
Bitset Iter 15.9 ms 2.4 ms 0.2x
Bitset Free 0.00 ms 0.32 ms 74.4x
Int to Int Write 20.1 ms 6.0 ms 0.3x
Int to Int Read 10.6 ms 3.1 ms 0.3x
Int to Int Iter 17.0 ms 2.4 ms 0.1x
Int to Int Free 0.19 ms 0.36 ms 1.9x
Int to Mixed Write 28.5 ms 8.7 ms 0.3x
Int to Mixed Read 11.3 ms 4.5 ms 0.4x
Int to Mixed Iter 17.0 ms 2.7 ms 0.2x
Int to Mixed Free 12.0 ms 3.3 ms 0.3x
Int to Packed Write 37.6 ms 8.6 ms 0.2x
Int to Packed Read 26.9 ms 4.5 ms 0.2x
Int to Packed Iter 34.4 ms 2.7 ms 0.1x
Int to Packed Free 7.2 ms 3.4 ms 0.5x
String to Int Write 87.1 ms 15.8 ms 0.2x
String to Int Read 35.7 ms 5.8 ms 0.2x
String to Int Iter 55.7 ms 3.7 ms 0.1x
String to Int Free 22.5 ms 1.4 ms 0.1x
String to Mixed Write 105.4 ms 16.5 ms 0.2x
String to Mixed Read 37.6 ms 6.6 ms 0.2x
String to Mixed Iter 56.8 ms 3.8 ms 0.1x
String to Mixed Free 67.1 ms 4.6 ms 0.1x
String to Int (Hash) Write 143.8 ms 15.4 ms 0.1x
String to Int (Hash) Read 20.0 ms 5.8 ms 0.3x
String to Int (Hash) Iter 68.1 ms 3.7 ms 0.1x
String to Int (Hash) Free 36.0 ms 1.3 ms 0.0x
String to Mixed (Hash) Write 203.0 ms 16.3 ms 0.1x
String to Mixed (Hash) Read 21.4 ms 6.6 ms 0.3x
String to Mixed (Hash) Iter 82.0 ms 3.8 ms 0.0x
String to Mixed (Hash) Free 113.5 ms 4.4 ms 0.0x
String to Int (Adaptive) Write 143.5 ms 15.5 ms 0.1x
String to Int (Adaptive) Read 19.5 ms 5.8 ms 0.3x
String to Int (Adaptive) Iter 67.2 ms 3.7 ms 0.1x
String to Int (Adaptive) Free 36.0 ms 1.3 ms 0.0x
String to Mixed (Adaptive) Write 220.0 ms 16.4 ms 0.1x
String to Mixed (Adaptive) Read 20.1 ms 6.5 ms 0.3x
String to Mixed (Adaptive) Iter 79.5 ms 3.8 ms 0.0x
String to Mixed (Adaptive) Free 114.4 ms 4.3 ms 0.0x
Cross-version detail (Linux)

Core Types — Linux

Each cell: Judy ms (speedup vs PHP array).

Type Op PHP 8.1 PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5 PHP 8.6
Bitset Write 14.2 ms (0.8x) 19.6 ms (0.3x) 14.7 ms (0.4x) 14.5 ms (0.5x) 17.5 ms (0.4x) 14.9 ms (0.4x)
Bitset Read 9.9 ms (0.4x) 12.3 ms (0.4x) 9.8 ms (0.3x) 10.3 ms (0.3x) 11.1 ms (0.3x) 9.7 ms (0.3x)
Int to Int Write 21.9 ms (0.6x) 24.4 ms (0.3x) 21.0 ms (0.3x) 20.7 ms (0.3x) 24.5 ms (0.3x) 20.1 ms (0.3x)
Int to Int Read 10.6 ms (0.4x) 11.0 ms (0.4x) 10.3 ms (0.3x) 11.3 ms (0.3x) 11.9 ms (0.4x) 10.6 ms (0.3x)
Int to Mixed Write 34.8 ms (0.5x) 34.9 ms (0.3x) 30.5 ms (0.3x) 30.0 ms (0.3x) 36.1 ms (0.3x) 28.5 ms (0.3x)
Int to Mixed Read 13.0 ms (0.5x) 13.9 ms (0.4x) 11.8 ms (0.4x) 11.9 ms (0.4x) 13.8 ms (0.4x) 11.3 ms (0.4x)
Int to Packed Write 45.7 ms (0.4x) 45.6 ms (0.2x) 42.0 ms (0.2x) 40.5 ms (0.2x) 50.2 ms (0.2x) 37.6 ms (0.2x)
Int to Packed Read 28.5 ms (0.2x) 31.6 ms (0.2x) 27.2 ms (0.2x) 27.9 ms (0.2x) 29.7 ms (0.2x) 26.9 ms (0.2x)
String to Int Write 90.2 ms (0.3x) 92.6 ms (0.2x) 88.5 ms (0.2x) 87.7 ms (0.2x) 91.4 ms (0.2x) 87.1 ms (0.2x)
String to Int Read 38.2 ms (0.2x) 40.0 ms (0.2x) 36.9 ms (0.2x) 36.2 ms (0.2x) 37.0 ms (0.2x) 35.7 ms (0.2x)
String to Mixed Write 109.6 ms (0.2x) 117.4 ms (0.2x) 117.3 ms (0.2x) 118.4 ms (0.2x) 117.5 ms (0.2x) 105.4 ms (0.2x)
String to Mixed Read 39.6 ms (0.2x) 43.5 ms (0.2x) 38.7 ms (0.2x) 39.3 ms (0.2x) 40.8 ms (0.2x) 37.6 ms (0.2x)
String to Int (Hash) Write 167.5 ms (0.1x) 159.9 ms (0.1x) 160.8 ms (0.1x) 156.1 ms (0.1x) 171.4 ms (0.1x) 143.8 ms (0.1x)
String to Int (Hash) Read 24.5 ms (0.3x) 27.3 ms (0.3x) 22.0 ms (0.3x) 22.4 ms (0.3x) 25.5 ms (0.3x) 20.0 ms (0.3x)
String to Mixed (Hash) Write 249.0 ms (0.1x) 237.5 ms (0.1x) 248.1 ms (0.1x) 242.8 ms (0.1x) 245.8 ms (0.1x) 203.0 ms (0.1x)
String to Mixed (Hash) Read 27.0 ms (0.3x) 30.8 ms (0.3x) 23.4 ms (0.3x) 24.1 ms (0.3x) 28.1 ms (0.3x) 21.4 ms (0.3x)
String to Int (Adaptive) Write 166.7 ms (0.1x) 158.1 ms (0.1x) 162.0 ms (0.1x) 153.1 ms (0.1x) 173.5 ms (0.1x) 143.5 ms (0.1x)
String to Int (Adaptive) Read 22.4 ms (0.4x) 27.0 ms (0.3x) 21.3 ms (0.3x) 21.2 ms (0.3x) 24.9 ms (0.3x) 19.5 ms (0.3x)
String to Mixed (Adaptive) Write 275.2 ms (0.1x) 237.2 ms (0.1x) 274.6 ms (0.1x) 264.4 ms (0.1x) 245.8 ms (0.1x) 220.0 ms (0.1x)
String to Mixed (Adaptive) Read 23.8 ms (0.4x) 29.5 ms (0.4x) 22.2 ms (0.3x) 22.7 ms (0.3x) 27.3 ms (0.4x) 20.1 ms (0.3x)

API — Linux

Operation PHP 8.1 PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5 PHP 8.6
putAll() 18.5 ms (1.5x) 21.3 ms (1.4x) 18.7 ms (1.4x) 17.9 ms (1.4x) 19.8 ms (1.4x) 17.5 ms (1.4x)
fromArray() 17.6 ms (1.5x) 21.5 ms (1.4x) 18.7 ms (1.4x) 17.8 ms (1.4x) 19.9 ms (1.4x) 17.5 ms (1.4x)
toArray() 30.2 ms (1.3x) 22.0 ms (1.6x) 23.9 ms (1.5x) 22.5 ms (1.5x) 24.2 ms (1.5x) 20.9 ms (1.5x)
getAll() 1.2 ms (1.8x) 1.1 ms (2.0x) 1.1 ms (1.8x) 1.1 ms (1.6x) 1.1 ms (1.8x) 1.1 ms (1.7x)
keys() 15.4 ms (1.7x) 12.4 ms (2.1x) 10.9 ms (2.0x) 10.5 ms (2.0x) 12.0 ms (2.2x) 9.9 ms (2.0x)
values() 16.5 ms (1.6x) 12.9 ms (2.0x) 12.1 ms (1.9x) 10.9 ms (1.9x) 13.7 ms (1.9x) 10.1 ms (1.9x)
sumValues() 7.3 ms (2.1x) 9.1 ms (2.5x) 7.1 ms (2.6x) 6.9 ms (2.4x) 8.6 ms (2.6x) 6.0 ms (2.7x)
populationCount() 0.00 ms (82272x) 0.00 ms (254327x) 0.00 ms (188175x) 0.00 ms (172919x) 0.00 ms (235353x) 0.00 ms (172297x)
deleteRange() 34.7 ms (1.4x) 39.4 ms (1.5x) 34.1 ms (1.5x) 32.9 ms (1.5x) 38.9 ms (1.5x) 32.5 ms (1.5x)
equals() 9.8 ms (3.9x) 11.1 ms (4.3x) 10.4 ms (4.3x) 10.0 ms (4.2x) 12.5 ms (4.0x) 10.0 ms (4.2x)
union() Bitset 25.5 ms (0.6x) 31.8 ms (0.1x) 24.7 ms (0.2x) 24.8 ms (0.2x) 32.1 ms (0.2x) 26.5 ms (0.2x)
intersect() Bitset 12.2 ms (0.6x) 13.8 ms (0.4x) 12.4 ms (0.5x) 12.2 ms (0.5x) 15.0 ms (0.4x) 12.8 ms (0.4x)
diff() Bitset 11.9 ms (0.5x) 13.3 ms (0.2x) 12.0 ms (0.3x) 12.0 ms (0.3x) 14.9 ms (0.2x) 12.6 ms (0.2x)
xor() Bitset 24.1 ms (1.2x) 27.5 ms (0.4x) 24.2 ms (0.6x) 24.1 ms (0.5x) 29.2 ms (0.4x) 25.3 ms (0.4x)
mergeWith() Int→Int 44.4 ms (1.3x) 52.1 ms (1.3x) 44.8 ms (1.3x) 44.6 ms (1.3x) 51.9 ms (1.3x) 42.0 ms (1.3x)
mergeWith() Str→Int 235.5 ms (1.1x) 228.6 ms (1.1x) 216.3 ms (1.1x) 210.1 ms (1.1x) 237.2 ms (1.1x) 200.5 ms (1.1x)
forEach() Int→Int 24.7 ms (0.7x) 26.8 ms (0.9x) 30.3 ms (0.7x) 27.3 ms (0.7x) 28.4 ms (0.9x) 25.9 ms (0.7x)
filter() Int→Int 39.1 ms (0.8x) 41.8 ms (0.9x) 40.5 ms (0.8x) 39.5 ms (0.8x) 50.4 ms (0.9x) 36.9 ms (0.8x)
map() Int→Int 44.4 ms (0.9x) 47.2 ms (~1.0x) 43.5 ms (0.9x) 43.0 ms (0.9x) 52.9 ms (~1.0x) 39.7 ms (0.9x)
forEach() Str→Int 60.4 ms (0.9x) 64.3 ms (~1.0x) 62.1 ms (0.9x) 61.5 ms (0.9x) 65.5 ms (0.9x) 62.0 ms (0.9x)
filter() Str→Int 119.2 ms (0.9x) 116.0 ms (~1.0x) 120.2 ms (0.9x) 120.0 ms (0.9x) 122.2 ms (~1.0x) 116.1 ms (0.9x)
map() Str→Int 158.5 ms (~1.0x) 155.7 ms (~1.0x) 159.6 ms (~1.0x) 159.4 ms (~1.0x) 160.8 ms (~1.0x) 158.5 ms (~1.0x)
Cross-version detail (Windows)

Core Types — Windows

Each cell: Judy ms (speedup vs PHP array).

Type Op PHP 8.1 PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5 PHP 8.6
Bitset Write 11.8 ms (0.8x) 19.3 ms (0.5x) 22.5 ms (0.6x) 25.1 ms (0.5x) 12.9 ms (0.7x) 12.5 ms (0.8x)
Bitset Read 8.5 ms (0.5x) 13.5 ms (0.5x) 17.5 ms (0.5x) 21.5 ms (0.4x) 9.6 ms (0.6x) 9.5 ms (0.6x)
Int to Int Write 19.7 ms (0.5x) 26.8 ms (0.4x) 34.4 ms (0.3x) 36.7 ms (0.3x) 22.1 ms (0.4x) 21.8 ms (0.4x)
Int to Int Read 9.3 ms (0.5x) 14.2 ms (0.5x) 19.5 ms (0.5x) 18.6 ms (0.4x) 11.0 ms (0.5x) 10.1 ms (0.6x)
Int to Mixed Write 27.7 ms (0.5x) 35.3 ms (0.4x) 46.4 ms (0.4x) 53.0 ms (0.3x) 34.2 ms (0.4x) 34.8 ms (0.4x)
Int to Mixed Read 10.9 ms (0.5x) 16.2 ms (0.5x) 20.7 ms (0.5x) 21.3 ms (0.5x) 11.4 ms (0.6x) 11.2 ms (0.6x)
Int to Packed Write 37.4 ms (0.4x) 48.8 ms (0.3x) 61.9 ms (0.3x) 71.1 ms (0.2x) 45.9 ms (0.3x) 44.1 ms (0.3x)
Int to Packed Read 28.4 ms (0.2x) 38.7 ms (0.2x) 48.0 ms (0.2x) 50.2 ms (0.2x) 31.8 ms (0.2x) 30.9 ms (0.2x)
String to Int Write 96.7 ms (0.2x) 113.5 ms (0.2x) 143.9 ms (0.2x) 147.0 ms (0.2x) 101.3 ms (0.2x) 102.2 ms (0.2x)
String to Int Read 46.5 ms (0.1x) 53.4 ms (0.2x) 59.3 ms (0.2x) 63.2 ms (0.2x) 49.3 ms (0.2x) 51.4 ms (0.2x)
String to Mixed Write 128.5 ms (0.2x) 152.5 ms (0.2x) 182.0 ms (0.2x) 193.5 ms (0.2x) 146.9 ms (0.2x) 140.8 ms (0.2x)
String to Mixed Read 47.2 ms (0.2x) 56.0 ms (0.2x) 59.5 ms (0.2x) 65.3 ms (0.2x) 51.3 ms (0.2x) 49.1 ms (0.2x)
String to Int (Hash) Write 177.4 ms (0.1x) 209.8 ms (0.1x) 241.2 ms (0.1x) 251.9 ms (0.1x) 186.9 ms (0.1x) 187.7 ms (0.1x)
String to Int (Hash) Read 32.9 ms (0.2x) 39.6 ms (0.3x) 42.2 ms (0.3x) 45.2 ms (0.3x) 38.0 ms (0.2x) 36.9 ms (0.2x)
String to Mixed (Hash) Write 265.6 ms (0.1x) 306.3 ms (0.1x) 339.5 ms (0.1x) 372.1 ms (0.1x) 307.9 ms (0.1x) 289.3 ms (0.1x)
String to Mixed (Hash) Read 36.3 ms (0.2x) 43.1 ms (0.3x) 46.0 ms (0.3x) 47.9 ms (0.3x) 43.7 ms (0.3x) 40.1 ms (0.2x)
String to Int (Adaptive) Write 171.9 ms (0.1x) 201.7 ms (0.1x) 238.7 ms (0.1x) 250.0 ms (0.1x) 185.5 ms (0.1x) 180.2 ms (0.1x)
String to Int (Adaptive) Read 34.3 ms (0.2x) 39.5 ms (0.3x) 41.7 ms (0.3x) 44.9 ms (0.3x) 38.4 ms (0.2x) 34.6 ms (0.2x)
String to Mixed (Adaptive) Write 287.2 ms (0.1x) 326.9 ms (0.1x) 352.8 ms (0.1x) 384.4 ms (0.1x) 353.6 ms (0.1x) 290.0 ms (0.1x)
String to Mixed (Adaptive) Read 36.7 ms (0.2x) 43.7 ms (0.3x) 46.0 ms (0.3x) 47.1 ms (0.3x) 44.9 ms (0.3x) 38.4 ms (0.2x)

API — Windows

Operation PHP 8.1 PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5 PHP 8.6
putAll() 19.1 ms (1.4x) 25.0 ms (1.4x) 29.7 ms (1.5x) 30.9 ms (1.6x) 21.1 ms (1.4x) 21.1 ms (1.4x)
fromArray() 18.8 ms (1.5x) 23.9 ms (1.4x) 28.8 ms (1.6x) 31.3 ms (1.6x) 21.3 ms (1.4x) 20.6 ms (1.4x)
toArray() 21.0 ms (1.6x) 24.0 ms (1.6x) 27.9 ms (1.8x) 28.5 ms (1.8x) 19.9 ms (1.7x) 19.1 ms (1.7x)
getAll() 0.86 ms (2.0x) 1.1 ms (2.2x) 1.4 ms (2.4x) 1.8 ms (1.9x) 1.2 ms (1.6x) 1.1 ms (1.8x)
keys() 10.5 ms (2.1x) 10.3 ms (2.4x) 12.7 ms (2.6x) 13.4 ms (2.4x) 8.3 ms (2.5x) 7.9 ms (2.3x)
values() 10.7 ms (2.0x) 10.7 ms (2.3x) 13.1 ms (2.5x) 13.6 ms (2.4x) 8.7 ms (2.1x) 8.0 ms (2.3x)
sumValues() 3.6 ms (3.2x) 6.0 ms (3.3x) 7.2 ms (3.5x) 6.8 ms (3.6x) 4.0 ms (3.5x) 3.9 ms (3.4x)
populationCount() 0.00 ms (123695x) 0.00 ms (200612x) 0.00 ms (269706x) 0.00 ms (130754x) 0.00 ms (146628x) 0.00 ms (139841x)
deleteRange() 29.6 ms (1.4x) 41.3 ms (1.5x) 54.3 ms (1.5x) 56.5 ms (1.5x) 32.8 ms (1.5x) 33.4 ms (1.5x)
equals() 6.1 ms (5.5x) 9.0 ms (5.3x) 11.2 ms (5.9x) 10.9 ms (6.3x) 6.7 ms (4.7x) 6.1 ms (5.0x)
union() Bitset 17.9 ms (0.5x) 29.4 ms (0.3x) 36.0 ms (0.3x) 37.1 ms (0.2x) 19.3 ms (0.3x) 20.0 ms (0.3x)
intersect() Bitset 7.7 ms (0.7x) 12.5 ms (0.5x) 16.4 ms (0.5x) 20.1 ms (0.4x) 8.3 ms (0.7x) 8.6 ms (0.6x)
diff() Bitset 7.8 ms (0.5x) 12.1 ms (0.3x) 16.2 ms (0.3x) 17.3 ms (0.3x) 8.3 ms (0.3x) 8.6 ms (0.3x)
xor() Bitset 16.3 ms (1.2x) 24.6 ms (0.6x) 32.4 ms (0.5x) 34.4 ms (0.6x) 16.8 ms (0.7x) 17.3 ms (0.7x)
mergeWith() Int→Int 34.8 ms (1.3x) 49.2 ms (1.4x) 61.9 ms (1.4x) 66.5 ms (1.4x) 36.9 ms (1.3x) 38.3 ms (1.2x)
mergeWith() Str→Int 215.6 ms (1.1x) 262.8 ms (1.1x) 308.3 ms (1.1x) 346.5 ms (1.1x) 244.3 ms (1.1x) 229.8 ms (1.1x)
forEach() Int→Int 14.8 ms (~1.0x) 22.3 ms (~1.0x) 29.7 ms (~1.0x) 31.3 ms (~1.0x) 16.7 ms (0.9x) 17.1 ms (0.9x)
filter() Int→Int 24.1 ms (~1.0x) 37.0 ms (~1.0x) 46.0 ms (1.1x) 49.7 ms (~1.0x) 30.8 ms (0.9x) 24.7 ms (1.1x)
map() Int→Int 27.1 ms (1.1x) 39.8 ms (1.1x) 56.2 ms (1.1x) 56.9 ms (~1.0x) 30.6 ms (1.1x) 29.3 ms (1.1x)
forEach() Str→Int 52.0 ms (~1.0x) 66.1 ms (~1.0x) 76.7 ms (~1.0x) 84.8 ms (~1.0x) 55.3 ms (~1.0x) 54.7 ms (~1.0x)
filter() Str→Int 100.7 ms (~1.0x) 125.1 ms (~1.0x) 152.7 ms (~1.0x) 162.6 ms (~1.0x) 105.7 ms (1.1x) 99.0 ms (~1.0x)
map() Str→Int 139.6 ms (~1.0x) 172.1 ms (~1.0x) 208.6 ms (~1.0x) 222.5 ms (~1.0x) 145.8 ms (~1.0x) 142.0 ms (~1.0x)
Windows results (PHP 8.6)

Memory (Windows, PHP 8.6)

Peak RSS of a child process building one structure, n = 500,000, minus an empty-process floor. Judy's memory advantage is type- and scale-dependent — see BENCHMARK.md for the curve across sizes on a dedicated host.

Type Judy PHP array Judy vs PHP array
Bitset < 136.0 KB 9.2 MB > 69x smaller
Int to Int 4.5 MB 9.2 MB 2.1x smaller
Int to Mixed 42.6 MB 38.2 MB 1.11x larger
Int to Packed 17.2 MB 38.2 MB 2.2x smaller
String to Int 10.5 MB 38.0 MB 3.6x smaller
String to Mixed 48.7 MB 68.9 MB 1.4x smaller
String to Int (Hash) 32.2 MB 37.9 MB 1.2x smaller
String to Mixed (Hash) 70.4 MB 68.9 MB about the same
String to Int (Adaptive) 32.3 MB 38.0 MB 1.2x smaller
String to Mixed (Adaptive) 70.4 MB 69.0 MB about the same

< marks a structure smaller than this instrument can resolve at n = 500,000: run-to-run movement in the empty-process floor puts the resolution at 136.0 KB. The bound is real, the exact figure is not available at this size — BENCHMARK.md measures the same types up to n = 8M, where they are well clear of the floor.

API Speedups (Windows, PHP 8.6)

Speedup = PHP time / Judy time. Bold = Judy faster.

Operation Judy PHP Speedup
putAll() 21.1 ms 29.0 ms 1.4x
fromArray() 20.6 ms 29.0 ms 1.4x
toArray() 19.1 ms 33.4 ms 1.7x
getAll() 1.1 ms 1.9 ms 1.8x
keys() 7.9 ms 18.4 ms 2.3x
values() 8.0 ms 18.6 ms 2.3x
sumValues() 3.9 ms 13.3 ms 3.4x
populationCount() 0.00 ms 14.0 ms O(1) vs O(n)
deleteRange() 33.4 ms 48.9 ms 1.5x
equals() 6.1 ms 30.3 ms 5.0x
union() Bitset 20.0 ms 5.7 ms 0.3x
intersect() Bitset 8.6 ms 5.5 ms 0.6x
diff() Bitset 8.6 ms 2.7 ms 0.3x
xor() Bitset 17.3 ms 12.7 ms 0.7x
mergeWith() Int→Int 38.3 ms 46.8 ms 1.2x
mergeWith() Str→Int 229.8 ms 246.8 ms 1.1x
forEach() Int→Int 17.1 ms 15.1 ms 0.9x
filter() Int→Int 24.7 ms 26.5 ms 1.1x
map() Int→Int 29.3 ms 31.8 ms 1.1x
forEach() Str→Int 54.7 ms 53.7 ms ~1.0x
filter() Str→Int 99.0 ms 100.5 ms ~1.0x
map() Str→Int 142.0 ms 148.3 ms ~1.0x

O(1) vs O(n) marks a row whose ratio is decided by the complexity classes rather than measured: populationCount() reads libJudy's cached population, the PHP arm counts every element. It scales with n and says nothing about this change. judy-bench.php declines the same comparison for size($lo, $hi) for the same reason.

Core Types (Windows, PHP 8.6)

Speedup = PHP time / Judy time. Bold = Judy faster.

Type Op Judy PHP array Speedup
Bitset Write 12.5 ms 9.5 ms 0.8x
Bitset Read 9.5 ms 5.7 ms 0.6x
Bitset Iter 12.2 ms 4.5 ms 0.4x
Bitset Free 0.01 ms 0.44 ms 81.9x
Int to Int Write 21.8 ms 9.3 ms 0.4x
Int to Int Read 10.1 ms 5.9 ms 0.6x
Int to Int Iter 12.6 ms 4.6 ms 0.4x
Int to Int Free 0.95 ms 0.45 ms 0.5x
Int to Mixed Write 34.8 ms 12.3 ms 0.4x
Int to Mixed Read 11.2 ms 6.6 ms 0.6x
Int to Mixed Iter 14.2 ms 5.4 ms 0.4x
Int to Mixed Free 15.5 ms 5.1 ms 0.3x
String to Int Write 102.2 ms 20.3 ms 0.2x
String to Int Read 51.4 ms 9.0 ms 0.2x
String to Int Iter 59.0 ms 4.9 ms 0.1x
String to Int Free 25.5 ms 1.3 ms 0.1x
String to Int (Adaptive) Write 180.2 ms 18.9 ms 0.1x
String to Int (Adaptive) Read 34.6 ms 7.6 ms 0.2x
String to Int (Adaptive) Iter 80.6 ms 4.8 ms 0.1x
String to Int (Adaptive) Free 50.6 ms 1.4 ms 0.0x
All types detail

Core Types (Windows, PHP 8.6) — All

Speedup = PHP time / Judy time. Bold = Judy faster.

Type Op Judy PHP array Speedup
Bitset Write 12.5 ms 9.5 ms 0.8x
Bitset Read 9.5 ms 5.7 ms 0.6x
Bitset Iter 12.2 ms 4.5 ms 0.4x
Bitset Free 0.01 ms 0.44 ms 81.9x
Int to Int Write 21.8 ms 9.3 ms 0.4x
Int to Int Read 10.1 ms 5.9 ms 0.6x
Int to Int Iter 12.6 ms 4.6 ms 0.4x
Int to Int Free 0.95 ms 0.45 ms 0.5x
Int to Mixed Write 34.8 ms 12.3 ms 0.4x
Int to Mixed Read 11.2 ms 6.6 ms 0.6x
Int to Mixed Iter 14.2 ms 5.4 ms 0.4x
Int to Mixed Free 15.5 ms 5.1 ms 0.3x
Int to Packed Write 44.1 ms 12.0 ms 0.3x
Int to Packed Read 30.9 ms 6.6 ms 0.2x
Int to Packed Iter 33.0 ms 5.3 ms 0.2x
Int to Packed Free 10.1 ms 5.0 ms 0.5x
String to Int Write 102.2 ms 20.3 ms 0.2x
String to Int Read 51.4 ms 9.0 ms 0.2x
String to Int Iter 59.0 ms 4.9 ms 0.1x
String to Int Free 25.5 ms 1.3 ms 0.1x
String to Mixed Write 140.8 ms 23.9 ms 0.2x
String to Mixed Read 49.1 ms 9.9 ms 0.2x
String to Mixed Iter 69.5 ms 6.7 ms 0.1x
String to Mixed Free 81.9 ms 8.4 ms 0.1x
String to Int (Hash) Write 187.7 ms 19.2 ms 0.1x
String to Int (Hash) Read 36.9 ms 8.0 ms 0.2x
String to Int (Hash) Iter 90.0 ms 4.8 ms 0.1x
String to Int (Hash) Free 56.3 ms 1.3 ms 0.0x
String to Mixed (Hash) Write 289.3 ms 23.5 ms 0.1x
String to Mixed (Hash) Read 40.1 ms 9.7 ms 0.2x
String to Mixed (Hash) Iter 120.1 ms 6.7 ms 0.1x
String to Mixed (Hash) Free 164.0 ms 8.5 ms 0.1x
String to Int (Adaptive) Write 180.2 ms 18.9 ms 0.1x
String to Int (Adaptive) Read 34.6 ms 7.6 ms 0.2x
String to Int (Adaptive) Iter 80.6 ms 4.8 ms 0.1x
String to Int (Adaptive) Free 50.6 ms 1.4 ms 0.0x
String to Mixed (Adaptive) Write 290.0 ms 22.9 ms 0.1x
String to Mixed (Adaptive) Read 38.4 ms 9.3 ms 0.2x
String to Mixed (Adaptive) Iter 108.1 ms 6.7 ms 0.1x
String to Mixed (Adaptive) Free 166.2 ms 8.0 ms 0.0x

Release Comparison (2.6.0 → 2.7.1)

Interleaved A/B comparison against v2.6.0 (pie install) on the same CI runner: 5 benchmark groups × 5 rounds × 2 arms, 300,000 elements, median of 3 per child. Each row is the median of the paired current/baseline ratios with a 95% percentile-bootstrap CI; it is flagged only when the whole drift-adjusted CI clears ±10%. Advisory — benchmarks never gate a merge.

Run-wide median delta +0.70% (PHP-array control +0.27%); runner common-mode +0.27% divided out of every row.

Summary: 0 regressions, 100 unchanged

Benchmark Baseline Current Delta 95% CI Status
adv.filter.int_to_int 23.6 ms 24.0 ms +1.8% [+0.1%, +3.3%] ~same
adv.filter.string_to_int 67.9 ms 69.4 ms +2.0% [+1.9%, +2.4%] ~same
adv.forEach.int_to_int 15.9 ms 16.1 ms -0.1% [-3.1%, +3.7%] ~same
adv.forEach.string_to_int 35.8 ms 36.1 ms -0.4% [-0.7%, +1.5%] ~same
adv.map.int_to_int 24.7 ms 25.4 ms +1.8% [-0.0%, +4.4%] ~same
adv.map.string_to_int 90.1 ms 93.0 ms +2.9% [-0.5%, +3.1%] ~same
adv.optiter.foreach.string_to_int_adaptive.default 70.4 ms 72.2 ms +2.3% [-1.5%, +7.2%] ~same
adv.optiter.foreach.string_to_int_adaptive.optimized 36.5 ms 36.6 ms -0.6% [-0.8%, +2.3%] ~same
adv.optiter.foreach.string_to_int_hash.default 52.3 ms 53.2 ms +1.5% [-1.4%, +4.4%] ~same
adv.optiter.foreach.string_to_int_hash.optimized 31.9 ms 31.3 ms -2.5% [-6.0%, -1.4%] ~same
adv.optiter.increment.string_to_int_hash.default 35.8 ms 33.2 ms +0.8% [-11.6%, +10.2%] ~same
adv.optiter.increment.string_to_int_hash.optimized 39.8 ms 40.3 ms +0.8% [-7.5%, +6.0%] ~same
adv.optiter.overwrite.string_to_int_adaptive.default 52.7 ms 52.5 ms -1.2% [-2.2%, +4.5%] ~same
adv.optiter.overwrite.string_to_int_adaptive.optimized 62.0 ms 59.2 ms -5.8% [-8.3%, -2.2%] ~same
adv.optiter.overwrite.string_to_int_hash.default 35.3 ms 34.4 ms +1.6% [-10.9%, +6.8%] ~same
adv.optiter.overwrite.string_to_int_hash.optimized 40.3 ms 40.2 ms -0.2% [-1.5%, +3.4%] ~same
adv.optiter.ratio.foreach.string_to_int_adaptive 51.2 ms 50.6 ms -2.2% [-7.5%, +0.5%] ~same
adv.optiter.ratio.foreach.string_to_int_hash 60.9 ms 58.9 ms -4.8% [-6.9%, -2.0%] ~same
adv.optiter.ratio.increment.string_to_int_hash 116.3 ms 115.9 ms +1.6% [-8.7%, +4.9%] ~same
adv.optiter.ratio.overwrite.string_to_int_adaptive 116.8 ms 111.7 ms -6.6% [-8.9%, -1.4%] ~same
adv.optiter.ratio.overwrite.string_to_int_hash 115.6 ms 114.4 ms -3.3% [-7.2%, +11.7%] ~same
adv.optiter.ratio.toArray.string_to_int_adaptive 54.7 ms 56.3 ms +2.9% [-1.5%, +5.8%] ~same
adv.optiter.ratio.toArray.string_to_int_hash 55.4 ms 56.2 ms +2.7% [-4.8%, +11.0%] ~same
adv.optiter.ratio.values.string_to_int_adaptive 43.2 ms 42.4 ms -0.8% [-3.2%, +1.1%] ~same
adv.optiter.ratio.values.string_to_int_hash 49.8 ms 47.0 ms -4.1% [-9.0%, +1.4%] ~same
adv.optiter.toArray.string_to_int_adaptive.default 83.9 ms 85.9 ms -0.1% [-3.7%, +4.6%] ~same
adv.optiter.toArray.string_to_int_adaptive.optimized 46.7 ms 48.3 ms +3.3% [-0.7%, +6.0%] ~same
adv.optiter.toArray.string_to_int_hash.default 61.6 ms 63.5 ms +0.3% [-4.0%, +7.4%] ~same
adv.optiter.toArray.string_to_int_hash.optimized 34.2 ms 35.7 ms +4.0% [-0.8%, +6.9%] ~same
adv.optiter.values.string_to_int_adaptive.default 69.2 ms 70.2 ms +1.6% [-5.0%, +3.9%] ~same
adv.optiter.values.string_to_int_adaptive.optimized 29.6 ms 29.7 ms +1.1% [-4.4%, +1.6%] ~same
adv.optiter.values.string_to_int_hash.default 49.3 ms 52.6 ms +0.5% [-2.7%, +10.2%] ~same
adv.optiter.values.string_to_int_hash.optimized 24.7 ms 24.7 ms -1.1% [-3.3%, +2.0%] ~same
api.deleteRange.int_to_int 19.4 ms 19.6 ms +0.5% [-0.6%, +2.7%] ~same
api.equals.int_to_int 6.0 ms 6.0 ms -1.1% [-2.8%, +0.8%] ~same
api.fromArray.int_to_int 11.8 ms 11.6 ms -1.9% [-3.1%, +1.5%] ~same
api.getAll.int_to_int 0.50 ms 0.49 ms -0.2% [-7.2%, +7.5%] ~same
api.increment.int_to_int 12.8 ms 12.8 ms -0.5% [-4.2%, +0.1%] ~same
api.keys.int_to_int 6.2 ms 6.0 ms -3.2% [-6.2%, -2.1%] ~same
api.mergeWith.int_to_int 26.1 ms 26.3 ms +0.2% [-0.6%, +2.6%] ~same
api.mergeWith.string_to_int 121.6 ms 123.8 ms +1.6% [+0.6%, +3.4%] ~same
api.populationCount.int_to_int 0.00 ms 0.00 ms -0.3% [-0.3%, -0.3%] ~same
api.putAll.int_to_int 11.8 ms 11.8 ms -0.6% [-1.5%, +0.5%] ~same
api.range.keys.int_to_int 0.54 ms 0.53 ms -2.5% [-6.0%, -1.6%] ~same
api.range.size.string_to_int 2.5 ms 2.5 ms +0.2% [+0.1%, +1.6%] ~same
api.range.size1000.int_to_int 0.49 ms 0.52 ms +3.5% [+0.1%, +7.0%] ~same
api.setop.diff.bitset 7.1 ms 7.0 ms -1.4% [-3.5%, -0.8%] ~same
api.setop.diff.int_to_int 9.0 ms 9.1 ms +1.0% [-1.1%, +1.6%] ~same
api.setop.diff.string_to_int 64.3 ms 64.6 ms +0.4% [-3.3%, +0.6%] ~same
api.setop.intersect.bitset 7.0 ms 7.1 ms +0.8% [+0.0%, +1.9%] ~same
api.setop.intersect.int_to_int 9.1 ms 9.3 ms +0.9% [-0.4%, +3.6%] ~same
api.setop.intersect.string_to_int 71.6 ms 70.7 ms -1.1% [-1.8%, +0.8%] ~same
api.setop.union.bitset 14.8 ms 14.6 ms -1.3% [-2.2%, -0.7%] ~same
api.setop.union.int_to_int 23.1 ms 22.9 ms -1.2% [-2.1%, -0.0%] ~same
api.setop.union.string_to_int 150.4 ms 154.5 ms +2.1% [+1.4%, +2.9%] ~same
api.setop.xor.bitset 14.1 ms 14.1 ms +0.1% [-1.4%, +1.7%] ~same
api.setop.xor.int_to_int 18.4 ms 18.6 ms +0.5% [-1.1%, +2.2%] ~same
api.sumValues.int_to_int 4.1 ms 4.1 ms -0.8% [-1.3%, +2.8%] ~same
api.toArray.int_to_int 15.1 ms 14.9 ms -2.8% [-5.8%, +5.1%] ~same
api.values.int_to_int 6.4 ms 6.4 ms +0.5% [-3.8%, +1.3%] ~same
core.bitset.free 0.00 ms 0.00 ms +2.7% [-13.8%, +8.8%] ~same
core.bitset.iter 8.8 ms 9.1 ms +3.2% [+0.4%, +4.9%] ~same
core.bitset.read 5.9 ms 6.1 ms +3.6% [+1.2%, +4.4%] ~same
core.bitset.write 8.6 ms 8.7 ms +0.7% [-1.4%, +4.4%] ~same
core.int_to_int.free 0.20 ms 0.20 ms -0.7% [-2.0%, +25.6%] ~same
core.int_to_int.iter 10.0 ms 10.2 ms +1.6% [+0.5%, +2.5%] ~same
core.int_to_int.read 6.4 ms 6.8 ms +5.1% [+3.5%, +8.8%] ~same
core.int_to_int.write 12.2 ms 12.3 ms -0.1% [-0.6%, +1.3%] ~same
core.int_to_mixed.free 7.6 ms 7.8 ms +0.5% [-4.6%, +4.7%] ~same
core.int_to_mixed.iter 10.3 ms 10.5 ms +2.1% [-0.2%, +5.3%] ~same
core.int_to_mixed.read 6.7 ms 7.1 ms +5.0% [+2.7%, +6.5%] ~same
core.int_to_mixed.write 17.5 ms 17.7 ms +0.5% [-0.8%, +1.1%] ~same
core.int_to_packed.free 5.0 ms 4.9 ms -1.1% [-6.0%, +0.7%] ~same
core.int_to_packed.iter 21.0 ms 21.9 ms +2.3% [+0.6%, +4.3%] ~same
core.int_to_packed.read 17.0 ms 16.7 ms -1.9% [-3.0%, -0.7%] ~same
core.int_to_packed.write 24.1 ms 23.6 ms -1.8% [-2.9%, -1.0%] ~same
core.string_to_int.free 12.3 ms 12.4 ms +1.0% [-0.7%, +2.4%] ~same
core.string_to_int.iter 32.3 ms 32.4 ms 0.0% [-0.3%, +0.1%] ~same
core.string_to_int.read 20.1 ms 20.2 ms +0.6% [-3.9%, +1.6%] ~same
core.string_to_int.write 49.4 ms 50.3 ms +1.3% [+0.6%, +2.3%] ~same
core.string_to_int_adaptive.free 22.5 ms 22.2 ms -2.1% [-3.6%, +4.8%] ~same
core.string_to_int_adaptive.iter 40.8 ms 39.7 ms -2.6% [-4.5%, -1.3%] ~same
core.string_to_int_adaptive.read 12.3 ms 12.4 ms +1.6% [-1.2%, +5.0%] ~same
core.string_to_int_adaptive.write 86.0 ms 88.1 ms +1.3% [+0.7%, +2.6%] ~same
core.string_to_int_hash.free 22.5 ms 22.4 ms -1.0% [-2.4%, +6.2%] ~same
core.string_to_int_hash.iter 39.9 ms 40.2 ms +0.5% [-0.5%, +2.1%] ~same
core.string_to_int_hash.read 12.7 ms 13.2 ms +2.6% [-1.4%, +9.2%] ~same
core.string_to_int_hash.write 87.9 ms 86.7 ms -1.0% [-3.4%, +1.2%] ~same
core.string_to_mixed.free 38.6 ms 38.8 ms -0.6% [-1.3%, +2.4%] ~same
core.string_to_mixed.iter 34.7 ms 33.6 ms -3.4% [-5.9%, -2.3%] ~same
core.string_to_mixed.read 21.0 ms 21.6 ms +2.6% [+2.5%, +3.7%] ~same
core.string_to_mixed.write 64.7 ms 65.4 ms +1.6% [-0.4%, +3.3%] ~same
core.string_to_mixed_adaptive.free 70.1 ms 70.9 ms +0.8% [-3.4%, +4.8%] ~same
core.string_to_mixed_adaptive.iter 57.5 ms 54.9 ms -5.6% [-10.2%, -2.5%] ~same
core.string_to_mixed_adaptive.read 12.8 ms 13.4 ms +5.2% [+0.1%, +7.9%] ~same
core.string_to_mixed_adaptive.write 131.1 ms 132.1 ms +0.2% [-1.4%, +1.9%] ~same
core.string_to_mixed_hash.free 62.6 ms 62.2 ms -0.1% [-3.7%, +4.8%] ~same
core.string_to_mixed_hash.iter 51.2 ms 49.7 ms -2.8% [-6.2%, -0.7%] ~same
core.string_to_mixed_hash.read 13.1 ms 13.4 ms +3.6% [+0.3%, +4.5%] ~same
core.string_to_mixed_hash.write 120.1 ms 119.6 ms -0.4% [-2.8%, +1.4%] ~same

Memory Comparison

Memory: 0 regressions, 10 unchanged

Type Baseline Current Delta Status
Bitset 160 B 160 B 0.0% ~same
Int to Int 160 B 160 B 0.0% ~same
Int to Mixed 4.6 MB 4.6 MB 0.0% ~same
Int to Packed 8.0 MB 8.0 MB 0.0% ~same
String to Int 64.2 KB 64.2 KB 0.0% ~same
String to Int (Adaptive) 64.2 KB 64.2 KB 0.0% ~same
String to Int (Hash) 64.2 KB 64.2 KB 0.0% ~same
String to Mixed 4.6 MB 4.6 MB 0.0% ~same
String to Mixed (Adaptive) 4.6 MB 4.6 MB 0.0% ~same
String to Mixed (Hash) 4.6 MB 4.6 MB 0.0% ~same
Full benchmark output

Linux — PHP 8.6

══════════════════════════════════════════════════════════════════════════════════════════
  php-judy Benchmark Suite — 500,000 elements, 7 iterations (median)
  PHP 8.6.0-dev | Judy ext 2.7.1 | Linux x86_64
  Suite: all | Groups: core.int,core.str,api.batch,api.setops,adv.iter,adv.sso
  memory_limit: -1
  Memory: peak RSS of a child process, empty-process floor subtracted, median of 3
══════════════════════════════════════════════════════════════════════════════════════════

── Core: Integer-Keyed Types ────────────────────────────────────────────────

  Type                         Write(ms)      Read(ms)      Iter(ms)      Free(ms)        Memory
  ────────────────────────  ────────────  ────────────  ────────────  ────────────  ────────────
  BITSET                           14.86          9.67         15.94          0.00    < 296.0 KB
    PHP array                       5.79          3.11          2.43          0.32        8.0 MB
  INT_TO_INT                       20.10         10.62         16.95          0.19        4.3 MB
    PHP array                       5.99          3.14          2.42          0.36        8.0 MB
  INT_TO_MIXED                     28.55         11.33         17.01         11.96       40.2 MB
    PHP array                       8.74          4.45          2.67          3.27       38.1 MB
  INT_TO_PACKED                    37.56         26.94         34.38          7.16       16.2 MB
    PHP array                       8.59          4.48          2.66          3.39       38.0 MB

── Core: String-Keyed Types ────────────────────────────────────────────────

  Type                         Write(ms)      Read(ms)      Iter(ms)      Free(ms)        Memory
  ────────────────────────  ────────────  ────────────  ────────────  ────────────  ────────────
  STRING_TO_INT                    87.13         35.65         55.67         22.46       10.2 MB
    PHP array                      15.78          5.78          3.71          1.40       38.0 MB
  STRING_TO_MIXED                 105.44         37.57         56.81         67.08       46.2 MB
    PHP array                      16.51          6.58          3.79          4.55       67.9 MB
  STR_TO_INT_HASH                 143.83         20.02         68.10         35.99       31.4 MB
    PHP array                      15.37          5.81          3.67          1.35       38.0 MB
  STR_TO_MIX_HASH                 202.97         21.42         82.00        113.48       67.4 MB
    PHP array                      16.28          6.64          3.81          4.38       68.0 MB
  STR_INT_ADAPTIVE                143.48         19.49         67.19         36.03       31.4 MB
    PHP array                      15.45          5.77          3.68          1.32       38.1 MB
  STR_MIX_ADAPTIVE                220.04         20.13         79.45        114.40       67.4 MB
    PHP array                      16.39          6.53          3.81          4.33       67.9 MB

── API: Batch Operations (INT_TO_INT) ────────────────────────────────────────

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  putAll()                               17.46         24.58        1.4x
  fromArray()                            17.47         24.58        1.4x
  toArray()                              20.89         30.87        1.5x
  getAll(50100 keys)                      1.08          1.88        1.7x
  increment(500000 ops/1000 keys)         20.66          9.12        0.4x

── API: Native Methods (keys, values, sum, popCount, deleteRange, equals) ────

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  keys() INT_TO_INT                       9.88         19.45        2.0x
  values() INT_TO_INT                    10.13         19.49        1.9x
  keys($lo,$hi) INT_TO_INT                0.81         19.81       24.5x
    vs slice()->keys()                    2.36             -        2.9x
  size($lo,$hi) INT x1000                 0.55             -        O(1)
  size($lo,$hi) STRING_TO_INT             4.08          4.92        1.2x
  sumValues() INT_TO_INT                  5.99         16.31        2.7x
  populationCount() INT_TO_INT            0.00         17.23   277899.1x
  deleteRange() INT_TO_INT mid50%         32.49         47.59        1.5x
  equals() INT_TO_INT                    10.03         42.38        4.2x

── API: Set Operations (BITSET, 50% overlap) ────────────────────────────────

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  union() BITSET                         26.55          4.59        0.2x
  intersect() BITSET                     12.78          5.13        0.4x
  diff() BITSET                          12.63          3.06        0.2x
  xor() BITSET                           25.30         10.50        0.4x
  union() INT_TO_INT                     34.36         69.61        2.0x
  intersect() INT_TO_INT                 14.71         34.60        2.4x
  diff() INT_TO_INT                      14.44         36.31        2.5x
  xor() INT_TO_INT                       29.22         72.87        2.5x
  union() STRING_TO_INT                 252.23        276.02        1.1x
  intersect() STRING_TO_INT             107.27        121.56        1.1x
  diff() STRING_TO_INT                  104.20        118.93        1.1x
  mergeWith() INT_TO_INT                 41.95         53.96        1.3x
  mergeWith() STRING_TO_INT             200.51        215.26        1.1x

── Advanced: C-Level forEach/filter/map ──────────────────────────────────────

  Operation                        C-level(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  forEach() INT_TO_INT                   25.91         18.39        0.7x
  forEach() STRING_TO_INT                61.98         56.33        0.9x
  filter() INT_TO_INT (50% pass)         36.86         30.64        0.8x
  filter() STRING_TO_INT (50% pass)        116.06        108.91        0.9x
  map() INT_TO_INT (*2)                  39.74         36.96        0.9x
  map() STRING_TO_INT (*2)              158.53        154.16        1.0x

── Advanced: optimizeIteration trade (opt-in key-index payload mirror) ──────

  Operation                        default(ms)    optim.(ms)   optim./def.
  ──────────────────────────────  ────────────  ────────────  ────────────
  foreach() HASH kl16                    44.16         28.01         63.4%
  values() HASH kl16                     39.96         21.39         53.5%
  toArray() HASH kl16                    48.79         28.78         59.0%
  overwrite() HASH kl16                  27.21         33.20        122.0%
  increment() HASH kl16                  27.59         32.86        119.1%
    HASH kl16: honoured?          yes
  foreach() ADAPTIVE kl40                54.30         30.43         56.0%
  values() ADAPTIVE kl40                 51.63         23.01         44.6%
  toArray() ADAPTIVE kl40                65.74         36.40         55.4%
  overwrite() ADAPTIVE kl40              39.49         44.73        113.3%
    ADAPTIVE kl40: honoured?      yes
  skipped — increment() ADAPTIVE kl40: Judy::increment() is only supported for INT_TO_INT, STRING_TO_INT and STRING_TO_INT_HASH types

  Ratios are optimized as a percentage of default, measured inside this
  build; below 100 is the traversal win, above 100 the write price. Lower
  is better on every row, so a lost win reads as a rise.

── Advanced: Adaptive SSO — short keys (<8 bytes) via JudyL ─────────────────

  Operation                   STR_TO_INT    STR_INT_HASH   STR_INT_ADPTV    Best ratio
  ──────────────────────  ──────────────  ──────────────  ──────────────  ────────────
  insert (short keys)          66.29 ms      106.55 ms       98.55 ms       1.6x
  read (random access)         11.47 ms       12.82 ms       13.74 ms       1.1x
  foreach (iterate all)        40.15 ms       67.96 ms       68.71 ms       1.7x

── Advanced: Adaptive SSO — long keys (>=8 bytes) fallback to JudyHS ────────

  Operation                   STR_TO_INT    STR_INT_HASH   STR_INT_ADPTV    Best ratio
  ──────────────────────  ──────────────  ──────────────  ──────────────  ────────────
  insert (long keys)          106.19 ms      176.97 ms      178.48 ms       1.7x
  read (random access)         23.15 ms       17.80 ms       17.67 ms       1.3x
  foreach (iterate all)        67.25 ms       87.34 ms       85.63 ms       1.3x

══════════════════════════════════════════════════════════════════════════════════════════
  Benchmark complete — 2026-10-08 23:12:45
  All timings: median of 7 iterations via hrtime(true), 1 warmup run
  Memory: peak RSS of a child process building one structure of 500,000 elements,
          minus an empty-process floor. Measured with getrusage(), not
          memory_get_usage(), for the reason above. Judy's memory advantage is
          scale- and type-dependent — see BENCHMARK.md for the curve.
══════════════════════════════════════════════════════════════════════════════════════════

  JSON results written to: bench.json

Windows — PHP 8.6

══════════════════════════════════════════════════════════════════════════════════════════
  php-judy Benchmark Suite — 500,000 elements, 7 iterations (median)
  PHP 8.6.0RC3 | Judy ext 2.7.1 | WINNT AMD64
  Suite: all | Groups: core.int,core.str,api.batch,api.setops,adv.iter,adv.sso
  memory_limit: 4G
  Memory: peak RSS of a child process, empty-process floor subtracted, median of 3
══════════════════════════════════════════════════════════════════════════════════════════

── Core: Integer-Keyed Types ────────────────────────────────────────────────

  Type                         Write(ms)      Read(ms)      Iter(ms)      Free(ms)        Memory
  ────────────────────────  ────────────  ────────────  ────────────  ────────────  ────────────
  BITSET                           12.46          9.52         12.19          0.01    < 136.0 KB
    PHP array                       9.54          5.75          4.54          0.44        9.2 MB
  INT_TO_INT                       21.81         10.11         12.62          0.95        4.5 MB
    PHP array                       9.29          5.93          4.60          0.45        9.2 MB
  INT_TO_MIXED                     34.84         11.23         14.22         15.53       42.6 MB
    PHP array                      12.30          6.60          5.41          5.05       38.2 MB
  INT_TO_PACKED                    44.15         30.94         32.98         10.10       17.2 MB
    PHP array                      11.95          6.65          5.34          5.04       38.2 MB

── Core: String-Keyed Types ────────────────────────────────────────────────

  Type                         Write(ms)      Read(ms)      Iter(ms)      Free(ms)        Memory
  ────────────────────────  ────────────  ────────────  ────────────  ────────────  ────────────
  STRING_TO_INT                   102.16         51.43         59.03         25.54       10.5 MB
    PHP array                      20.26          9.01          4.88          1.33       38.0 MB
  STRING_TO_MIXED                 140.84         49.08         69.52         81.95       48.7 MB
    PHP array                      23.92          9.90          6.70          8.44       68.9 MB
  STR_TO_INT_HASH                 187.73         36.94         89.97         56.29       32.2 MB
    PHP array                      19.18          7.99          4.77          1.34       37.9 MB
  STR_TO_MIX_HASH                 289.32         40.11        120.10        164.04       70.4 MB
    PHP array                      23.53          9.74          6.74          8.51       68.9 MB
  STR_INT_ADAPTIVE                180.24         34.62         80.65         50.64       32.3 MB
    PHP array                      18.89          7.63          4.76          1.36       38.0 MB
  STR_MIX_ADAPTIVE                289.96         38.42        108.10        166.23       70.4 MB
    PHP array                      22.89          9.25          6.68          8.01       69.0 MB

── API: Batch Operations (INT_TO_INT) ────────────────────────────────────────

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  putAll()                               21.13         29.01        1.4x
  fromArray()                            20.61         29.01        1.4x
  toArray()                              19.14         33.38        1.7x
  getAll(50100 keys)                      1.06          1.92        1.8x
  increment(500000 ops/1000 keys)         18.79         14.48        0.8x

── API: Native Methods (keys, values, sum, popCount, deleteRange, equals) ────

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  keys() INT_TO_INT                       7.92         18.44        2.3x
  values() INT_TO_INT                     8.00         18.55        2.3x
  keys($lo,$hi) INT_TO_INT                0.49         18.50       37.7x
    vs slice()->keys()                    1.96             -        4.0x
  size($lo,$hi) INT x1000                 0.44             -        O(1)
  size($lo,$hi) STRING_TO_INT             3.90          4.81        1.2x
  sumValues() INT_TO_INT                  3.88         13.28        3.4x
  populationCount() INT_TO_INT            0.00         13.98   139841.0x
  deleteRange() INT_TO_INT mid50%         33.39         48.88        1.5x
  equals() INT_TO_INT                     6.06         30.27        5.0x

── API: Set Operations (BITSET, 50% overlap) ────────────────────────────────

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  union() BITSET                         20.03          5.67        0.3x
  intersect() BITSET                      8.64          5.45        0.6x
  diff() BITSET                           8.61          2.71        0.3x
  xor() BITSET                           17.26         12.69        0.7x
  union() INT_TO_INT                     32.15         57.87        1.8x
  intersect() INT_TO_INT                 12.42         26.74        2.2x
  diff() INT_TO_INT                      11.46         29.67        2.6x
  xor() INT_TO_INT                       23.49         59.66        2.5x
  union() STRING_TO_INT                 277.50        310.38        1.1x
  intersect() STRING_TO_INT             122.42        142.89        1.2x
  diff() STRING_TO_INT                  113.26        129.56        1.1x
  mergeWith() INT_TO_INT                 38.26         46.84        1.2x
  mergeWith() STRING_TO_INT             229.80        246.76        1.1x

── Advanced: C-Level forEach/filter/map ──────────────────────────────────────

  Operation                        C-level(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  forEach() INT_TO_INT                   17.12         15.11        0.9x
  forEach() STRING_TO_INT                54.70         53.70        1.0x
  filter() INT_TO_INT (50% pass)         24.73         26.49        1.1x
  filter() STRING_TO_INT (50% pass)         98.96        100.51        1.0x
  map() INT_TO_INT (*2)                  29.33         31.83        1.1x
  map() STRING_TO_INT (*2)              142.05        148.31        1.0x

── Advanced: optimizeIteration trade (opt-in key-index payload mirror) ──────

  Operation                        default(ms)    optim.(ms)   optim./def.
  ──────────────────────────────  ────────────  ────────────  ────────────
  foreach() HASH kl16                    77.63         36.58         47.1%
  values() HASH kl16                     46.98         26.21         55.8%
  toArray() HASH kl16                    68.89         51.05         74.1%
  overwrite() HASH kl16                  35.86         43.68        121.8%
  increment() HASH kl16                  36.40         45.84        125.9%
    HASH kl16: honoured?          yes
  foreach() ADAPTIVE kl40                75.73         32.33         42.7%
  values() ADAPTIVE kl40                 71.59         28.70         40.1%
  toArray() ADAPTIVE kl40                93.36         47.56         50.9%
  overwrite() ADAPTIVE kl40              54.07         74.66        138.1%
    ADAPTIVE kl40: honoured?      yes
  skipped — increment() ADAPTIVE kl40: Judy::increment() is only supported for INT_TO_INT, STRING_TO_INT and STRING_TO_INT_HASH types

  Ratios are optimized as a percentage of default, measured inside this
  build; below 100 is the traversal win, above 100 the write price. Lower
  is better on every row, so a lost win reads as a rise.

── Advanced: Adaptive SSO — short keys (<8 bytes) via JudyL ─────────────────

  Operation                   STR_TO_INT    STR_INT_HASH   STR_INT_ADPTV    Best ratio
  ──────────────────────  ──────────────  ──────────────  ──────────────  ────────────
  insert (short keys)          63.93 ms      115.24 ms      105.41 ms       1.8x
  read (random access)         10.80 ms       10.15 ms        9.67 ms       1.1x
  foreach (iterate all)        42.10 ms       67.18 ms       65.13 ms       1.6x

── Advanced: Adaptive SSO — long keys (>=8 bytes) fallback to JudyHS ────────

  Operation                   STR_TO_INT    STR_INT_HASH   STR_INT_ADPTV    Best ratio
  ──────────────────────  ──────────────  ──────────────  ──────────────  ────────────
  insert (long keys)          106.66 ms      215.52 ms      215.57 ms       2.0x
  read (random access)         27.31 ms       16.39 ms       16.95 ms       1.7x
  foreach (iterate all)        94.48 ms      145.82 ms      144.76 ms       1.5x

══════════════════════════════════════════════════════════════════════════════════════════
  Benchmark complete — 2026-10-08 23:14:13
  All timings: median of 7 iterations via hrtime(true), 1 warmup run
  Memory: peak RSS of a child process building one structure of 500,000 elements,
          minus an empty-process floor. Measured with getrusage(), not
          memory_get_usage(), for the reason above. Judy's memory advantage is
          scale- and type-dependent — see BENCHMARK.md for the curve.
══════════════════════════════════════════════════════════════════════════════════════════

  JSON results written to: bench.json

@orieg
orieg merged commit 57df19c into main Oct 8, 2026
39 checks passed
@orieg
orieg deleted the fix/unserialize-strict-validation branch October 8, 2026 23:19
@orieg orieg mentioned this pull request Oct 8, 2026
4 of 6 tasks
orieg added a commit that referenced this pull request Oct 8, 2026
Release 2.8.0: version lockstep bump (php_judy.h + package.xml), 2.7.1 notes moved into <changelog>, new 2.8.0 notes covering the #217 hardening sprint and #225.
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.

1 participant