Performance lessons learnt along the road.
The quest
The quest is one to optimize DI52 performances without loss of functionality.
I’ve outlined the main tool I will use to know where I stand in an earlier post as Tom Butler’s DI benchmarks.
While I had to adapt them to my context the tool remains substantially the same.
The starting point is this one:
The DI52 places itself before in the middle ground almost all the times.
More tools
In my lack of experience “thinking performance” I’ve scoured the web for answers and besides those sounding pretty much like “write it in X, PHP sucks” I’ve found few answers.
While the benchmarks do a good job of showing a real-world usage simulation the real deal here for me is not to go for the first place everywhere but to see where and how things could be improved.
To see how the code “plays” I’ve used the true and tested XDebug profiler and qcachegrind to take a look at the results.
Simple tricks
I’ve first gone with well known (to others, not me) PHP tricks:
===
is faster than==
when applicable$var === null
is faster thanis_null($var)
isset($array($key))
is faster thanin_array($key, $array)
orarray_key_exists($key, $array)
; this did require some refactoring but with tests in place that’s an easy trickforeach
versusarray_map
orarray_walk
. There is not a blanket approach for this: sometimes a solution is faster than the other and the fact that PHP 5.2 does not support closures complicates it. I’ve made single replacements offoreach
witharray_map/walk
, tested and rolled back when performance decreased- passing variables by reference when possible. In simple terms this means changing function and method signatures from
function someFunc($var);
tofunction someFunc(&$var);
; here I really have seen no measurable benefits.
The one I already knew and systematically applied is to calculate loop variables: this means going from something like this:
for($i=0; $i<count($loop); $i++){ ... }
to this:
$loopCount = count($loop);
for($i=0; $i<$loopCount; $i++){ ... }
All considered the benefits were not enourmous but still that’s something.
Calls
The real optimization kicked in analyzing the cache grind files.
I could find no “this is how you do it” guide solid enough to follow so made my rules up.
The first criteria I’ve applied is to sort the function calls by times those are called; I’ve made the simple assumption that one call is better than more if that call can be avoided. The image above shows shy of 40k calls to class_exists
and interface_exists
functions are made while running the tests; those were redundant and I’ve removed since I was essentially replicating exceptions that built-in PHP functions would throw of their own.
Iterating in a similar way (run tests ang get grind, modify code, run tests again) I was able to remove many redundant pieces of code.
Caching
Following along the same path I’ve inserted “caching” of results wherever possible in the code.
This has nothing to do with “real” caching systems like Memcached, APC, opcache or similar but with the simple practice of not redoing a calculatioin twice.
An example straight from the code is the one from the tad_DI52_Bindings_Resolver::resolveUnbound
method (hit a lot during the tests):
protected function resolveUnbound($classOrInterface)
{
if (isset($this->reflectors[$classOrInterface])) {
list($reflector, $constructor, $parameters) = $this->reflectors[$classOrInterface];
} else {
$reflector = new ReflectionClass($classOrInterface);
if (!$reflector->isInstantiable()) {
throw new Exception('[' . $classOrInterface . '] is not instantiatable.');
}
$constructor = $reflector->getConstructor();
$parameters = $constructor !== null ? $constructor->getParameters() : array();
$this->reflectors[$classOrInterface] = array($reflector, $constructor, $parameters);
}
if ($constructor === null) {
return new $classOrInterface;
}
$dependencies = $this->getDependencies($parameters, $classOrInterface);
$this->dependencies = array();
return $reflector->newInstanceArgs($dependencies);
}
During the life cycle of a request the ReflectorClass
created for a class would not change and hence there is no point in calculating it each time.
A similar concept I’ve applied all throughout the code base avoiding calculations wherever possible.
Final results
Looking at the initial graph the final one shows DI52 in a better position:
The package, while maintaining its functionalities and PHP 5.2 back compatibility, sure got faster.
If, at this stage, it has to lose to dice, pimple and other consolidated solutions I’m fine with it.
Next
Tom Butler’s DI benchmarks](https://github.com/TomBZombie/php-dependency-injection-benchmarks) include 2 more tests I’ve not run and am curious to.
I will work on performance more to cover those.