Skip to content

reflection: Use fast ZPP for ReflectionProperty::(get|set)Value() #16329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

TimWolla
Copy link
Member

@TimWolla TimWolla commented Oct 9, 2024

During the Doctrine Core Team Meetup 2024 the Doctrine team investigated the performance overhead of using setRawValueWithoutLazyInitialization() instead of setValue() and came to the surprising conclusion that setRawValueWithoutLazyInitialization() outperformed setValue(), despite doing more work.

These two scripts are used as the benchmark:

<?php

class Foo
{
    public $id;
    public $foo1;
    public $foo2;
    public $foo3;
    public $foo4;
}

$reflection = new ReflectionClass(Foo::class);
$properties = $reflection->getProperties();

for ($i = 0; $i < 1000000; $i++) {
    $foo = new Foo();
    foreach ($properties as $property) {
        $property->setValue($foo, 1);
    }
}

and

<?php

class Foo
{
    public $id;
    public $foo1;
    public $foo2;
    public $foo3;
    public $foo4;
}

$reflection = new ReflectionClass(Foo::class);
$properties = $reflection->getProperties();

for ($i = 0; $i < 1000000; $i++) {
    $foo = new Foo();
    foreach ($properties as $property) {
        $property->setRawValueWithoutLazyInitialization($foo, 1);
    }
}

Benchmarking these with a current git master shows that setValue() is 50% slower:

$ hyperfine -L script setValue,setRawValueWithoutLazyInitialization '/tmp/php-before /tmp/test/{script}.php'
Benchmark 1: /tmp/php-before /tmp/test/setValue.php
  Time (mean ± σ):     216.0 ms ±   5.8 ms    [User: 212.0 ms, System: 3.7 ms]
  Range (min … max):   208.2 ms … 225.3 ms    13 runs

Benchmark 2: /tmp/php-before /tmp/test/setRawValueWithoutLazyInitialization.php
  Time (mean ± σ):     145.6 ms ±   3.6 ms    [User: 141.6 ms, System: 3.8 ms]
  Range (min … max):   140.4 ms … 152.8 ms    20 runs

Summary
  /tmp/php-before /tmp/test/setRawValueWithoutLazyInitialization.php ran
    1.48 ± 0.05 times faster than /tmp/php-before /tmp/test/setValue.php

Looking into the “why” revealed that the setValue() script spent quite some time in zend_parse_parameters().

A 50% overhead can be significant, given that setValue() is commonly called several thousand times in a single request when using Doctrine.

This commit changes the non-static property case of setValue() to make use of the fast parameter parsing API and adjusts getValue() for consistency.

The resulting comparison shows that both setValue() and setRawValueWithoutLazyInitialization() are now (almost) equal:

$ hyperfine -L script setValue,setRawValueWithoutLazyInitialization 'sapi/cli/php /tmp/test/{script}.php'
Benchmark 1: sapi/cli/php /tmp/test/setValue.php
  Time (mean ± σ):     143.0 ms ±   6.4 ms    [User: 139.4 ms, System: 3.4 ms]
  Range (min … max):   134.8 ms … 157.7 ms    18 runs

Benchmark 2: sapi/cli/php /tmp/test/setRawValueWithoutLazyInitialization.php
  Time (mean ± σ):     147.0 ms ±   5.5 ms    [User: 143.0 ms, System: 3.6 ms]
  Range (min … max):   139.9 ms … 159.8 ms    19 runs

Summary
  sapi/cli/php /tmp/test/setValue.php ran
    1.03 ± 0.06 times faster than sapi/cli/php /tmp/test/setRawValueWithoutLazyInitialization.php

Copy link
Member

@nielsdos nielsdos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise LGTM.

During the Doctrine Core Team Meetup 2024 the Doctrine team investigated the
performance overhead of using `setRawValueWithoutLazyInitialization()` instead
of `setValue()` and came to the surprising conclusion that
`setRawValueWithoutLazyInitialization()` outperformed `setValue()`, despite
doing more work.

These two scripts are used as the benchmark:

    <?php

    class Foo
    {
        public $id;
        public $foo1;
        public $foo2;
        public $foo3;
        public $foo4;
    }

    $reflection = new ReflectionClass(Foo::class);
    $properties = $reflection->getProperties();

    for ($i = 0; $i < 1000000; $i++) {
        $foo = new Foo();
        foreach ($properties as $property) {
            $property->setValue($foo, 1);
        }
    }

and

    <?php

    class Foo
    {
        public $id;
        public $foo1;
        public $foo2;
        public $foo3;
        public $foo4;
    }

    $reflection = new ReflectionClass(Foo::class);
    $properties = $reflection->getProperties();

    for ($i = 0; $i < 1000000; $i++) {
        $foo = new Foo();
        foreach ($properties as $property) {
            $property->setRawValueWithoutLazyInitialization($foo, 1);
        }
    }

Benchmarking these with a current git master shows that `setValue()` is 50%
slower:

    $ hyperfine -L script setValue,setRawValueWithoutLazyInitialization '/tmp/php-before /tmp/test/{script}.php'
    Benchmark 1: /tmp/php-before /tmp/test/setValue.php
      Time (mean ± σ):     216.0 ms ±   5.8 ms    [User: 212.0 ms, System: 3.7 ms]
      Range (min … max):   208.2 ms … 225.3 ms    13 runs

    Benchmark 2: /tmp/php-before /tmp/test/setRawValueWithoutLazyInitialization.php
      Time (mean ± σ):     145.6 ms ±   3.6 ms    [User: 141.6 ms, System: 3.8 ms]
      Range (min … max):   140.4 ms … 152.8 ms    20 runs

    Summary
      /tmp/php-before /tmp/test/setRawValueWithoutLazyInitialization.php ran
        1.48 ± 0.05 times faster than /tmp/php-before /tmp/test/setValue.php

Looking into the “why” revealed that the `setValue()` script spent quite some
time in `zend_parse_parameters()`.

A 50% overhead can be significant, given that `setValue()` is commonly called
several thousand times in a single request when using Doctrine.

This commit changes the non-static property case of `setValue()` to make use of
the fast parameter parsing API and adjusts `getValue()` for consistency.

The resulting comparison shows that both `setValue()` and
`setRawValueWithoutLazyInitialization()` are now (almost) equal:

    $ hyperfine -L script setValue,setRawValueWithoutLazyInitialization 'sapi/cli/php /tmp/test/{script}.php'
    Benchmark 1: sapi/cli/php /tmp/test/setValue.php
      Time (mean ± σ):     143.0 ms ±   6.4 ms    [User: 139.4 ms, System: 3.4 ms]
      Range (min … max):   134.8 ms … 157.7 ms    18 runs

    Benchmark 2: sapi/cli/php /tmp/test/setRawValueWithoutLazyInitialization.php
      Time (mean ± σ):     147.0 ms ±   5.5 ms    [User: 143.0 ms, System: 3.6 ms]
      Range (min … max):   139.9 ms … 159.8 ms    19 runs

    Summary
      sapi/cli/php /tmp/test/setValue.php ran
        1.03 ± 0.06 times faster than sapi/cli/php /tmp/test/setRawValueWithoutLazyInitialization.php
@TimWolla TimWolla force-pushed the reflection-property-getsetvalue-fast-zpp branch from ea58e73 to c4a7d06 Compare October 10, 2024 06:44
@TimWolla TimWolla merged commit 3da6818 into php:master Oct 10, 2024
10 checks passed
@TimWolla TimWolla deleted the reflection-property-getsetvalue-fast-zpp branch October 10, 2024 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants