I saw a piece of code earlier this week (which, unfortunately, I am unable to retrieve) and I am curious about the way the author went about implementing the __call() magic method. The code looked something like the following:
class Sample
{
protected function test()
{
var_dump(func_get_args());
}
public function __call($func, $args)
{
if(!method_exists($this, $func))
{
return null;
}
switch(count($args))
{
case 0:
return $this->$func();
case 1:
return $this->$func($args[0]);
case 2:
return $this->$func($args[0], $args[1]);
case 3:
return $this->$func($args[0], $args[1], $args[2]);
case 4:
return $this->$func($args[0], $args[1], $args[2], $args[3]);
case 5:
return $this->$func($args[0], $args[1], $args[2], $args[3], $args[4]);
default:
return call_user_func_array($this->$func, $args);
}
}
}
$obj = new Sample();
$obj->test("Hello World"); // Would be called via switch label 1
As you can see, the author could have just used call_user_func_array() and dropped the switch entirely, so that would lead to me to believe there was (hopefully) some intelligent reasoning behind this.
The only reason I could think of would be perhaps some overhead of the function call to call_user_func_array(), but that doesn’t seem like a good enough reason to use a bunch of case statements. Is there an angle here I don’t seem to be getting?
The reason is that there is overhead on
call_user_func_array. It has the overhead of an additional function call. Typically this is in the range of microseconds, but it can become important in two cases:Recursive Function Calls
Since it’s adding another call to the stack, it will double the amount of stack usage. So you can run into issues (with xdebug, or memory constraints) which will cause your application to crash if you run out of stack. In applications (or parts), using this style approach can reduce your stack usage by as much as 33% (which can be the difference between an application running and crashing)
Performance
If you’re calling the function a lot, then those microseconds can add up significantly. Since this is in a framework (It looks like something done by Lithium), it will likely be called tens, hundreds or even thousands of times in the lifetime of the application. So, even though each individual call is a micro-optimization, the effect adds up significantly.
So yes, you can remove the switch and replace it with
call_user_func_arrayand it will be 100% the same with respect to functionality. But you’ll loose the two optimization benefits mentioned above.EDIT And to prove the performance difference:
I decided to do a benchmark myself. Here’s a link to the exact source that I used:
http://codepad.viper-7.com/s32CSb (also included at the bottom of this answer for reference)
Now, I tested it on a Linux system, a windows system and codepad’s site (2 command line, and 1 online, and 1 with XDebug enabled) All running 5.3.6 or 5.3.8
Conclusion
Since the results are rather long, I’ll summarize first.
If you’re calling this a lot, it’s not a micro-optimization to do this. Sure, an individual call is insignificant difference. But if it’s going to be used a lot, it can save quite a bit of time.
Now, it’s worth noting that all except one of these tests are run with XDebug off. This is extremely important, as xdebug appears to significantly alter the results of the benchmark.
Here are the raw results:
Linux
(I actually ran it about a dozen times, and the results are consistent). So, you can clearly see that on that system, it’s significantly faster to use the switch for functions with 3 or less arguments. For 4 arguments, it’s close enough to qualify as a micro-optimization. For 5 it’s slower (due to the overhead of the switch statement).
Now, objects are another story. For objects, it’s significantly faster to use the switch statement even with 4 arguments. And the 5 argument is slightly slower.
Windows
Again, just as with Linux, it’s faster for every case except 5 arguments (which is expected). So nothing out of the normal here.
Codepad
This shows the same picture as with Linux. With 4 arguments or less, it’s significantly faster to run it through the switch. With 5 arguments, it is significantly slower with the switch.
Windows With XDebug
Now this tells a different story. In this case with XDebug enabled (but no coverage analysis, just the extension turned on), it’s almost always slower to use the switch optimization. This is curious since many benchmarks are run on dev boxes with xdebug enabled. Yet production boxes usually don’t run with xdebug. So it’s a pure lesson in executing benchmarks in proper environments.
Source