Skip to content

Craftsmanship hardening sprint: security, resilience, coverage, CI gate, docs - #217

Merged
orieg merged 6 commits into
mainfrom
craftsmanship-hardening
Oct 7, 2026
Merged

orieg merged 6 commits into
mainfrom
craftsmanship-hardening

Conversation

@orieg

@orieg orieg commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Craftsmanship hardening sprint — 5 batches, all gates green

Hardening only: no new API surface, no behavior change except verified bug fixes, each
shipped with a .phpt that fails on the pre-fix code. 47 files, +2605/−199. Every batch
gate (1 reviewer) and the final 3-reviewer changeset review (lenses: C memory-safety,
test discipline, docs/house-rules) returned PASS / APPROVE with 0 CRITICAL, 0 MAJOR.

Commits

Commit Batch Contents
2b0643e 1 — Security Embedded-NUL string-key rejection on all six string-keyed types (exception, no silent truncation collision — fixes #117); increment() oversized-key message reports true length; rejection covers every key-taking method incl. range bounds
56c82d6 2 — Test coverage 10 new non-vacuous pins (tests/ only): clone/populate counter agreement, INI judy.debug_preview_size contract, byCount empty-slot matrix, bounds TypeErrors, judy_type() negatives, increment boundary arithmetic, callback arg order + exception propagation, adaptive counter zero-value, set-ops mixed-type throw, debug-info dynamic-property non-shadowing
41758af 3 — Error handling & resilience Canonical ENTRY expiry predicate (shared clock capture); traversals/materialisation hide expired entries while count()/size() raw counters include them until pruneExpired() (verified + pinned); mergeWith preserves ENTRY TTL/flags verbatim + mixed-refcount + reentrancy safety; deleteRange/pruneExpired destructor-reentrancy safety; GC cycles through MIXED and ENTRY values collectable; equals() on ENTRY compares payloads
e0b6b8d 4 — Code quality judy.string.maxlength INI cap (with clamp-helper fix caught by the gate probe); NULL-pointer inits on navigation branches; CI arginfo gate corrected — php-src gen_stub.php --verify does not catch stub↔arginfo drift (it rewrites the header and only checks aliases); gate is now regenerate-then-diff and fails on drift (red-run proven)
88124d8 5 — Docs & observability 3-reviewer docs review (2 CRITICAL, 3 MAJOR, 3 MINOR all fixed): matrix STRING_TO_INT_ADAPTIVE set-ops cell corrected to yes (doc aligned to existing pin regression_adaptive_setops_001); STRING_TO_ENTRY added to Key/Value table; generate-api-docs.php --check now asserts matrix row-completeness + cell vocabulary (sabotage red-run proven); "Rust Expanse"/--with-expanse fiction stripped (config.m4 only defines --with-judy; Expanse is a real sibling libJudy-compat engine linked via --with-judy=DIR); Judy_arginfo.h regenerated (stub-hash stamp only); cache-ttl-pruning example corrected to verified expiry truth; BENCHMARK/CONTRIBUTING/SECURITY reconciled
9991cd6 Wrap-up Two Reviewer-C package.xml formatting MINORs (EOF newline, <file> indent) — hygiene only

Validation

  • Clean rebuild via phpize --clean: 0 compiler warnings (CI-exact source-location pattern).
  • Full suite: 258/258 PASS (0 skipped/warned/failed, ~42 s), re-run on the rebased tree.
  • All 10 examples/*.php exit 0. One documented caveat: dedup-large-stream.php spawns
    children with a JUDY_SO env flag, so the plain smoke exercises its graceful fallback;
    re-run with JUDY_SO set verifies the full measurement path (array/judy/hashed agree on
    unique: 266396).
  • discipline check: PASS (27 gates, 0 errors, 1 override — see below).
  • php scripts/generate-api-docs.php --check: PASS.

Disclosures (taken as read by the 6.2 reviewers)

  • Valgrind (3.7): SKIPPED and recorded — no valgrind on the dev machine; no valgrind-clean claim is made.
  • Rule-3 exceptions (behavior pins that cannot fail on pre-fix code, documented in-tree):
    1.5 class-instance UAF (source-review detector), 1.6 increment wrap-vs-exhaustion choice
    (characterization), 3.4 JERR not portably inducible, 3.5 leak verdict was no leak.
  • __unserialize nits (report-only): the main-checkout holds an in-flight
    __unserialize strict-validation edit from another session; 3 nits + an ordering hazard
    with tests/serialize_error_001.phpt's pinned Warning were reported to its owner. This
    PR does not touch that path.
  • Carried follow-ups (recorded, not in this PR): key-length cap not inside
    judy_string_slot_acquire; set() TTL signed-overflow; bulk half-population on a bad
    key; hand-rolled callback materialisation; runtime phpt that probes every
    api-metadata.php matrix cell; duplication inventory D1–D7 (no consolidation ordered);
    stale README md5sums; optimizeIteration-inheritance test deferred.
  • Protected paths untouched: libjudy/, baselines/, php_judy.h
    PHP_JUDY_VERSION, and package.xml <release>/<version> are byte-identical to main.
  • The sprint's plan/state files live under gitignored .claude/* and are not part of this PR.
  • Discipline override: the AGENTS.md edit in batch 5 ships with an
    allow-agent-instructions: AGENTS.md <reason> directive in its commit body (and here)
    — the local MCP discipline tool shows a residual finding only because it evaluates
    without the PR description; the CLI engine (what CI runs) honors the directive and passes.

Fixes #117

orieg added 6 commits October 7, 2026 01:40
Gate 1: PASS (1 reviewer, C memory-safety lens) - 0 CRITICAL, 0 MAJOR,
3 MINOR, 4 NOTE; comment fixes applied in passing, follow-ups F-G1-1..4
recorded in plan Appendix A. Validation at gate: 240/240 PASS
(234 baseline + 6 new), 0 compiler warnings.

Fixes and red-runs (Rule 3):

1.1  Append guard + exception text now enumerate all 7 string-keyed
     types incl. STRING_TO_ENTRY ($j[]=1 was SIGSEGV, exit 139).
     Red-run: FAIL (tests/string_to_entry_append_001, crash pre-fix;
     string_to_int_005 / string_to_mixed_005 EXPECTs updated to the
     7-type message).

1.2  forEach/filter/map on STRING_TO_ENTRY no longer read a
     judy_cache_entry_t* as zval*: routed through judy_value_from_slot().
     Red-run: FAIL (tests/string_to_entry_callback_iterator_001,
     exit 139 pre-fix; pins expired-entry -> NULL, Gate-1 ruling (b)).

1.3  equals() gains an ENTRY branch: zend_is_identical over values
     materialised via judy_value_from_slot(), expired reads as NULL on
     both sides (Gate-1 ruling (a)).
     Red-run: FAIL (identical ENTRY arrays returned false / latent bad
     deref; tests/equals_string_to_entry_001).

1.4  set() enforces PHP_JUDY_MAX_LENGTH before slot acquisition, with
     increment()'s exact message. A 70 000-byte key was stored, then the
     next ordered traversal copied it unbounded into the 64 KB
     key_scratch (heap overflow).
     Red-run: FAIL (observed: 'set() accepted oversized key', count=1,
     keys() returned the full 70 000-byte string through key_scratch).

1.5  increment() captures key_len before zend_string_release (UAF read
     of ZSTR_LEN in the exception message). RULE-3 EXCEPTION: the .phpt
     cannot fail pre-fix on standard Zend MM (free-list header occupies
     bytes 0-15, ZSTR_LEN at offset 16, no allocation in between) -
     shipped as characterization; detector is source review (valgrind
     unavailable, Step 3.7 SKIP recorded). Observed pre-fix: PASS.

1.6  increment() boundary arithmetic wraps via (zend_ulong) add in both
     branches - removes signed-overflow UB at PHP_INT_MAX/MIN.
     RULE-3 EXCEPTION (characterization): the red-run FAIL was an
     incorrect initial EXPECT (INT_MIN-2 wraps to ...806, not ...805);
     with the corrected EXPECT the pre-fix build already produced the
     pinned wrap semantics (observed). Gate default ruling: WRAP, a
     throw would need explicit sign-off (plan Step 1.6 / test comment).

Canonical ENTRY/expiry contract ruling recorded at Gate 1 (equals,
traversal visibility, count() pre-prune wording, mergeWith TTL/flags) -
plan Gate-1 record; cited by 1.3/3.3/3.6/5.2, never re-decided.
Gate 2: PASS (1 reviewer, vacuous-test lens) - 0 CRITICAL, 0 MAJOR,
1 MINOR, 4 NOTE; fixes applied in passing. Reviewer independently
extracted all 11 FILE sections (byte-exact outputs) and ran 6 mutation
simulations proving each pin fails on regression (arg-order flip,
throw-at-visit-3, silent set-op merge, wrong expected message,
inclusive searchNext, display_errors=0). Validation: 250/250 PASS
(234 baseline + 6 Batch-1 + 10 new), scope = tests/ only, no source
edits.

New pins (pass on arrival by design - they pin CURRENT behavior;
non-vacuity is the Gate-2 mutation evidence recorded in the plan):

2.1  callback_arg_order_001: forEach/filter/map hand ($value, $key)
     on all 11 types - values deliberately distinct in type AND value
     so a swap cannot alias into a false OK (docs are backwards; fixed
     in Batch 5). callback_exception_001: exception propagates,
     iteration stops (visits=2), source intact, object usable after;
     3 storage classes x 3 ops.

2.2  set_ops_mixed_type_throw_001: union/intersect/diff/xor throw the
     exact 'BITSET and integer-valued' message on same-type
     INT_TO_MIXED / INT_TO_PACKED / STRING_TO_MIXED / STRING_TO_ENTRY
     operands, both operands untouched (12 previously unpinned cells +
     ENTRY). average_values_contract_001: empty->NULL for every type,
     hand-computed means (15.0/1.5), BITSET presence mean, throw cells
     packed/mixed/entry for averageValues, plus the sumValues() throw
     cells missing from sum_values_001 including the empty-array
     asymmetry (avg NULL vs sum THROW).

2.3  bounds_type_error_001: string bounds on int-keyed deleteRange()
     (engine-style TypeError naming the argument) and int bounds on
     string-keyed slice() (explicit C TypeError); silent-coercion
     cells deliberately NOT pinned (bug N1 gets its failing
     expectation in Step 3.8). nav_boundary_001: searchNext()
     exclusivity, NULL (not false) past-end, first("")/searchNext("")
     inclusive/exclusive; last("")/prev("") divergence deliberately
     NOT pinned (recorded follow-up).

2.4  bycount_empty_slot_matrix_001: documented nulls for byCount +
     empty-slot methods on the 3 string-keyed INT types; full matrix
     on INT_TO_PACKED (zero prior coverage); lastEmpty()=-1 semantics
     pinned with the '-1 stored -> -2' proof it is a real search.
     judy_type_negative_001: TypeErrors on int/string/stdClass,
     int 0 for newInstanceWithoutConstructor (stable over 500 runs).

2.5  string_to_entry_error_matrix_001: NUL rejection + wrong-receiver
     TypeErrors for get/getEntry/getExpiry/getFlags (real get_class()
     asserted after the Gate-2 MINOR), getAll/jsonSerialize happy
     paths, increment() throw on both ADAPTIVE types.
     unserialize_malformed_data_001: valid type + non-array data
     rejects; comment carries the pending-main-checkout-diff caveat.
     serialize_error_001: %A tightened to the exact blank line +
     exact Warning line (%s path, on line 23) + exact throw line.

Batch-2 rule honored: no source file changed. Step 4.3 disclosure on
record in the plan Gate-2 record: the pinned Warning is emitted by
JTYPE() before the invalid-type throw, and the other session's pending
__unserialize range-check would throw before JTYPE - that diff landing
fails serialize_error_001 on the missing Warning line.
3.1  get_gc: collect STRING_TO_ENTRY stored values so value cycles are
     collectable (private cursor; expired entries exposed to GC deliberately).
3.2  deleteRange() string-keyed ENTRY branch now frees the cache-entry struct
     (trie/adaptive/hash) — pre-fix it leaked the struct AND its value refcount
     and never ran __destruct. Delete-before-free ordering preserved.
3.3  Expired entries skipped by value-serving traversals (iterator, get,
     keys/values/toArray) while count()/size() stay raw-counter O(1) — the
     Gate-1 canonical ruling, applied consistently via judy_entry_is_expired_at.
3.4  clone() fail path: partial-populate errors now free state and throw via a
     goto fail label (clone of ZEND_CLONE semantics); populate JERR guards +
     JUDY_UNLIKELY; fixed a BITSET fromArray fall-through missing return.
     Rule-3 exception: JERR is not portably inducible — success-path pins +
     Gate-3 source review carry the coverage.
3.5  pruneExpired() re-entrancy + counter discipline: private emalloc cursor
     (freed incl. on early exit), JSLD-confirmed removals gated on Rc == 1
     before counter--/bytes_sub/free, entry struct freed BEFORE the value
     destructor via ZVAL_COPY_VALUE steal. Measurement verdict: no per-entry
     leak — the throw path leaves a bounded ~3560 B Zend-MM plateau (identical
     at 1000 and 3000 rounds) plus ~57 B/round maxRSS watermark; pre-fix
     1768xN figures were peak-waterline retention, not leaks.
3.6  mergeWith() STRING_TO_ENTRY >> ENTRY now copies expires_at/flags verbatim
     (new judy_write_entry_verbatim(): struct copied via slot_acquire, value
     addref'd before any user code, publish-then-destroy on overwrite) instead
     of zeroing them through the generic write helper. Expired sources stay
     hidden on read in the target, mirroring slice().
3.7  valgrind dynamic spot-check: SKIP-RECORDED (no valgrind on this machine);
     the Step 1.5 UAF fix keeps source-review + .phpt characterization coverage.
3.8  Wrong-category range bounds now throw TypeError instead of silently
     coercing to a wrong-range read: int-keyed keys/values/toArray/size/slice
     reject non-numeric strings (keys("a","b") returned [0] before);
     string-keyed deleteRange rejects integer bounds (Z_PARAM_STRING coerced
     ints to their string form). Numeric strings still name integer keys,
     matching deleteRange()'s Z_PARAM_LONG weak-mode contract.

Gate 3 (expert review, security/C-memory-safety lens): APPROVE, 0 CRITICAL,
0 MAJOR. Fixed both actioned MINORs: F-G3-1 all deleteRange struct-frees now
gated on the delete Rc == 1 (JERR is truthy and means the slot still owns the
value — a latent UAF; BITSET counter gated too); F-G3-2 mergeWith ENTRY copy
FAILURE now throws instead of silently dropping the entry. Manifest sync: 23
missing test <file> entries added to package.xml <contents> (16 tracked + 7
new), <release> untouched, XML re-validated.

Tests: 257/257 PASS on default and --enable-judy-debug-mirror builds,
0 compiler warnings both. check_diff clean. Disclosure recorded (Gate 3, out
of 3.6 scope): filter()/map() still strip ENTRY TTL/flags through the generic
write helper — value transforms, not copies; future-release decision.
Docs & observability hardening with Gate-5 expert-review findings fixed
in-tree (all verified against code or a runtime probe of modules/judy.so):

- forEach()/filter()/map() callback order documented as ($value, $key),
  matching judy_callback_iterator (args[0]=value) and the pinned tests
- Compatibility matrix: 11th STRING_TO_ENTRY column added; the
  STRING_TO_INT_ADAPTIVE set-ops cell corrected to 'yes' (runtime-verified,
  pinned by tests/regression_adaptive_setops_001.phpt); STRING_TO_ENTRY row
  added to the Key/Value type table; footer now says the *unbounded*
  count()/size() forms are raw-counter O(1) — a bounded size() walks the
  key index (judy_count_range) and likewise includes expired-but-unpruned
- generate-api-docs.php --check now also asserts every matrix row carries
  exactly matrix_columns with legend vocabulary, so a missing cell fails
  instead of rendering a silent '?' (red-run proven: dropped-cell -> exit 1)
- SECURITY.md: 2.7.x supported / <2.7 unsupported, now shipped via
  package.xml <file role=doc>
- docs/index.html: hero v2.7.1, PHP 8.1-8.5 + 8.6 prerelease, fictional
  --with-expanse removed everywhere; Expanse described accurately as an
  optional pure-Rust libJudy engine linkable via --with-judy=DIR
- package.xml 2.7.0 changelog: --with-expanse claim corrected to the real
  --with-judy=DIR story
- README.md '2.6x faster' pruneExpired claim reworded qualitatively;
  BENCHMARK.md version stamps reconciled (measured 2.4.2, re-verified
  through 2.5.2; vendoring arms measured separately on 2.6.0), tail fixed
- CONTRIBUTING.md file-home table now lists shipped vs repo-only docs
  (AGENTS.md/CLAUDE.md/CONTRIBUTING.md/llms.txt repo-only)
- examples/cache-ttl-pruning.php teaches the Gate-1 ruling truthfully:
  reads and traversals hide an expired entry; only count()/size() raw
  counters include it until pruneExpired() (example re-run, output matches)
- Judy_arginfo.h regenerated (stub hash stamp only) after the stub docblock
  edit so the CI regenerate-then-diff arginfo gate stays green

allow-agent-instructions: AGENTS.md doc-accuracy fix: empty-slot variants exist on all types, return null on string-keyed (verified in C + runtime probe)
Step 6.2 3-reviewer changeset review (all APPROVE, 0 CRITICAL/MAJOR). The only
actionable findings were two Reviewer-C package.xml formatting MINORs, folded
into this wrap-up commit per the reviewers' own guidance:

- restore the trailing newline at EOF (batch 5's changelog edit had dropped it)
- align the tests/ini_string_maxlength_001.phpt <file> entry indent (10 -> 9 spaces)

No content change: xmllint clean, <release>/<version>/<date>/<stability>
tokens byte-identical to main. Verified: discipline PASS (overrides: 1).
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Test Results

PHP Platform Arch TS Tests Pass Fail Skip Duration
✅ 8.1 Linux x64 - 258 258 0 0 8.7s
✅ 8.2 Linux x64 - 258 258 0 0 7.3s
✅ 8.3 Linux x64 - 258 258 0 0 10.0s
✅ 8.4 Linux x64 - 258 258 0 0 7.4s
✅ 8.5 Linux x64 - 258 258 0 0 11.2s
✅ 🧪 8.6 Linux x64 - 258 258 0 0 12.6s
✅ 8.1 Linux (Expanse) x64 - 258 258 0 0 10.5s
✅ 8.2 Linux (Expanse) x64 - 258 258 0 0 10.3s
✅ 8.3 Linux (Expanse) x64 - 258 258 0 0 9.4s
✅ 8.4 Linux (Expanse) x64 - 258 258 0 0 10.7s
✅ 8.5 Linux (Expanse) x64 - 258 258 0 0 8.8s
✅ 🧪 8.6 Linux (Expanse) x64 - 258 258 0 0 13.4s
✅ 8.1 Windows x64 nts 258 258 0 0 14.7s
✅ 8.2 Windows x64 nts 258 258 0 0 11.9s
✅ 8.3 Windows x64 nts 258 258 0 0 14.8s
✅ 8.4 Windows x64 nts 258 258 0 0 15.8s
✅ 8.5 Windows x64 nts 258 258 0 0 15.5s
✅ 🧪 8.6 Windows x64 nts 258 258 0 0 10.5s
✅ 8.1 Windows (Expanse) x64 nts 258 258 0 0 15.4s
✅ 8.2 Windows (Expanse) x64 nts 258 258 0 0 13.8s
✅ 8.3 Windows (Expanse) x64 nts 258 258 0 0 14.9s
✅ 8.4 Windows (Expanse) x64 nts 258 258 0 0 16.7s
✅ 8.5 Windows (Expanse) x64 nts 258 258 0 0 15.8s
✅ 🧪 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 < 376.0 KB 8.0 MB > 22x smaller
Int to Int 4.2 MB 7.9 MB 1.9x smaller
Int to Mixed 40.2 MB 38.0 MB 1.06x larger
Int to Packed 16.3 MB 38.0 MB 2.3x smaller
String to Int 10.1 MB 38.0 MB 3.8x smaller
String to Mixed 46.3 MB 68.0 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.1 MB about the same
String to Int (Adaptive) 31.5 MB 38.1 MB 1.2x smaller
String to Mixed (Adaptive) 67.4 MB 68.1 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 376.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() 19.8 ms 29.3 ms 1.5x
fromArray() 20.0 ms 29.3 ms 1.5x
toArray() 22.7 ms 35.6 ms 1.6x
getAll() 1.1 ms 2.1 ms 1.9x
keys() 12.0 ms 24.7 ms 2.1x
values() 12.2 ms 24.8 ms 2.0x
sumValues() 8.6 ms 19.8 ms 2.3x
populationCount() 0.00 ms 21.7 ms O(1) vs O(n)
deleteRange() 39.0 ms 56.9 ms 1.5x
equals() 12.1 ms 50.2 ms 4.1x
union() Bitset 32.0 ms 4.7 ms 0.1x
intersect() Bitset 14.9 ms 5.6 ms 0.4x
diff() Bitset 14.6 ms 3.3 ms 0.2x
xor() Bitset 29.1 ms 11.2 ms 0.4x
mergeWith() Int→Int 51.7 ms 67.5 ms 1.3x
mergeWith() Str→Int 234.1 ms 252.7 ms 1.1x
forEach() Int→Int 27.3 ms 23.9 ms 0.9x
filter() Int→Int 43.2 ms 40.4 ms 0.9x
map() Int→Int 47.4 ms 47.2 ms ~1.0x
forEach() Str→Int 63.9 ms 58.8 ms 0.9x
filter() Str→Int 120.6 ms 111.6 ms 0.9x
map() Str→Int 162.7 ms 155.4 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 18.3 ms 7.1 ms 0.4x
Bitset Read 11.9 ms 3.9 ms 0.3x
Bitset Iter 20.2 ms 3.0 ms 0.1x
Bitset Free 0.01 ms 0.31 ms 45.3x
Int to Int Write 24.0 ms 7.2 ms 0.3x
Int to Int Read 11.7 ms 4.7 ms 0.4x
Int to Int Iter 21.5 ms 3.0 ms 0.1x
Int to Int Free 0.19 ms 0.32 ms 1.7x
Int to Mixed Write 34.5 ms 10.5 ms 0.3x
Int to Mixed Read 13.8 ms 5.6 ms 0.4x
Int to Mixed Iter 21.9 ms 4.0 ms 0.2x
Int to Mixed Free 14.8 ms 4.0 ms 0.3x
String to Int Write 93.3 ms 16.4 ms 0.2x
String to Int Read 36.6 ms 7.4 ms 0.2x
String to Int Iter 56.4 ms 4.9 ms 0.1x
String to Int Free 18.3 ms 1.2 ms 0.1x
String to Int (Adaptive) Write 161.6 ms 16.2 ms 0.1x
String to Int (Adaptive) Read 24.8 ms 7.1 ms 0.3x
String to Int (Adaptive) Iter 67.2 ms 4.9 ms 0.1x
String to Int (Adaptive) Free 40.6 ms 1.1 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 18.3 ms 7.1 ms 0.4x
Bitset Read 11.9 ms 3.9 ms 0.3x
Bitset Iter 20.2 ms 3.0 ms 0.1x
Bitset Free 0.01 ms 0.31 ms 45.3x
Int to Int Write 24.0 ms 7.2 ms 0.3x
Int to Int Read 11.7 ms 4.7 ms 0.4x
Int to Int Iter 21.5 ms 3.0 ms 0.1x
Int to Int Free 0.19 ms 0.32 ms 1.7x
Int to Mixed Write 34.5 ms 10.5 ms 0.3x
Int to Mixed Read 13.8 ms 5.6 ms 0.4x
Int to Mixed Iter 21.9 ms 4.0 ms 0.2x
Int to Mixed Free 14.8 ms 4.0 ms 0.3x
Int to Packed Write 48.0 ms 10.6 ms 0.2x
Int to Packed Read 30.0 ms 5.8 ms 0.2x
Int to Packed Iter 40.3 ms 3.6 ms 0.1x
Int to Packed Free 9.9 ms 3.9 ms 0.4x
String to Int Write 93.3 ms 16.4 ms 0.2x
String to Int Read 36.6 ms 7.4 ms 0.2x
String to Int Iter 56.4 ms 4.9 ms 0.1x
String to Int Free 18.3 ms 1.2 ms 0.1x
String to Mixed Write 116.0 ms 19.3 ms 0.2x
String to Mixed Read 40.6 ms 9.6 ms 0.2x
String to Mixed Iter 65.9 ms 6.6 ms 0.1x
String to Mixed Free 63.7 ms 6.8 ms 0.1x
String to Int (Hash) Write 161.1 ms 16.2 ms 0.1x
String to Int (Hash) Read 26.3 ms 7.4 ms 0.3x
String to Int (Hash) Iter 67.6 ms 4.9 ms 0.1x
String to Int (Hash) Free 40.6 ms 1.1 ms 0.0x
String to Mixed (Hash) Write 233.9 ms 19.7 ms 0.1x
String to Mixed (Hash) Read 27.8 ms 9.5 ms 0.3x
String to Mixed (Hash) Iter 88.0 ms 6.2 ms 0.1x
String to Mixed (Hash) Free 120.0 ms 6.7 ms 0.1x
String to Int (Adaptive) Write 161.6 ms 16.2 ms 0.1x
String to Int (Adaptive) Read 24.8 ms 7.1 ms 0.3x
String to Int (Adaptive) Iter 67.2 ms 4.9 ms 0.1x
String to Int (Adaptive) Free 40.6 ms 1.1 ms 0.0x
String to Mixed (Adaptive) Write 234.1 ms 19.3 ms 0.1x
String to Mixed (Adaptive) Read 27.6 ms 9.5 ms 0.3x
String to Mixed (Adaptive) Iter 87.4 ms 6.2 ms 0.1x
String to Mixed (Adaptive) Free 117.7 ms 6.7 ms 0.1x
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 17.8 ms (0.4x) 8.3 ms (0.5x) 18.0 ms (0.4x) 8.9 ms (0.5x) 17.7 ms (0.4x) 18.3 ms (0.4x)
Bitset Read 11.7 ms (0.3x) 5.7 ms (0.4x) 10.7 ms (0.4x) 5.6 ms (0.4x) 10.4 ms (0.4x) 11.9 ms (0.3x)
Int to Int Write 23.8 ms (0.3x) 11.7 ms (0.3x) 24.1 ms (0.3x) 13.8 ms (0.3x) 23.6 ms (0.3x) 24.0 ms (0.3x)
Int to Int Read 12.5 ms (0.3x) 6.0 ms (0.3x) 11.8 ms (0.3x) 6.8 ms (0.3x) 12.3 ms (0.4x) 11.7 ms (0.4x)
Int to Mixed Write 35.0 ms (0.3x) 18.8 ms (0.4x) 33.0 ms (0.3x) 18.7 ms (0.4x) 36.0 ms (0.3x) 34.5 ms (0.3x)
Int to Mixed Read 13.9 ms (0.4x) 7.5 ms (0.4x) 14.2 ms (0.4x) 7.6 ms (0.4x) 14.0 ms (0.4x) 13.8 ms (0.4x)
Int to Packed Write 49.6 ms (0.2x) 22.5 ms (0.3x) 45.1 ms (0.2x) 25.4 ms (0.3x) 48.1 ms (0.2x) 48.0 ms (0.2x)
Int to Packed Read 29.4 ms (0.2x) 15.0 ms (0.2x) 30.7 ms (0.2x) 17.1 ms (0.2x) 29.7 ms (0.2x) 30.0 ms (0.2x)
String to Int Write 91.7 ms (0.2x) 54.2 ms (0.2x) 91.0 ms (0.2x) 57.6 ms (0.2x) 92.5 ms (0.2x) 93.3 ms (0.2x)
String to Int Read 37.2 ms (0.2x) 23.6 ms (0.2x) 36.7 ms (0.2x) 25.6 ms (0.2x) 37.4 ms (0.2x) 36.6 ms (0.2x)
String to Mixed Write 113.5 ms (0.2x) 71.0 ms (0.2x) 113.1 ms (0.2x) 77.9 ms (0.2x) 117.8 ms (0.2x) 116.0 ms (0.2x)
String to Mixed Read 40.7 ms (0.2x) 26.8 ms (0.2x) 40.5 ms (0.2x) 29.2 ms (0.2x) 41.7 ms (0.3x) 40.6 ms (0.2x)
String to Int (Hash) Write 164.9 ms (0.1x) 103.6 ms (0.1x) 167.3 ms (0.1x) 110.9 ms (0.1x) 178.3 ms (0.1x) 161.1 ms (0.1x)
String to Int (Hash) Read 25.6 ms (0.3x) 15.3 ms (0.3x) 25.5 ms (0.3x) 16.7 ms (0.3x) 25.5 ms (0.3x) 26.3 ms (0.3x)
String to Mixed (Hash) Write 232.4 ms (0.1x) 153.9 ms (0.1x) 231.4 ms (0.1x) 177.7 ms (0.1x) 251.2 ms (0.1x) 233.9 ms (0.1x)
String to Mixed (Hash) Read 27.8 ms (0.3x) 18.0 ms (0.3x) 28.3 ms (0.4x) 17.6 ms (0.4x) 29.4 ms (0.4x) 27.8 ms (0.3x)
String to Int (Adaptive) Write 166.1 ms (0.1x) 101.4 ms (0.1x) 161.6 ms (0.1x) 106.5 ms (0.1x) 184.0 ms (0.1x) 161.6 ms (0.1x)
String to Int (Adaptive) Read 25.5 ms (0.3x) 14.8 ms (0.3x) 24.6 ms (0.3x) 16.0 ms (0.3x) 25.0 ms (0.3x) 24.8 ms (0.3x)
String to Mixed (Adaptive) Write 231.8 ms (0.1x) 160.5 ms (0.1x) 238.0 ms (0.1x) 186.0 ms (0.1x) 252.0 ms (0.1x) 234.1 ms (0.1x)
String to Mixed (Adaptive) Read 27.2 ms (0.3x) 17.4 ms (0.4x) 27.6 ms (0.4x) 19.9 ms (0.3x) 28.1 ms (0.4x) 27.6 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() 19.2 ms (1.5x) 9.9 ms (1.5x) 19.8 ms (1.4x) 11.7 ms (1.4x) 19.3 ms (1.4x) 19.8 ms (1.5x)
fromArray() 19.6 ms (1.4x) 10.2 ms (1.5x) 19.8 ms (1.4x) 11.8 ms (1.4x) 19.2 ms (1.4x) 20.0 ms (1.5x)
toArray() 22.8 ms (1.7x) 13.0 ms (1.6x) 23.4 ms (1.5x) 12.7 ms (1.7x) 28.7 ms (1.5x) 22.7 ms (1.6x)
getAll() 1.1 ms (2.0x) 0.54 ms (1.9x) 1.1 ms (2.2x) 0.62 ms (1.9x) 1.2 ms (1.7x) 1.1 ms (1.9x)
keys() 12.7 ms (2.2x) 5.7 ms (3.1x) 10.6 ms (2.3x) 5.6 ms (2.1x) 12.7 ms (2.1x) 12.0 ms (2.1x)
values() 14.3 ms (2.0x) 6.1 ms (2.8x) 11.2 ms (2.1x) 5.7 ms (2.1x) 14.4 ms (1.8x) 12.2 ms (2.0x)
sumValues() 8.2 ms (2.7x) 4.2 ms (2.2x) 6.4 ms (3.0x) 3.4 ms (2.7x) 9.6 ms (2.3x) 8.6 ms (2.3x)
populationCount() 0.00 ms (235631x) 0.00 ms (100888x) 0.00 ms (201666x) 0.00 ms (93998x) 0.00 ms (239856x) 0.00 ms (216932x)
deleteRange() 39.4 ms (1.5x) 19.0 ms (1.5x) 38.2 ms (1.5x) 21.7 ms (1.4x) 41.1 ms (1.4x) 39.0 ms (1.5x)
equals() 12.2 ms (4.1x) 5.6 ms (4.0x) 10.9 ms (4.3x) 5.4 ms (4.4x) 13.2 ms (3.9x) 12.1 ms (4.1x)
union() Bitset 33.0 ms (0.2x) 14.6 ms (0.2x) 32.2 ms (0.2x) 16.2 ms (0.2x) 31.4 ms (0.2x) 32.0 ms (0.1x)
intersect() Bitset 14.9 ms (0.4x) 6.1 ms (0.5x) 13.7 ms (0.4x) 7.1 ms (0.4x) 13.2 ms (0.4x) 14.9 ms (0.4x)
diff() Bitset 14.6 ms (0.3x) 6.1 ms (0.3x) 13.7 ms (0.2x) 6.9 ms (0.3x) 13.0 ms (0.3x) 14.6 ms (0.2x)
xor() Bitset 30.1 ms (0.5x) 12.4 ms (0.6x) 29.2 ms (0.4x) 13.8 ms (0.5x) 26.4 ms (0.5x) 29.1 ms (0.4x)
mergeWith() Int→Int 51.3 ms (1.3x) 25.2 ms (1.3x) 50.5 ms (1.3x) 27.0 ms (1.3x) 50.4 ms (1.3x) 51.7 ms (1.3x)
mergeWith() Str→Int 231.3 ms (1.1x) 138.4 ms (1.1x) 233.5 ms (1.1x) 143.0 ms (~1.0x) 244.5 ms (1.1x) 234.1 ms (1.1x)
forEach() Int→Int 29.4 ms (0.8x) 14.3 ms (0.8x) 27.3 ms (0.8x) 13.2 ms (0.8x) 28.3 ms (0.8x) 27.3 ms (0.9x)
filter() Int→Int 46.0 ms (0.9x) 20.5 ms (0.9x) 42.8 ms (0.9x) 20.8 ms (0.9x) 45.5 ms (0.9x) 43.2 ms (0.9x)
map() Int→Int 48.9 ms (~1.0x) 22.5 ms (1.1x) 48.2 ms (0.9x) 23.7 ms (~1.0x) 49.2 ms (~1.0x) 47.4 ms (~1.0x)
forEach() Str→Int 66.9 ms (0.9x) 43.1 ms (0.8x) 66.5 ms (0.9x) 38.0 ms (0.9x) 65.6 ms (0.9x) 63.9 ms (0.9x)
filter() Str→Int 121.1 ms (0.9x) 70.7 ms (~1.0x) 121.1 ms (0.9x) 79.1 ms (0.9x) 132.7 ms (0.9x) 120.6 ms (0.9x)
map() Str→Int 165.6 ms (~1.0x) 94.4 ms (~1.0x) 161.4 ms (~1.0x) 99.2 ms (0.9x) 166.3 ms (~1.0x) 162.7 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 22.0 ms (0.7x) 11.3 ms (0.8x) 23.7 ms (0.5x) 22.0 ms (0.6x) 22.3 ms (0.5x) 19.3 ms (0.6x)
Bitset Read 17.4 ms (0.4x) 8.7 ms (0.6x) 17.9 ms (0.5x) 17.7 ms (0.5x) 17.9 ms (0.5x) 13.6 ms (0.6x)
Int to Int Write 33.7 ms (0.4x) 19.5 ms (0.4x) 34.8 ms (0.4x) 36.6 ms (0.4x) 35.1 ms (0.4x) 29.1 ms (0.4x)
Int to Int Read 17.4 ms (0.4x) 9.5 ms (0.6x) 19.0 ms (0.4x) 18.8 ms (0.4x) 18.7 ms (0.5x) 13.7 ms (0.5x)
Int to Mixed Write 46.3 ms (0.4x) 25.9 ms (0.4x) 46.4 ms (0.4x) 55.0 ms (0.3x) 51.9 ms (0.3x) 41.4 ms (0.3x)
Int to Mixed Read 20.8 ms (0.5x) 10.1 ms (0.6x) 21.1 ms (0.5x) 21.1 ms (0.5x) 19.8 ms (0.5x) 14.7 ms (0.5x)
Int to Packed Write 65.5 ms (0.3x) 35.9 ms (0.3x) 61.1 ms (0.3x) 71.7 ms (0.2x) 69.0 ms (0.2x) 57.9 ms (0.2x)
Int to Packed Read 47.7 ms (0.2x) 27.1 ms (0.2x) 47.5 ms (0.2x) 50.0 ms (0.2x) 47.4 ms (0.2x) 40.0 ms (0.2x)
String to Int Write 139.0 ms (0.2x) 93.9 ms (0.2x) 143.3 ms (0.2x) 144.9 ms (0.2x) 142.5 ms (0.2x) 118.4 ms (0.2x)
String to Int Read 59.0 ms (0.2x) 40.7 ms (0.2x) 62.9 ms (0.2x) 63.4 ms (0.2x) 60.8 ms (0.2x) 49.9 ms (0.2x)
String to Mixed Write 179.0 ms (0.2x) 126.0 ms (0.2x) 182.8 ms (0.2x) 189.8 ms (0.2x) 191.0 ms (0.1x) 158.0 ms (0.2x)
String to Mixed Read 60.1 ms (0.2x) 43.3 ms (0.2x) 62.9 ms (0.2x) 65.5 ms (0.2x) 63.8 ms (0.2x) 52.3 ms (0.2x)
String to Int (Hash) Write 247.4 ms (0.1x) 171.4 ms (0.1x) 249.3 ms (0.1x) 250.7 ms (0.1x) 255.8 ms (0.1x) 217.7 ms (0.1x)
String to Int (Hash) Read 43.4 ms (0.3x) 34.9 ms (0.2x) 43.9 ms (0.3x) 46.0 ms (0.3x) 44.0 ms (0.3x) 39.3 ms (0.2x)
String to Mixed (Hash) Write 338.3 ms (0.1x) 260.5 ms (0.1x) 347.0 ms (0.1x) 371.2 ms (0.1x) 355.8 ms (0.1x) 319.5 ms (0.1x)
String to Mixed (Hash) Read 44.3 ms (0.3x) 40.2 ms (0.3x) 46.1 ms (0.3x) 49.8 ms (0.3x) 47.0 ms (0.3x) 43.7 ms (0.3x)
String to Int (Adaptive) Write 241.9 ms (0.1x) 175.3 ms (0.1x) 244.1 ms (0.1x) 245.6 ms (0.1x) 242.6 ms (0.1x) 209.8 ms (0.1x)
String to Int (Adaptive) Read 42.6 ms (0.3x) 35.1 ms (0.2x) 43.6 ms (0.3x) 45.7 ms (0.3x) 43.8 ms (0.3x) 38.6 ms (0.2x)
String to Mixed (Adaptive) Write 348.3 ms (0.1x) 286.9 ms (0.1x) 357.3 ms (0.1x) 386.5 ms (0.1x) 381.0 ms (0.1x) 336.6 ms (0.1x)
String to Mixed (Adaptive) Read 45.3 ms (0.3x) 38.7 ms (0.3x) 46.8 ms (0.3x) 48.2 ms (0.3x) 47.5 ms (0.3x) 42.4 ms (0.3x)

API — Windows

Operation PHP 8.1 PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5 PHP 8.6
putAll() 30.1 ms (1.5x) 19.7 ms (1.5x) 29.1 ms (1.6x) 31.9 ms (1.6x) 31.3 ms (1.5x) 26.8 ms (1.4x)
fromArray() 29.3 ms (1.6x) 20.6 ms (1.4x) 28.6 ms (1.6x) 31.6 ms (1.6x) 30.8 ms (1.5x) 27.0 ms (1.4x)
toArray() 27.6 ms (1.7x) 21.3 ms (1.6x) 29.5 ms (1.7x) 28.4 ms (1.7x) 27.9 ms (1.7x) 22.9 ms (1.6x)
getAll() 1.3 ms (2.3x) 0.89 ms (2.2x) 1.4 ms (2.4x) 1.7 ms (2.0x) 1.6 ms (1.9x) 1.4 ms (1.6x)
keys() 16.0 ms (2.3x) 8.1 ms (2.3x) 13.3 ms (2.4x) 12.9 ms (2.5x) 13.1 ms (2.4x) 10.7 ms (2.3x)
values() 16.4 ms (2.1x) 8.6 ms (2.3x) 13.8 ms (2.4x) 13.3 ms (2.4x) 13.1 ms (2.4x) 10.6 ms (2.3x)
sumValues() 7.1 ms (3.4x) 3.8 ms (3.4x) 8.4 ms (3.2x) 7.2 ms (3.4x) 7.3 ms (3.6x) 5.8 ms (3.5x)
populationCount() 0.00 ms (245167x) 0.00 ms (135424x) 0.00 ms (259746x) 0.00 ms (131002x) 0.00 ms (259221x) 0.00 ms (216288x)
deleteRange() 52.9 ms (1.5x) 31.3 ms (1.5x) 54.5 ms (1.5x) 56.0 ms (1.5x) 57.3 ms (1.4x) 46.4 ms (1.4x)
equals() 10.9 ms (5.9x) 6.7 ms (4.8x) 11.2 ms (5.7x) 11.3 ms (6.0x) 11.3 ms (5.5x) 8.9 ms (5.0x)
union() Bitset 35.4 ms (0.4x) 17.9 ms (0.3x) 35.7 ms (0.2x) 35.1 ms (0.3x) 36.7 ms (0.2x) 30.4 ms (0.2x)
intersect() Bitset 16.4 ms (0.5x) 7.7 ms (0.7x) 16.2 ms (0.5x) 16.7 ms (0.5x) 17.2 ms (0.5x) 14.5 ms (0.4x)
diff() Bitset 16.1 ms (0.4x) 7.8 ms (0.4x) 16.0 ms (0.3x) 16.5 ms (0.3x) 17.2 ms (0.3x) 14.1 ms (0.3x)
xor() Bitset 32.5 ms (0.8x) 15.7 ms (0.9x) 32.3 ms (0.5x) 33.0 ms (0.6x) 34.6 ms (0.5x) 28.4 ms (0.5x)
mergeWith() Int→Int 63.2 ms (1.3x) 37.7 ms (1.3x) 64.3 ms (1.4x) 64.2 ms (1.4x) 64.8 ms (1.3x) 54.5 ms (1.3x)
mergeWith() Str→Int 313.6 ms (1.1x) 220.6 ms (1.1x) 310.4 ms (1.1x) 336.0 ms (1.1x) 336.3 ms (1.1x) 284.3 ms (1.1x)
forEach() Int→Int 28.8 ms (~1.0x) 15.4 ms (~1.0x) 29.0 ms (~1.0x) 29.7 ms (~1.0x) 35.5 ms (0.8x) 24.7 ms (0.9x)
filter() Int→Int 43.6 ms (1.1x) 26.0 ms (1.1x) 46.7 ms (1.1x) 48.3 ms (~1.0x) 50.0 ms (~1.0x) 36.4 ms (1.1x)
map() Int→Int 50.8 ms (1.1x) 28.5 ms (1.1x) 53.4 ms (1.2x) 53.2 ms (1.1x) 53.6 ms (1.2x) 43.2 ms (1.1x)
forEach() Str→Int 76.0 ms (~1.0x) 52.1 ms (1.1x) 76.7 ms (~1.0x) 83.2 ms (~1.0x) 85.7 ms (0.9x) 71.5 ms (~1.0x)
filter() Str→Int 153.6 ms (~1.0x) 104.4 ms (~1.0x) 155.6 ms (~1.0x) 161.4 ms (~1.0x) 159.2 ms (~1.0x) 131.9 ms (~1.0x)
map() Str→Int 208.9 ms (~1.0x) 144.3 ms (1.1x) 216.2 ms (~1.0x) 219.9 ms (~1.0x) 222.9 ms (~1.0x) 184.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 160.0 KB 9.2 MB 59.0x smaller
Int to Int 4.5 MB 9.2 MB 2.1x smaller
Int to Mixed 42.6 MB 38.3 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 69.0 MB 1.4x smaller
String to Int (Hash) 32.2 MB 38.0 MB 1.2x smaller
String to Mixed (Hash) 70.5 MB 69.0 MB about the same
String to Int (Adaptive) 32.2 MB 38.0 MB 1.2x smaller
String to Mixed (Adaptive) 70.5 MB 69.0 MB about the same

API Speedups (Windows, PHP 8.6)

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

Operation Judy PHP Speedup
putAll() 26.8 ms 36.5 ms 1.4x
fromArray() 27.0 ms 36.5 ms 1.4x
toArray() 22.9 ms 37.4 ms 1.6x
getAll() 1.4 ms 2.3 ms 1.6x
keys() 10.7 ms 24.5 ms 2.3x
values() 10.6 ms 24.7 ms 2.3x
sumValues() 5.8 ms 20.2 ms 3.5x
populationCount() 0.00 ms 21.6 ms O(1) vs O(n)
deleteRange() 46.4 ms 66.6 ms 1.4x
equals() 8.9 ms 44.6 ms 5.0x
union() Bitset 30.4 ms 7.2 ms 0.2x
intersect() Bitset 14.5 ms 6.3 ms 0.4x
diff() Bitset 14.1 ms 3.5 ms 0.3x
xor() Bitset 28.4 ms 14.5 ms 0.5x
mergeWith() Int→Int 54.5 ms 70.8 ms 1.3x
mergeWith() Str→Int 284.3 ms 299.0 ms 1.1x
forEach() Int→Int 24.7 ms 23.3 ms 0.9x
filter() Int→Int 36.4 ms 38.7 ms 1.1x
map() Int→Int 43.2 ms 46.2 ms 1.1x
forEach() Str→Int 71.5 ms 69.1 ms ~1.0x
filter() Str→Int 131.9 ms 134.5 ms ~1.0x
map() Str→Int 184.0 ms 182.8 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 19.3 ms 10.8 ms 0.6x
Bitset Read 13.6 ms 7.6 ms 0.6x
Bitset Iter 21.0 ms 5.3 ms 0.3x
Bitset Free 0.01 ms 0.45 ms 59.4x
Int to Int Write 29.1 ms 10.7 ms 0.4x
Int to Int Read 13.7 ms 7.0 ms 0.5x
Int to Int Iter 19.1 ms 5.3 ms 0.3x
Int to Int Free 0.53 ms 0.45 ms 0.9x
Int to Mixed Write 41.4 ms 12.7 ms 0.3x
Int to Mixed Read 14.7 ms 7.4 ms 0.5x
Int to Mixed Iter 19.7 ms 5.6 ms 0.3x
Int to Mixed Free 17.8 ms 5.3 ms 0.3x
String to Int Write 118.4 ms 20.7 ms 0.2x
String to Int Read 49.9 ms 9.4 ms 0.2x
String to Int Iter 62.2 ms 6.1 ms 0.1x
String to Int Free 27.0 ms 1.4 ms 0.1x
String to Int (Adaptive) Write 209.8 ms 20.5 ms 0.1x
String to Int (Adaptive) Read 38.6 ms 9.1 ms 0.2x
String to Int (Adaptive) Iter 99.4 ms 6.1 ms 0.1x
String to Int (Adaptive) Free 56.3 ms 1.5 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 19.3 ms 10.8 ms 0.6x
Bitset Read 13.6 ms 7.6 ms 0.6x
Bitset Iter 21.0 ms 5.3 ms 0.3x
Bitset Free 0.01 ms 0.45 ms 59.4x
Int to Int Write 29.1 ms 10.7 ms 0.4x
Int to Int Read 13.7 ms 7.0 ms 0.5x
Int to Int Iter 19.1 ms 5.3 ms 0.3x
Int to Int Free 0.53 ms 0.45 ms 0.9x
Int to Mixed Write 41.4 ms 12.7 ms 0.3x
Int to Mixed Read 14.7 ms 7.4 ms 0.5x
Int to Mixed Iter 19.7 ms 5.6 ms 0.3x
Int to Mixed Free 17.8 ms 5.3 ms 0.3x
Int to Packed Write 57.9 ms 12.8 ms 0.2x
Int to Packed Read 40.0 ms 7.4 ms 0.2x
Int to Packed Iter 43.5 ms 5.8 ms 0.1x
Int to Packed Free 13.1 ms 5.2 ms 0.4x
String to Int Write 118.4 ms 20.7 ms 0.2x
String to Int Read 49.9 ms 9.4 ms 0.2x
String to Int Iter 62.2 ms 6.1 ms 0.1x
String to Int Free 27.0 ms 1.4 ms 0.1x
String to Mixed Write 158.0 ms 24.2 ms 0.2x
String to Mixed Read 52.3 ms 11.5 ms 0.2x
String to Mixed Iter 78.3 ms 7.4 ms 0.1x
String to Mixed Free 89.4 ms 8.0 ms 0.1x
String to Int (Hash) Write 217.7 ms 20.0 ms 0.1x
String to Int (Hash) Read 39.3 ms 9.4 ms 0.2x
String to Int (Hash) Iter 101.1 ms 6.1 ms 0.1x
String to Int (Hash) Free 56.5 ms 1.4 ms 0.0x
String to Mixed (Hash) Write 319.5 ms 24.9 ms 0.1x
String to Mixed (Hash) Read 43.7 ms 11.2 ms 0.3x
String to Mixed (Hash) Iter 130.5 ms 7.4 ms 0.1x
String to Mixed (Hash) Free 173.4 ms 7.9 ms 0.0x
String to Int (Adaptive) Write 209.8 ms 20.5 ms 0.1x
String to Int (Adaptive) Read 38.6 ms 9.1 ms 0.2x
String to Int (Adaptive) Iter 99.4 ms 6.1 ms 0.1x
String to Int (Adaptive) Free 56.3 ms 1.5 ms 0.0x
String to Mixed (Adaptive) Write 336.6 ms 25.0 ms 0.1x
String to Mixed (Adaptive) Read 42.4 ms 11.4 ms 0.3x
String to Mixed (Adaptive) Iter 132.7 ms 7.4 ms 0.1x
String to Mixed (Adaptive) Free 185.0 ms 7.9 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.29% (PHP-array control +0.25%); runner common-mode +0.25% divided out of every row.

Summary: 0 regressions, 84 unchanged, 16 not evaluated (unstable)

Benchmark Baseline Current Delta 95% CI Status
adv.filter.int_to_int 15.1 ms 14.5 ms -1.4% [-10.0%, +2.1%] ~same
adv.filter.string_to_int 45.2 ms 45.1 ms -1.2% [-11.5%, +2.7%] ~same
adv.forEach.string_to_int 24.8 ms 25.2 ms +0.9% [-10.4%, +12.7%] ~same
adv.map.int_to_int 15.3 ms 15.1 ms -3.3% [-16.9%, +6.4%] ~same
adv.map.string_to_int 60.8 ms 60.2 ms -2.3% [-7.0%, +1.9%] ~same
adv.optiter.foreach.string_to_int_adaptive.default 54.0 ms 53.9 ms +4.6% [-11.6%, +17.5%] ~same
adv.optiter.foreach.string_to_int_adaptive.optimized 34.4 ms 33.3 ms -1.2% [-6.0%, +8.7%] ~same
adv.optiter.foreach.string_to_int_hash.optimized 26.5 ms 26.8 ms +0.5% [-3.2%, +2.3%] ~same
adv.optiter.increment.string_to_int_hash.optimized 39.0 ms 42.2 ms +15.2% [-13.7%, +16.3%] ~same
adv.optiter.ratio.foreach.string_to_int_adaptive 63.7 ms 65.0 ms -5.1% [-10.3%, +11.4%] ~same
adv.optiter.ratio.foreach.string_to_int_hash 56.6 ms 50.4 ms -5.9% [-12.6%, +1.9%] ~same
adv.optiter.ratio.overwrite.string_to_int_adaptive 94.5 ms 91.3 ms -3.6% [-17.8%, +16.1%] ~same
adv.optiter.ratio.toArray.string_to_int_adaptive 63.2 ms 58.6 ms -4.4% [-19.3%, +10.2%] ~same
adv.optiter.ratio.values.string_to_int_adaptive 59.3 ms 60.0 ms +1.8% [-11.1%, +14.7%] ~same
adv.optiter.ratio.values.string_to_int_hash 47.9 ms 48.4 ms -4.8% [-5.9%, +4.8%] ~same
adv.optiter.toArray.string_to_int_adaptive.optimized 37.7 ms 37.6 ms -0.2% [-5.0%, +7.8%] ~same
adv.optiter.toArray.string_to_int_hash.optimized 26.9 ms 26.8 ms -0.6% [-9.1%, +4.4%] ~same
adv.optiter.values.string_to_int_adaptive.optimized 29.6 ms 29.2 ms -4.6% [-5.2%, +4.9%] ~same
adv.optiter.values.string_to_int_hash.default 46.3 ms 45.3 ms -0.9% [-4.5%, +10.1%] ~same
adv.optiter.values.string_to_int_hash.optimized 22.0 ms 21.9 ms -1.4% [-8.4%, +4.5%] ~same
api.equals.int_to_int 3.2 ms 3.5 ms +6.1% [-1.7%, +16.1%] ~same
api.fromArray.int_to_int 7.5 ms 7.5 ms +0.5% [-1.8%, +2.3%] ~same
api.getAll.int_to_int 0.28 ms 0.29 ms +0.6% [-1.8%, +2.8%] ~same
api.increment.int_to_int 7.0 ms 7.2 ms +1.5% [+0.7%, +6.7%] ~same
api.keys.int_to_int 3.1 ms 3.0 ms -1.2% [-3.6%, +4.6%] ~same
api.mergeWith.int_to_int 15.7 ms 16.1 ms +1.3% [-0.6%, +2.9%] ~same
api.mergeWith.string_to_int 81.6 ms 82.9 ms +1.2% [+0.1%, +8.9%] ~same
api.populationCount.int_to_int 0.00 ms 0.00 ms -0.2% [-0.2%, -0.2%] ~same
api.putAll.int_to_int 7.4 ms 7.4 ms +0.5% [-1.2%, +5.2%] ~same
api.range.keys.int_to_int 0.27 ms 0.26 ms -3.5% [-8.6%, +8.0%] ~same
api.range.size.string_to_int 1.5 ms 1.5 ms +1.5% [-0.4%, +4.7%] ~same
api.range.size1000.int_to_int 0.36 ms 0.39 ms +5.0% [-11.8%, +25.7%] ~same
api.setop.diff.bitset 3.7 ms 3.8 ms +2.2% [-3.0%, +13.1%] ~same
api.setop.diff.int_to_int 4.7 ms 4.9 ms +3.5% [-4.1%, +4.6%] ~same
api.setop.diff.string_to_int 41.2 ms 41.8 ms +0.2% [-10.0%, +5.8%] ~same
api.setop.intersect.bitset 3.7 ms 3.8 ms +5.6% [+1.0%, +7.3%] ~same
api.setop.intersect.int_to_int 4.7 ms 4.7 ms -0.1% [-3.4%, +6.1%] ~same
api.setop.intersect.string_to_int 44.4 ms 46.8 ms +2.0% [-1.0%, +15.9%] ~same
api.setop.union.bitset 9.0 ms 9.1 ms +2.7% [-1.2%, +5.4%] ~same
api.setop.union.int_to_int 12.8 ms 12.6 ms -2.0% [-7.0%, +3.8%] ~same
api.setop.union.string_to_int 102.4 ms 104.6 ms +0.7% [-0.3%, +8.5%] ~same
api.setop.xor.bitset 7.4 ms 7.8 ms +3.2% [-0.6%, +11.1%] ~same
api.setop.xor.int_to_int 9.6 ms 9.6 ms -1.4% [-15.2%, +2.5%] ~same
api.sumValues.int_to_int 2.0 ms 1.9 ms -2.5% [-7.2%, +10.8%] ~same
api.toArray.int_to_int 8.0 ms 8.2 ms +2.0% [+0.3%, +4.8%] ~same
api.values.int_to_int 3.2 ms 3.0 ms -4.5% [-10.0%, +10.9%] ~same
core.bitset.free 0.00 ms 0.00 ms +18.8% [+4.3%, +29.7%] ~same
core.bitset.iter 5.0 ms 5.4 ms +7.8% [-0.5%, +10.9%] ~same
core.bitset.read 3.2 ms 3.4 ms +3.5% [-3.9%, +10.0%] ~same
core.int_to_int.free 0.19 ms 0.19 ms +2.2% [-16.7%, +4.3%] ~same
core.int_to_int.iter 5.3 ms 5.2 ms -2.8% [-8.2%, +5.1%] ~same
core.int_to_int.read 3.5 ms 3.8 ms +11.6% [-2.8%, +14.7%] ~same
core.int_to_int.write 7.7 ms 8.0 ms +3.5% [-0.9%, +7.0%] ~same
core.int_to_mixed.free 4.4 ms 4.0 ms -3.8% [-15.6%, +2.6%] ~same
core.int_to_mixed.iter 5.3 ms 5.5 ms +0.8% [-5.9%, +3.7%] ~same
core.int_to_mixed.read 4.2 ms 4.3 ms +1.8% [-1.4%, +4.9%] ~same
core.int_to_mixed.write 10.3 ms 10.4 ms -0.7% [-4.3%, +3.6%] ~same
core.int_to_packed.free 2.4 ms 2.4 ms -1.5% [-5.9%, +4.8%] ~same
core.int_to_packed.iter 10.4 ms 10.4 ms -1.0% [-6.0%, +0.1%] ~same
core.int_to_packed.read 8.8 ms 9.1 ms +1.4% [+0.9%, +7.6%] ~same
core.int_to_packed.write 14.2 ms 13.8 ms -3.0% [-4.7%, -0.9%] ~same
core.string_to_int.free 6.8 ms 6.9 ms +1.6% [-4.4%, +3.9%] ~same
core.string_to_int.iter 19.8 ms 19.5 ms -1.8% [-15.9%, +5.8%] ~same
core.string_to_int.read 14.0 ms 13.5 ms -3.7% [-6.3%, +0.1%] ~same
core.string_to_int.write 30.4 ms 31.2 ms +2.1% [-2.8%, +6.1%] ~same
core.string_to_int_adaptive.free 14.3 ms 13.4 ms -5.6% [-10.7%, -4.3%] ~same
core.string_to_int_adaptive.read 8.3 ms 8.5 ms +2.6% [-9.5%, +5.6%] ~same
core.string_to_int_adaptive.write 51.6 ms 50.2 ms -1.8% [-8.1%, +2.9%] ~same
core.string_to_int_hash.free 13.2 ms 13.3 ms +0.8% [-6.4%, +19.1%] ~same
core.string_to_int_hash.iter 24.5 ms 24.4 ms -0.9% [-1.6%, +2.1%] ~same
core.string_to_int_hash.read 8.6 ms 8.9 ms +3.6% [-4.2%, +7.5%] ~same
core.string_to_int_hash.write 51.0 ms 51.8 ms -0.2% [-4.9%, +3.0%] ~same
core.string_to_mixed.free 22.4 ms 22.6 ms +0.8% [-1.6%, +6.7%] ~same
core.string_to_mixed.iter 21.9 ms 22.2 ms -0.8% [-2.7%, +10.0%] ~same
core.string_to_mixed.read 15.4 ms 15.5 ms +0.7% [-3.7%, +2.0%] ~same
core.string_to_mixed.write 39.0 ms 37.9 ms -0.7% [-9.5%, +6.7%] ~same
core.string_to_mixed_adaptive.free 47.4 ms 42.9 ms -6.1% [-11.4%, -3.4%] ~same
core.string_to_mixed_adaptive.iter 31.0 ms 29.9 ms -5.2% [-17.9%, +5.3%] ~same
core.string_to_mixed_adaptive.read 10.5 ms 10.2 ms -3.2% [-8.4%, +3.3%] ~same
core.string_to_mixed_adaptive.write 84.6 ms 81.3 ms -4.3% [-9.6%, +1.9%] ~same
core.string_to_mixed_hash.free 42.4 ms 42.1 ms +1.4% [-2.4%, +14.2%] ~same
core.string_to_mixed_hash.iter 35.6 ms 35.6 ms 0.0% [-4.0%, +5.1%] ~same
core.string_to_mixed_hash.read 10.7 ms 10.4 ms -5.1% [-7.2%, +3.7%] ~same
core.string_to_mixed_hash.write 74.7 ms 73.9 ms -1.4% [-3.5%, +3.9%] ~same
16 benchmarks not evaluated (within-arm scatter exceeded the effect being claimed)
Benchmark Baseline Current Delta 95% CI Status
adv.forEach.int_to_int 8.8 ms 8.9 ms +0.1% [-6.8%, +80.4%] — unstable
adv.optiter.foreach.string_to_int_hash.default 46.8 ms 52.8 ms +5.7% [-5.2%, +15.3%] — unstable
adv.optiter.increment.string_to_int_hash.default 47.8 ms 41.7 ms -9.1% [-27.8%, +2.0%] — unstable
adv.optiter.overwrite.string_to_int_adaptive.default 51.7 ms 51.7 ms -0.2% [-18.4%, +16.6%] — unstable
adv.optiter.overwrite.string_to_int_adaptive.optimized 48.8 ms 44.5 ms +4.2% [-32.7%, +20.0%] — unstable
adv.optiter.overwrite.string_to_int_hash.default 38.7 ms 50.2 ms +9.5% [-21.1%, +50.6%] — unstable
adv.optiter.overwrite.string_to_int_hash.optimized 39.0 ms 43.7 ms +5.7% [-9.7%, +59.2%] — unstable
adv.optiter.ratio.increment.string_to_int_hash 87.9 ms 90.6 ms +16.7% [-2.6%, +28.6%] — unstable
adv.optiter.ratio.overwrite.string_to_int_hash 91.4 ms 88.7 ms +2.6% [-36.6%, +14.2%] — unstable
adv.optiter.ratio.toArray.string_to_int_hash 49.2 ms 47.8 ms -3.1% [-5.2%, +42.6%] — unstable
adv.optiter.toArray.string_to_int_adaptive.default 59.6 ms 62.0 ms +3.9% [-14.1%, +23.5%] — unstable
adv.optiter.toArray.string_to_int_hash.default 56.0 ms 56.4 ms +2.8% [-36.4%, +8.8%] — unstable
adv.optiter.values.string_to_int_adaptive.default 49.7 ms 48.8 ms -6.6% [-17.4%, +7.0%] — unstable
api.deleteRange.int_to_int 12.0 ms 12.3 ms +1.9% [-1.9%, +66.3%] — unstable
core.bitset.write 5.4 ms 5.3 ms -0.3% [-7.3%, +76.6%] — unstable
core.string_to_int_adaptive.iter 24.1 ms 24.2 ms +3.2% [-2.8%, +37.0%] — unstable

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                           18.28         11.90         20.22          0.01    < 376.0 KB
    PHP array                       7.10          3.94          3.01          0.31        8.0 MB
  INT_TO_INT                       24.01         11.68         21.51          0.19        4.2 MB
    PHP array                       7.24          4.68          3.01          0.32        7.9 MB
  INT_TO_MIXED                     34.47         13.77         21.93         14.76       40.2 MB
    PHP array                      10.54          5.55          4.01          3.97       38.0 MB
  INT_TO_PACKED                    48.00         29.95         40.26          9.89       16.3 MB
    PHP array                      10.57          5.79          3.62          3.92       38.0 MB

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

  Type                         Write(ms)      Read(ms)      Iter(ms)      Free(ms)        Memory
  ────────────────────────  ────────────  ────────────  ────────────  ────────────  ────────────
  STRING_TO_INT                    93.27         36.64         56.41         18.28       10.1 MB
    PHP array                      16.37          7.42          4.86          1.17       38.0 MB
  STRING_TO_MIXED                 116.01         40.58         65.94         63.71       46.3 MB
    PHP array                      19.28          9.56          6.63          6.84       68.0 MB
  STR_TO_INT_HASH                 161.10         26.29         67.63         40.63       31.4 MB
    PHP array                      16.22          7.43          4.86          1.15       38.0 MB
  STR_TO_MIX_HASH                 233.85         27.83         88.01        120.03       67.4 MB
    PHP array                      19.71          9.53          6.20          6.73       68.1 MB
  STR_INT_ADAPTIVE                161.56         24.85         67.19         40.59       31.5 MB
    PHP array                      16.17          7.12          4.88          1.13       38.1 MB
  STR_MIX_ADAPTIVE                234.09         27.61         87.37        117.66       67.4 MB
    PHP array                      19.26          9.53          6.24          6.73       68.1 MB

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

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  putAll()                               19.84         29.28        1.5x
  fromArray()                            20.00         29.28        1.5x
  toArray()                              22.66         35.63        1.6x
  getAll(50100 keys)                      1.13          2.10        1.9x
  increment(500000 ops/1000 keys)         21.88         11.07        0.5x

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

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  keys() INT_TO_INT                      12.00         24.65        2.1x
  values() INT_TO_INT                    12.24         24.81        2.0x
  keys($lo,$hi) INT_TO_INT                1.06         26.08       24.6x
    vs slice()->keys()                    2.96             -        2.8x
  size($lo,$hi) INT x1000                 0.73             -        O(1)
  size($lo,$hi) STRING_TO_INT             3.84          5.13        1.3x
  sumValues() INT_TO_INT                  8.62         19.84        2.3x
  populationCount() INT_TO_INT            0.00         21.69   309902.7x
  deleteRange() INT_TO_INT mid50%         38.96         56.89        1.5x
  equals() INT_TO_INT                    12.14         50.22        4.1x

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

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  union() BITSET                         32.02          4.73        0.1x
  intersect() BITSET                     14.92          5.61        0.4x
  diff() BITSET                          14.60          3.32        0.2x
  xor() BITSET                           29.13         11.17        0.4x
  union() INT_TO_INT                     43.51         87.17        2.0x
  intersect() INT_TO_INT                 17.73         43.06        2.4x
  diff() INT_TO_INT                      17.48         44.65        2.6x
  xor() INT_TO_INT                       35.24         89.51        2.5x
  union() STRING_TO_INT                 273.63        311.17        1.1x
  intersect() STRING_TO_INT             113.99        137.30        1.2x
  diff() STRING_TO_INT                  110.39        134.09        1.2x
  mergeWith() INT_TO_INT                 51.75         67.48        1.3x
  mergeWith() STRING_TO_INT             234.05        252.68        1.1x

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

  Operation                        C-level(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  forEach() INT_TO_INT                   27.33         23.87        0.9x
  forEach() STRING_TO_INT                63.95         58.82        0.9x
  filter() INT_TO_INT (50% pass)         43.21         40.40        0.9x
  filter() STRING_TO_INT (50% pass)        120.57        111.58        0.9x
  map() INT_TO_INT (*2)                  47.38         47.21        1.0x
  map() STRING_TO_INT (*2)              162.66        155.36        1.0x

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

  Operation                        default(ms)    optim.(ms)   optim./def.
  ──────────────────────────────  ────────────  ────────────  ────────────
  foreach() HASH kl16                    44.83         32.04         71.5%
  values() HASH kl16                     40.33         23.09         57.3%
  toArray() HASH kl16                    53.38         32.61         61.1%
  overwrite() HASH kl16                  30.59         34.94        114.2%
  increment() HASH kl16                  30.80         35.26        114.5%
    HASH kl16: honoured?          yes
  foreach() ADAPTIVE kl40                55.80         39.82         71.4%
  values() ADAPTIVE kl40                 48.79         31.11         63.8%
  toArray() ADAPTIVE kl40                64.77         44.00         67.9%
  overwrite() ADAPTIVE kl40              49.50         43.22         87.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)          73.40 ms      122.35 ms      112.79 ms       1.7x
  read (random access)         13.09 ms       12.62 ms       12.69 ms       1.0x
  foreach (iterate all)        48.16 ms       80.06 ms       82.31 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)          120.94 ms      207.61 ms      206.95 ms       1.7x
  read (random access)         32.54 ms       22.78 ms       24.61 ms       1.4x
  foreach (iterate all)        84.76 ms       95.99 ms       94.77 ms       1.1x

══════════════════════════════════════════════════════════════════════════════════════════
  Benchmark complete — 2026-10-07 08:46:50
  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                           19.35         13.61         20.95          0.01      160.0 KB
    PHP array                      10.79          7.55          5.35          0.45        9.2 MB
  INT_TO_INT                       29.08         13.73         19.07          0.53        4.5 MB
    PHP array                      10.67          7.01          5.35          0.45        9.2 MB
  INT_TO_MIXED                     41.39         14.71         19.70         17.75       42.6 MB
    PHP array                      12.69          7.40          5.57          5.30       38.3 MB
  INT_TO_PACKED                    57.86         39.98         43.50         13.06       17.2 MB
    PHP array                      12.79          7.37          5.77          5.21       38.2 MB

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

  Type                         Write(ms)      Read(ms)      Iter(ms)      Free(ms)        Memory
  ────────────────────────  ────────────  ────────────  ────────────  ────────────  ────────────
  STRING_TO_INT                   118.39         49.91         62.25         27.04       10.5 MB
    PHP array                      20.71          9.38          6.08          1.40       38.0 MB
  STRING_TO_MIXED                 157.97         52.35         78.30         89.45       48.7 MB
    PHP array                      24.18         11.50          7.41          7.97       69.0 MB
  STR_TO_INT_HASH                 217.74         39.27        101.12         56.53       32.2 MB
    PHP array                      19.99          9.38          6.10          1.42       38.0 MB
  STR_TO_MIX_HASH                 319.50         43.66        130.51        173.45       70.5 MB
    PHP array                      24.87         11.21          7.39          7.93       69.0 MB
  STR_INT_ADAPTIVE                209.76         38.57         99.38         56.35       32.2 MB
    PHP array                      20.49          9.10          6.12          1.50       38.0 MB
  STR_MIX_ADAPTIVE                336.60         42.45        132.70        184.98       70.5 MB
    PHP array                      25.04         11.43          7.39          7.94       69.0 MB

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

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  putAll()                               26.82         36.45        1.4x
  fromArray()                            27.00         36.45        1.4x
  toArray()                              22.95         37.42        1.6x
  getAll(50100 keys)                      1.41          2.30        1.6x
  increment(500000 ops/1000 keys)         25.69         18.78        0.7x

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

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  keys() INT_TO_INT                      10.71         24.52        2.3x
  values() INT_TO_INT                    10.60         24.74        2.3x
  keys($lo,$hi) INT_TO_INT                0.76         26.04       34.2x
    vs slice()->keys()                    2.76             -        3.6x
  size($lo,$hi) INT x1000                 0.59             -        O(1)
  size($lo,$hi) STRING_TO_INT             4.46          5.85        1.3x
  sumValues() INT_TO_INT                  5.81         20.22        3.5x
  populationCount() INT_TO_INT            0.00         21.63   216288.0x
  deleteRange() INT_TO_INT mid50%         46.39         66.55        1.4x
  equals() INT_TO_INT                     8.91         44.61        5.0x

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

  Operation                           Judy(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  union() BITSET                         30.43          7.16        0.2x
  intersect() BITSET                     14.47          6.34        0.4x
  diff() BITSET                          14.05          3.55        0.3x
  xor() BITSET                           28.44         14.53        0.5x
  union() INT_TO_INT                     45.71         85.45        1.9x
  intersect() INT_TO_INT                 16.69         39.88        2.4x
  diff() INT_TO_INT                      16.65         42.99        2.6x
  xor() INT_TO_INT                       33.73         86.17        2.6x
  union() STRING_TO_INT                 323.49        355.99        1.1x
  intersect() STRING_TO_INT             149.72        170.76        1.1x
  diff() STRING_TO_INT                  137.77        158.70        1.2x
  mergeWith() INT_TO_INT                 54.45         70.83        1.3x
  mergeWith() STRING_TO_INT             284.28        299.01        1.1x

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

  Operation                        C-level(ms)       PHP(ms)     Speedup
  ──────────────────────────────  ────────────  ────────────  ──────────
  forEach() INT_TO_INT                   24.70         23.29        0.9x
  forEach() STRING_TO_INT                71.51         69.12        1.0x
  filter() INT_TO_INT (50% pass)         36.40         38.71        1.1x
  filter() STRING_TO_INT (50% pass)        131.91        134.50        1.0x
  map() INT_TO_INT (*2)                  43.25         46.17        1.1x
  map() STRING_TO_INT (*2)              183.97        182.85        1.0x

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

  Operation                        default(ms)    optim.(ms)   optim./def.
  ──────────────────────────────  ────────────  ────────────  ────────────
  foreach() HASH kl16                    70.08         47.53         67.8%
  values() HASH kl16                     61.82         37.56         60.8%
  toArray() HASH kl16                    79.13         55.32         69.9%
  overwrite() HASH kl16                  50.26         56.89        113.2%
  increment() HASH kl16                  52.61         60.12        114.3%
    HASH kl16: honoured?          yes
  foreach() ADAPTIVE kl40               103.04         44.70         43.4%
  values() ADAPTIVE kl40                 91.69         34.69         37.8%
  toArray() ADAPTIVE kl40               111.60         55.26         49.5%
  overwrite() ADAPTIVE kl40              69.01         84.61        122.6%
    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)          88.80 ms      154.16 ms      139.24 ms       1.7x
  read (random access)         16.50 ms       18.44 ms       16.83 ms       1.1x
  foreach (iterate all)        54.33 ms       98.87 ms       95.89 ms       1.8x

── 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)          148.90 ms      270.36 ms      271.60 ms       1.8x
  read (random access)         37.56 ms       28.39 ms       28.03 ms       1.3x
  foreach (iterate all)       107.53 ms      163.30 ms      163.84 ms       1.5x

══════════════════════════════════════════════════════════════════════════════════════════
  Benchmark complete — 2026-10-07 08:48:17
  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 a91c8ca into main Oct 7, 2026
39 checks passed
@orieg
orieg deleted the craftsmanship-hardening branch October 7, 2026 15:38
orieg added a commit that referenced this pull request Oct 8, 2026
…d nightly (#223)

The secondary assertion in tests/string_to_entry_delete_range_001.phpt
measured a memory_get_usage() delta around deleteRange() and required it
to be > 0. Under valgrind memcheck the delta came out 0: freed zvals land
on the Zend MM freelist, and memory_get_usage() does not reliably drop,
while the Judy trie (and its held zvals) is invisible to it entirely.
The nightly valgrind job failed its first post-#217 run on this (issue
#222), even though the primary leak assertion -- __destruct fires 1000/1000
-- passed and valgrind reported 0 definite leaks.

Replace the reading with Judy::memoryUsage(), the extension's own
string-keyed accounting (approx_payload_bytes), which deleteRange()
decrements by every confirmed removal and which is independent of the
allocator: 38000 -> 0 deterministically on this test. Test identity
(--TEST-- title, file name) is unchanged, so the test-floor ratchet
baseline in reports/junit.xml still matches.
@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.

Trie types silently truncate keys at an embedded NUL; the other four string types throw

1 participant