Skip to content

Read built in decoder properties from ElementLookup directly - #84

Merged
veewee merged 13 commits into
absent-optional-elementsfrom
perf-direct-element-lookup
Oct 16, 2026
Merged

veewee merged 13 commits into
absent-optional-elementsfrom
perf-direct-element-lookup

Conversation

@veewee

@veewee veewee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member
Q A
Type improvement
BC Break yes, for custom object lenses that throw
Fixed issues

Stacked on #80, so it targets that branch until it is merged.

Lens overhead on decode

#80 moved decoding to ElementLookup, and that made sparse decoding a little slower than on #79. When I profiled it, nearly all of the difference came from the lens indirection. Every property went through Lens::readonly, its closure and the try/catch in runLens, only to call element(), attribute() or text() on the lookup.

One plain reader per property

ObjectAccess now keeps one closure per property for each direction, decodeReaders and encodeReaders. A built in decode reader calls the lookup directly, and the encode reader is reflecta's throwless optional_property. A custom ProvidesObjectDecoderLens or ProvidesObjectEncoderLens lens is called as is, so runLens, the try/catch and the optional() wrap are gone.

Custom lenses that throw

A custom lens used to have its exception swallowed and read as null. Now the exception fails the encode or decode as an EncodingException, with the original exception as previous. Custom object lenses must handle their own misses, for example with optional() or optional_property(). A null from a custom lens still never counts as absent (#79).

Same output

The decoded results and encoded requests of the 3MB workloads have the same sha1 as #80.

Speed

Base is element-lookup (#80). Medians over three interleaved rounds on the same machine.

Workload #80 This PR
3MB decode sparse (stdClass) 0.504 s 0.460 s (-9%)
3MB decode sparse (classmap) 0.917 s 0.897 s (-2%)
3MB decode dense (stdClass) 0.273 s 0.266 s
3MB decode dense (classmap) 0.328 s 0.326 s

Encode and the phpbench subjects stay within the run to run spread. An earlier version of this PR read the lookup inline through a match and reached -13% on sparse stdClass. I dropped that for one shape per property and less code.

Hoist per property type meta out of the object loops
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ee480bef-30c1-4684-94ad-e18d49ae556c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Xmlns::xsi() builds a new object on every call. OptionalElementEncoder now resolves it once per iso, and DocumentToLookupArrayReader compares against a class constant.
Locator\Node\value() reads substitutedNodeValue and coerces it to a string, which it already is on a Dom\Element. Reading the property skips the function call and the Psl coercion.
…per-decode-reads

# Conflicts:
#	src/Xml/Reader/DocumentToLookupArrayReader.php
@veewee
veewee added this pull request to stack #88 October 16, 2026 07:33
Base automatically changed from element-lookup to absent-optional-elements October 16, 2026 07:34
Every property of a decoded object went through Lens::readonly, its
closure and the try/catch in runLens, only to call element(),
attribute() or text() on the lookup. ObjectAccess now records per
property how the built in lens reads it, and ObjectEncoder::from makes
that call itself. Those three calls cannot throw, so the try/catch had
nothing to catch for them.

Custom ProvidesObjectDecoderLens lenses keep going through runLens.
The built in lenses stay in decoderLenses and are built from the same
record, so they read exactly what the direct call reads.
… too

Missing elements, attributes in another namespace and nil or mixed text all read null, and those are the reads where a lens and the direct call could differ.
@veewee
veewee force-pushed the perf-direct-element-lookup branch from d5ceee2 to a9cc817 Compare October 16, 2026 07:34
@veewee
veewee merged commit dde12e2 into absent-optional-elements Oct 16, 2026
24 checks passed
@veewee
veewee deleted the perf-direct-element-lookup branch October 16, 2026 08:49
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