php-src/ext/reflection
Tim Düsterhus 3da6818c9e
reflection: Use fast ZPP for ReflectionProperty::(get|set)Value() (#16329)
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
2024-10-10 09:19:53 +02:00
..
tests Merge branch 'PHP-8.4' 2024-10-07 14:18:36 +02:00
config.m4 Autotools: Normalize and quote all PHP_NEW_EXTENSION arguments (#15144) 2024-07-29 00:14:59 +02:00
config.w32
CREDITS
php_reflection.c reflection: Use fast ZPP for ReflectionProperty::(get|set)Value() (#16329) 2024-10-10 09:19:53 +02:00
php_reflection.h Lazy objects 2024-08-30 17:30:03 +02:00
php_reflection.stub.php Merge branch 'PHP-8.4' 2024-10-07 14:18:36 +02:00
php_reflection_arginfo.h Merge branch 'PHP-8.4' 2024-10-07 14:18:36 +02:00