Update 12/06/15: I’ve opened a PR making the PropertyAccess Component 84% faster than before. It implements strategies explained at the end of this post.
This PropertyAccess component allows to access to a property of an object (or a to a key of an array) regardless of the access strategy that must be used. It is smart enough to access directly to public properties and to use getters, setters, issers, adders, removers, magic methods and so on for private and protected ones. It is also allows to access a property trough an object graph using a straightforward notation: $propertyAccessor->getValue($rootObject, ‘foo.bar.baz’); .
As you may know, some components of the Symfony framework including Form and the Serializer (see the ObjectNormalizer) rely heavily on it. The JSON-LD normalizer of the API Platform framework also use it intensively. In fact, more than 250+ open source projects rely on the PropertyAccess component (and it has been installed more than 1M times).
It’s a convenient and popular piece of software. But this convenience has a cost: the PropertyAccess component is slow: internally, it uses the PHP Reflection API to guesses what’s the strategy needed to read or write a property.
When using this component in a loop (ex: to normalize a collection of objects to an associative array using the ObjectNormalizer), its slowness become a significant problem.
Here is a benchmark for the PropertyAccess component:
<?php require 'vendor/autoload.php'; class Foo { private $baz; public $bar; public function getBaz() { return $this->baz; } public function setBaz($baz) { $this->baz = $baz; } } use Symfony\Component\PropertyAccess\PropertyAccess; $accessor = PropertyAccess::createPropertyAccessor(); echo 'Property Access Benchmark'.PHP_EOL; $start = microtime(true); for ($i = 0; $i < 10000; ++$i) { $foo = new Foo(); $accessor->setValue($foo, 'bar', 'Lorem'); $accessor->setValue($foo, 'baz', 'Ipsum'); $accessor->getValue($foo, 'bar'); $accessor->getValue($foo, 'baz'); } echo 'Time: '.(microtime(true) - $start).PHP_EOL;
And here is the profile generated by the awesome Blackfire.io: with Symfony 2.7.6:
As you can see the logic to guess the access strategy is executed at each loop iteration. As the structure of a PHP class generally never changes at runtime, a possible optimisation path is to guess the strategy to use for the property of a given class during the first iteration then cache it in an array. This way, the cached access strategy can be reused without extra computations every other time during the current execution thread.
Let’s try that:
(Click on the image to read the full Pull Request description and the related code).
Thanks to this new caching strategy, the PropertyAccessor Component included in Symfony 2.7.7 (and more recent versions) is now 54% faster according to the same benchmark:
But the component can be even more optimized.
For instance, the constructor of the Symfony\Component\PropertyAccess\PropertyPath class uses a slow regular expression. This constructor is still called at each iteration in a loop. The result of the regular expression isn’t cached:
A similar cache mechanism should be used to avoid executing the same regular expression at each iteration. I’ve started to work on a patch doing that, but it still needs some work before being ready to be merged in Symfony. This patch improves the performance of the PropertyAccess of 20% more. Using both patches, the component is 70% faster than in 2.7.6.
There is another promising way to improve the performance of this component: for now the access strategy is always guessed the first time a property is accessed during an execution thread (e.g. when handling an HTTP request). Using a permanent cache system (such as APC or Redis trough the Doctrine Cache library) in complement of the in-memory array would allow to guess the strategy only one time, (e.g. during the first HTTP request), then reuse it for each next executions. Symfony 3.1 will have this feature.
By the way, a similar optimization has been done on the ObjectNormalizer. It needs to know the list of properties of an object to normalize it. This list wasn’t cached either. In Symfony 2.8, it’s done:
Thanks to this patch and to the optimization of the PropertyAccess component, the ObjectNormalizer is 40% faster in Symfony 2.8/3.0 than in Symfony 2.7.6!
Great work, as always! One thing though, comparisons are showing an increase of about 30% memory usage. I’m assuming that you favored performances over memory usage but won’t this be a trouble maker in case of a big symfony application?
Hi Antoine,
Yes this optimization implies a little memory usage (~ 180kB) but this increase should be relatively limited: in the worst case we one time store in memory an association between the Class::property and a int value. Before we were recomputing everything at every time (nothing was stored in memory, it explain the “high” increase in percentage but limited in term of absolute memory (some kB, even in a large loop like in the benchmark).
Even for a complex object graph it should not be signifiant (but the improvement in term of execution time is).