views:

183

answers:

3

While optimizing a function in PHP, I changed

if(is_array($obj)) foreach($obj as $key=>$value { [snip] } 
else if(is_object($obj)) foreach($obj as $key=>$value { [snip] } 

to

if($obj == (array) $obj) foreach($obj as $key=>$value { [snip] } 
else if($obj == (obj) $obj) foreach($obj as $key=>$value { [snip] } 

After learning about ===, I changed that to

if($obj === (array) $obj) foreach($obj as $key=>$value { [snip] } 
else if($obj === (obj) $obj) foreach($obj as $key=>$value { [snip] } 

Changing each test from is_* to casting resulted in a major speedup (>30%).

I understand that === is faster than == as no coercion has to be done, but why is casting the variable so much faster than calling any of the is_*-functions?

Edit: Since everyone asked about correctness, I wrote this little test:

$foo=(object) array('bar'=>'foo');
$bar=array('bar'=>'foo');

if($foo===(array) $foo) echo '$foo is an array?';
if($bar===(object) $bar) echo '$bar is an object?';

It doesn't print any error and both variables don't get changed, so I think it's working, but I'm ready to be convinced otherwise.

Another Edit: Artefacto's program gives me the following numbers:

PHP 5.3.2-1ubuntu4.2 (64bit) on a Core i5-750 with Xdebug
Elapsed (1): 0.46174287796021 / 0.28902506828308
Elapsed (2): 0.52625703811646 / 0.3072669506073
Elapsed (3): 0.57169318199158 / 0.12708187103271
Elapsed (4): 0.51496887207031 / 0.30524897575378
Speculation: Casting and comparing can be about 1.7-4 times faster.
PHP 5.3.2-1ubuntu4.2 (64bit) on a Core i5-750 without Xdebug
Elapsed (1): 0.15818405151367 / 0.214271068573
Elapsed (2): 0.1531388759613 / 0.25853085517883
Elapsed (3): 0.16164898872375 / 0.074632883071899
Elapsed (4): 0.14408397674561 / 0.25812387466431
Without Xdebug, the extra function call didn't matter anymore, so every test (except 3) ran faster.
PHP 5.3.2-1ubuntu4.2 on a Pentium M 1.6GHz
Elapsed (1): 0.97393798828125 / 0.9062979221344
Elapsed (2): 0.39448714256287 / 0.86932587623596
Elapsed (3): 0.44513893127441 / 0.23662400245667
Elapsed (4): 0.38685202598572 / 0.82854390144348
Speculation: Casting an array is slower, casting an object can be faster, but might not be slower.
PHP 5.2.6-1+lenny8 on a Xeon 5110
Elapsed (1): 0.273758888245 / 0.530702114105
Elapsed (2): 0.276469945908 / 0.605964899063
Elapsed (3): 0.332523107529 / 0.137730836868
Elapsed (4): 0.267735004425 / 0.556323766708
Speculation: These results are similar to Artefacto's results, I think it's PHP 5.2.

The solution: The profiler I used (Xdebug) made function calls about 3 times slower (even when not profiling), but didn't affect casting and comparing noticeably, so casting and comparing appeared to be faster, even though it just wasn't affected by the debugger/profiler.

+4  A: 

I can't really reproduce. In fact, your strategy gives me longer times in all but one case:

<?php

class A {
    private $a = 4;
    private $b = 4;
    private $f = 7;
}

$arr = array("a" => 4, "b" => 4, "f" => 7);

$obj = new A();

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    is_array($obj) and die("err");
}

echo "Elapsed (1.1): " . (microtime(true) - $t);
echo "\n";

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    ($obj === (array) $obj) and die("err");
}

echo "Elapsed (1.2): " . (microtime(true) - $t);
echo "\n";

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    is_object($arr) and die("err");
}

echo "Elapsed (2.1): " . (microtime(true) - $t);
echo "\n";

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    ($arr === (object) $arr) and die("err");
}

echo "Elapsed (2.2): " . (microtime(true) - $t);
echo "\n";

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    is_object($obj) or die("err");
}

echo "Elapsed (3.1): " . (microtime(true) - $t);
echo "\n";

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    ($obj === (object) $obj) or die("err");
}

