Add object-identity short-circuit to isEqualTo() and compareTo() - #130
Add object-identity short-circuit to isEqualTo() and compareTo()#130gnutix wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #130 +/- ##
=========================================
Coverage 99.30% 99.31%
- Complexity 1109 1134 +25
=========================================
Files 48 48
Lines 2445 2468 +23
=========================================
+ Hits 2428 2451 +23
Misses 17 17 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
027583f to
e17de00
Compare
Benchmark harness (for inspection — not proposed for merge)As noted in the PR description, I kept these files out of the branch so nothing extra lands in the package. Posting them here so you can read and run them. Two files. Drop them in a No Composer/PHPBench dependency: the script ships its own PSR-4 autoloader so the same benchmark runs against two different
|
The date-time value types are deeply immutable, and objects frequently get compared against themselves (a boundary reused across a derived range, a value carried unchanged through a map/filter, the same reference passed twice). In those cases an identity check is far cheaper than walking the full value comparison. Add `$this === $that` as a sufficient-condition fast path: - isEqualTo(): return true up-front for the same instance; - compareTo(): return 0 up-front for the same instance. The relational helpers (isBefore/isAfter/...) delegate to compareTo(), so they inherit the fast path and stay correct for the identity case (0 -> not before, not after, but before-or-equal / after-or-equal). Safety invariant: identity only ever adds a fast true/0, never decides a false. When identity fails, the code falls through to the full value comparison, so distinct-but-equal instances (unserialized, hydrated, separately parsed) still get the correct answer. This relies only on reflexivity, which every sane comparison satisfies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HmwANBW3Ve98SXoADwAct6
e17de00 to
28b731f
Compare
What
Add an object-identity fast path to the comparison methods of the immutable
value types:
Applied to
isEqualTo()inDuration,Instant,Interval,LocalDate,LocalDateRange,LocalDateTime,LocalTime,MonthDay,Period,TimeZone,Year,YearMonth,YearMonthRange,YearWeek,ZonedDateTime, and tocompareTo()wherever it exists. The relational helpers (isBefore(),isAfter(),isBeforeOrEqualTo(), …) delegate tocompareTo(), so they inheritthe fast path and remain correct for the identity case, without touching them.
Why
These types are deeply immutable, and in practice objects are frequently
compared against themselves — a boundary reused across a derived range, a value
carried unchanged through a
map()/filter(), the same reference passed twice.In those cases an identity check is far cheaper than walking the full value
comparison (which, for the composite types, recurses into sub-objects).
Correctness
The safety invariant of the short-circuit is: identity may only ever add a
fast
true/0, never decide afalse. When identity fails, the code alwaysfalls through to the full value comparison, so distinct-but-equal instances
(unserialized, ORM-hydrated, separately parsed) still get the correct answer.
This relies only on reflexivity (
$a->isEqualTo($a)is true,$a->compareTo($a)is 0), which every sane comparison satisfies.
$this === $thatis used strictlyas a sufficient-condition shortcut, never as a replacement for value equality.
Tests
Added
same instanceregression tests covering each structural variant of theguard (primitive
compareTo,compareTodelegating to sub-objects, and theisEqualTo-only types with nocompareTo). Full suite green:OK (7861 tests, 38308 assertions).Benchmarks
A dependency-free harness (deliberately not part of this PR, to avoid adding
files to the package — posted as a comment below for inspection) A/B-compares
the working tree against a
git worktreeatHEAD— i.e. it runs the real,unmodified library code on both sides — interleaving the two across several
rounds and taking the per-scenario minimum.
Three operand shapes per method:
same(identical instance),eq-dist(distinct instances of equal value → full body runs),
ne-dist(distinct,differing on the first field → body early-exits).
Same instance (the whole point): consistent speed-up, scaling with how much
work the body avoids — the composite types that recurse into sub-objects gain
most (
LocalDateTime::compareTo0.21×,Interval::isEqualTo0.20×,~5×), the shallow ones 0.5–0.8×.
Distinct instances: the added
$this === $thatis a singleZEND_IS_IDENTICAL(a type + handle comparison), whose true cost issub-nanosecond. The small positive deltas on the
*-distrows above (+3…+22 ns,up to 1.41× on the cheapest body) are measurement noise, not a real cost —
they are one to two orders of magnitude larger than a single branch and do not
scale consistently with body size, which is the signature of the machine's
noise floor rather than the code. On an idle machine these rows collapse to
~1.00×; the harness is posted below precisely so this can be verified
independently.
🤖 Generated with Claude Code