Skip to content

Perf/optimizations#19

Merged
Nejcc merged 6 commits into
masterfrom
perf/optimizations
May 29, 2026
Merged

Perf/optimizations#19
Nejcc merged 6 commits into
masterfrom
perf/optimizations

Conversation

@Nejcc

@Nejcc Nejcc commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Vector classes now support arithmetic operations: addition, subtraction, scaling, dot product, magnitude, distance, and normalization.
    • Introduced schema compilation feature for improved struct construction performance.
  • Bug Fixes

    • Field validation rules now correctly apply to nested struct fields.
  • Tests

    • Added comprehensive test coverage for attribute validation.

Review Change Stack

Nejcc added 6 commits May 28, 2026 13:25
AbstractVector hot paths (add, subtract, scale, dot, distance,
magnitude, normalize) all used array_map(fn) with a closure invoked
per component. The closure allocation + variadic call dominated the
runtime — magnitude on a Vec2 was spending 3.7us doing 2 multiplies.

Two changes:

1. Rewrite all hot methods to use foreach with direct index access.
   Same algorithm, no closures.

2. Add bool \$trusted = false constructor flag on AbstractVector.
   Arithmetic results (add/sub/scale/normalize) are dimension-
   preserving and numeric-preserving by construction, so they
   skip validateComponents() via new static(\$result, true).

Numbers (100k iters):
  Vec2 add+sub+scale:  20.17us -> 6.43us  (3.1x)
  Vec2 dot:             3.70us -> 2.00us  (1.9x)
  Vec2 magnitude:       3.74us -> 0.98us  (3.8x)
  Vec3 add+sub+scale:  24.98us -> 8.44us  (3.0x)
  Vec4 add+sub+scale:  29.48us -> 8.11us  (3.6x)

Behavior preserved. 592/592 tests pass.
The foreach-based hot paths on AbstractVector still pay per-component
loop overhead and per-component array writes. For fixed-dimension
vectors the dimension is known statically — unroll the math entirely.

Override add/subtract/scale/dot/magnitude/distance/normalize on each
of Vec2, Vec3, Vec4 with direct \$components[0], \$components[1], ...
access. Parameter widens from self to AbstractVector (PHP LSP rules
disallow narrowing) but is checked with instanceof self in each method
to preserve the cross-dimension error contract.

Numbers (100k iters), cumulative vs original abstract:
  Vec2 add+sub+scale:  20.17us -> 3.29us  (6.1x)
  Vec2 dot:             3.70us -> 0.68us  (5.4x)
  Vec2 magnitude:       3.74us -> 0.81us  (4.6x)
  Vec3 add+sub+scale:  24.98us -> 3.79us  (6.6x)
  Vec4 add+sub+scale:  29.48us -> 3.55us  (8.3x)

Per-op floor is now ~1.1us — instanceof check + 3-4 float ops +
array literal + new self + readonly assign. The remaining cost is
PHP object construction itself.

Behavior preserved (same exceptions, same results). 592/592 tests pass.
… dispatch

Struct construction was ~3us per field for a typical schema. Five
small fixes, each individually small, compound on hot paths
(Laravel form requests, batch deserialization, etc.):

1. Inline validateField() into the constructor loop. The method
   call cost wasn't huge but it's per-field.

2. Skip the rules loop entirely when rules === []. The previous
   code invoked array_all with a closure even on fields with no
   rules, which is the common case.

3. Replace array_all(closure) with foreach. Closure allocation per
   field x per validation = wasted frames.

4. Type-dispatch via match() instead of an if-cascade with seven
   string comparisons. PHP's match generates a jump table.

5. Resolve values with isset() first, falling through to
   array_key_exists() only when the key is missing-or-null. Most
   values aren't null, so the common path is now a single isset.

Also replaced reset() (which mutates the array pointer) with
array_key_first() in the BC-format detection at the top of the
constructor.

Numbers (10k iters):
  Struct construction (8 fields):   24.09us -> 12.55us  (1.92x)
  Struct construction (3 fields):    6.32us ->  5.39us  (1.17x)
  Per-field cost:                    ~3.0us -> ~1.57us

Behavior preserved. Same exceptions thrown for the same conditions
(InvalidArgumentException for missing/wrong-type, ValidationException
for failed rules). 592/592 tests pass.
Struct construction was spending a chunk of its time on per-instance
schema normalisation work that is identical across every construction
with the same schema: detecting the legacy ['id' => 'int'] format,
defaulting type/nullable/default/rules/alias, computing the "required"
flag. Move it out of the hot loop.