echo "Elapsed (3.2): " . (microtime(true) - $t);
echo "\n";

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    is_array($arr) or die("err");
}

echo "Elapsed (4.1): " . (microtime(true) - $t);
echo "\n";

$t = microtime(true);

for ($i = 0; $i < 500000; $i++) {
    ($arr === (array) $arr) or die("err");
}

echo "Elapsed (4.2): " . (microtime(true) - $t);

Output:

Elapsed (1.1): 0.366055965424
Elapsed (1.2): 0.550662994385
Elapsed (2.1): 0.337422132492
Elapsed (2.2): 0.579686880112
Elapsed (3.1): 0.402997970581
Elapsed (3.2): 0.190818071365
Elapsed (4.1): 0.332742214203
Elapsed (4.2): 0.549873113632

The cast and compare was only faster for checking if something is an object. Speculation follows: perhaps because object identity checking requires only determining whether the handler tables and the object handles are the same, while checking for array identity requires, in the worst case, comparing all the values.

Artefacto
Xdebug was skewing my numbers, I found the problem when I was comparing the i5 to the Xeon. I accepted your answer, but you might want to add a note about the PHP version and addons (debugger, opcode cache, etc.) affecting the benchmark.
tstenner
@tstenner Windows 7 64-bit, AND Athlon 64 3500+, Release build of PHP 5.3.2 compiled with VC6. No debugger or opcode cache (not that an opcode cache would make a difference; time counting stats after the script compilation).
Artefacto
A: 

Running such a tests is a waste of time.
The difference between SOAP network call and such a micro operation is incomparable.
You cannot optimize anything with this.

Col. Shrapnel
This is not really an answer to the question. But a good comment though.
Gumbo
it is not *direct* answer but it's still an answer. I know that SO rules allow only direct answers, but I disagree with that. Not every question can be answered directly or deserves a direct answer. Especially this kind of "performance-related" questions when too many people believe in the "major speedup (>30%)" of such a micro-optimizations. Without such a sobering answers SO will become a source of misbeliefs.
Col. Shrapnel
This function was called about 10k times, blocking one core for a second. The optimized version didn't.
tstenner
@tstenner 10k times per single soap request?
Col. Shrapnel
@Col. Shrapnel for all requests combined, as I was going through the resulting object recursively.
tstenner
@tstenner Artefacto did 500 000 iterations in less than second. And you says your 10 000 took a second. That's a difference. You have optimized something else.
Col. Shrapnel
Artefacto was timing just checking whether something is an object/array or not, 1 second/10k calls is for the whole function that doesn't just check if it's an object or not.
tstenner
@tstenner well exactly what i have said above - you have optimized something else in that code. And that other optimization made your code faster, not casting
Col. Shrapnel
@Col. Shrapnel: This change made the function I was optimizing about 30% faster, considering that in one of the cases casting was about 4 times faster, that's not so unrealistic.
tstenner
@tstenner even 10 times faster - it doesnt matter. What is **proportion* between casting and other parts of this program? You have optimizing a micro part of it.
Col. Shrapnel
@Col. Shrapnel It was just 10%, but 80% was just waiting for some data, which didn't cause any noticable load on the server, so I reduced the server load considerably.
tstenner
+3  A: 
  1. It may be that in certain PHP versions function call would take longer than conversion.
  2. Debuggers/profilers will significantly change these relationships since they override function call handlers, etc. so they change how long it takes to call functions.
  3. If you are optimizing these things, you're looking in the wrong place. If your PHP application does something non-trivial its performance almost never would improve from microsecond advantage you might gain from trying to outsmart the engine, and even if it does it will probably go away with next PHP version which would change engine implementation in some details. For example, 5.2 and 5.3 engines differ internally, and next version would have more differences. These differences usually don't influence the code but they will make all your micro-optimizations irrelevant.
  4. Converting something that is not an object/array to an object/array would probably be slower that checking in general because it'd have to create a new object/array.
StasM
A microsecond might not seem much, but this test was taking more than a quarter of the function's execution time.
tstenner
4. surprised me too, that's why a asked this question ;)Anyway, nice answer. +1
tstenner