New value object CompiledSchema (final, immutable, public readonly):

  \$compiled = CompiledSchema::compile(\$schema);
  foreach (\$rows as \$row) {
      \$structs[] = new Struct(\$compiled, \$row);
  }

Struct::__construct now accepts array|CompiledSchema. Array path is
unchanged from the previous commit — no regression for one-shot
callers. Compiled path goes through a faster initFromCompiled() that
reads pre-resolved field tuples [type, nullable, default, rules,
alias, required] by integer offset.

Numbers (10k iters):
  Compile (one-time):                       ~6.4us
  Struct(array)         8 fields:           12.47us
  Struct(CompiledSchema) 8 fields:          10.15us  (1.23x)
  Struct(array)         3 fields:            5.24us
  Struct(CompiledSchema) 3 fields:           4.39us  (1.19x)

Break-even is roughly 3-5 instances per schema — after that the
compiled path is always cheaper. Cumulative vs the original Struct
implementation (24.09us for 8 fields), the compiled path is 2.37x
faster.

Behavior preserved. Same exceptions, same field-resolution order
(explicit value -> alias -> default), same nullability semantics.
592/592 tests pass.
Attributes\Validator::validateProperty() was calling
\$property->getAttributes() and \$attribute->newInstance() on every
invocation. For a typical form-request flow that validates the same
property across many items in a payload (or many requests in a worker
process), the reflection work dominates: ~1.5us per attribute,
repeated forever.

Cache the parsed attribute instances keyed by 'Class::property'.
Property structure doesn't change at runtime, so we instantiate
once and reuse.

Also added a public static clearCache() for long-running processes
that reload classes (rare) and for tests that need a clean slate.

Numbers (50k iters):
  Range (1 attr):           4.60us -> 1.75us  (2.62x)
  Email+NotNull (2 attrs): 12.27us -> 7.85us  (1.56x)
  NotNull+Length (2 attrs): 6.28us -> 3.02us  (2.08x)

Gain scales with attributes per property — each cached attribute
saves ~1.5us of newInstance() reflection work.

Also added Tests/Attributes/ValidatorTest.php (5 tests, previously
zero coverage on this class). All 597 tests pass.
Same pattern as the earlier Struct optimisation, transplanted to its
two siblings.

AdvancedStruct (nearly identical to pre-opt Struct):
- isset() fast-path for value resolution
- skip the rules foreach when rules === []
- match() type dispatch instead of if-cascade
- isValidType() made static

ImmutableStruct (different shape, narrower wins):
- inline nullable detection (\$type[0] === '?') instead of method
  calls to isNullable() + stripNullable()
- only call get_debug_type() when an error is about to be thrown,
  not eagerly
- skip the rules foreach when no rules are declared

Numbers (avg of 3x 20k iter runs):
  AdvancedStruct  (8 fields):  ~14.5us -> ~8.6us  (1.69x)
  ImmutableStruct (8 fields):  ~48.8us -> ~39.0us (1.25x)

ImmutableStruct's smaller win reflects irreducible overhead from
parent inheritance, separate initialize/setInitial phases, and
frozen-state tracking. No structural rewrite — the fixes are
mechanical and minimal-risk. Behavior preserved, 597/597 tests pass.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2167610-706b-4064-b756-4f370206cab5

📥 Commits

Reviewing files that changed from the base of the PR and between 3092f37 and c85d29e.

📒 Files selected for processing (10)
  • Tests/Attributes/ValidatorTest.php
  • src/Abstract/AbstractVector.php
  • src/Attributes/Validator.php
  • src/Composite/Struct/AdvancedStruct.php
  • src/Composite/Struct/CompiledSchema.php
  • src/Composite/Struct/ImmutableStruct.php
  • src/Composite/Struct/Struct.php
  • src/Composite/Vector/Vec2.php
  • src/Composite/Vector/Vec3.php
  • src/Composite/Vector/Vec4.php

📝 Walkthrough

Walkthrough

This PR introduces three optimization features. Validator now caches instantiated attribute objects per Class::property, eliminating reflection overhead on repeated validations. AbstractVector and concrete vector classes (Vec2/Vec3/Vec4) use explicit loops and a trusted-construction flag to avoid re-validating computed arithmetic results. Struct classes integrate a new CompiledSchema type that pre-compiles field definitions for efficient reuse, with refactored validation helpers across Struct, AdvancedStruct, and ImmutableStruct.

Changes

Validator Attribute Caching

Layer / File(s) Summary
Validator cache implementation
src/Attributes/Validator.php
validateProperty() builds a cache key from ReflectionProperty declaring class and property name, caches and reuses instantiated attribute objects per key, and dispatches validation via match on cached type. New compileAttributes() helper instantiates all property attributes once. Public clearCache() resets the static cache for long-running processes or tests.
Validator cache tests and fixtures
Tests/Attributes/ValidatorTest.php
ValidatorTest clears validator cache in setUp(), then tests Range, Email, Length, and NotNull attribute validation behavior with expected exception types. Cache reuse test confirms repeated ReflectionProperty calls reuse cached instances, and verifies clearCache() works mid-test. ValidatorFixture provides annotated public properties for reflection targets.

Vector Arithmetic Optimization

Layer / File(s) Summary
AbstractVector trusted-construction and arithmetic rewrites
src/Abstract/AbstractVector.php
Constructor gains bool $trusted = false parameter; when true, skips validation for internally computed vectors. All arithmetic methods (magnitude, normalize, dot, add, subtract, scale, distance) converted from functional array operations to explicit indexed loops, returning new static($result, true) to avoid re-validating mathematically computed results.
Vec2 vector operations
src/Composite/Vector/Vec2.php
Adds add, subtract, scale for component-wise arithmetic; dot for scalar product; magnitude for Euclidean norm; distance for 2D distance; and normalize for unit vectors. Binary operations validate operand is Vec2 and throw InvalidArgumentException otherwise. All results constructed with trusted flag.
Vec3 vector operations
src/Composite/Vector/Vec3.php
Implements full 3D arithmetic suite (add, subtract, scale, dot, magnitude, distance, normalize) with runtime dimension checks for binary operations and zero-magnitude error handling for normalize. All results use trusted-construction.
Vec4 vector operations
src/Composite/Vector/Vec4.php
Adds 4D arithmetic operations (add, subtract, scale, dot, magnitude, distance, normalize) with operand type/dimension validation and zero-magnitude error handling, all constructing results with trusted flag.

Struct Schema Compilation and Validation Refactoring

Layer / File(s) Summary
CompiledSchema pre-compilation
src/Composite/Struct/CompiledSchema.php
New immutable CompiledSchema class compiles a schema array once into per-field tuples [type, nullable, default, rules, alias, required] for efficient reuse. compile() detects legacy "field => type" shorthand, normalizes to richer per-field definitions, computes required flag from nullable/default, and retains the normalized original schema for downstream consumers.
Struct integration with CompiledSchema
src/Composite/Struct/Struct.php
Constructor signature updated to __construct(array|CompiledSchema $schema, array $values = []). Raw arrays route through legacy-shorthand detection and normalization. CompiledSchema instances route through new initFromCompiled() that iterates compiled tuples for efficient field resolution and validation. Validation helpers refactored: validateField() performs type checks and rule iteration; isValidType() becomes protected static with match-based implementation handling builtin types and class_exists fallback.
AdvancedStruct validation refactoring
src/Composite/Struct/AdvancedStruct.php
Constructor refactored to inline field value resolution (direct key, null if key exists and is null, alias lookup, default) and perform type/rule validation inline. validateField() and isValidType() updated to align with new protected static isValidType using match-based type checking and consistent callable rule iteration.
ImmutableStruct validation refactoring
src/Composite/Struct/ImmutableStruct.php
validateValue() inlines nullable type detection and returns early only for null values on nullable fields. Nested-struct type checks now throw but continue execution so validation rules run on nested structs (previously skipped). Rule loop explicitly skipped only when rules are empty.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Whiskers twitching with delight,
Caching validators left and right,
Vectors dance with trusted pace,
Schemas compile in one embrace!
Quick math and schema, side by side, 🚀

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.55)

Composer install failed: the lock file is not up to date with the latest changes in composer.json. Run composer update and commit the updated composer.lock.


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 and usage tips.

@Nejcc Nejcc merged commit e278067 into master May 29, 2026
0 of 25 checks passed
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
18.2% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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