Performance of try-catch in php

Viewed 31463

What kind of performance implications are there to consider when using try-catch statements in php 5?

I've read some old and seemingly conflicting information on this subject on the web before. A lot of the framework I currently have to work with was created on php 4 and lacks many of the niceties of php 5. So, I don't have much experience myself in using try-catchs with php.

9 Answers

One thing to consider is that the cost of a try block where no exception is thrown is a different question from the cost of actually throwing and catching an exception.

If exceptions are only thrown in failure cases, you almost certainly don't care about performance, since you won't fail very many times per execution of your program. If you're failing in a tight loop (a.k.a: banging your head against a brick wall), your application likely has worse problems than being slow. So don't worry about the cost of throwing an exception unless you're somehow forced to use them for regular control flow.

Someone posted an answer talking about profiling code which throws an exception. I've never tested it myself, but I confidently predict that this will show a much bigger performance hit than just going in and out of a try block without throwing anything.

Another thing to consider is that where you nest calls a lot of levels deep, it can even be faster to have a single try...catch right at the top than it is to check return values and propagate errors on every call.

In the opposite of that situation, where you find that you're wrapping every call in its own try...catch block, your code will be slower. And uglier.

I was bored and profiled the following (I left the timing code out):

function no_except($a, $b) { 
    $a += $b;
    return $a;
}
function except($a, $b) { 
    try {
        $a += $b;
    } catch (Exception $e) {}
    return $a;
}

using two different loops:

echo 'no except with no surrounding try';
for ($i = 0; $i < NUM_TESTS; ++$i) {
    no_except(5, 7);
}
echo 'no except with surrounding try';
for ($i = 0; $i < NUM_TESTS; ++$i) {
    try {
        no_except(5, 7);
    } catch (Exception $e) {}
}
echo 'except with no surrounding try';
for ($i = 0; $i < NUM_TESTS; ++$i) {
    except(5, 7);
}
echo 'except with surrounding try';
for ($i = 0; $i < NUM_TESTS; ++$i) {
    try {
        except(5, 7);
    } catch (Exception $e) {}
}

With 1000000 runs on my WinXP box run apache and PHP 5.2.6:

no except with no surrounding try = 3.3296
no except with surrounding try = 3.4246
except with no surrounding try = 3.2548
except with surrounding try = 3.2913

These results were consistent and remained in similar proportion no matter which order the tests ran.

Conclusion: Adding code to handle rare exceptions is no slower than code the ignores exceptions.

Generally, use an exception to guard against unexpected failures, and use error checking in your code against failures that are part of normal program state. To illustrate:

  1. Record not found in database - valid state, you should be checking the query results and messaging the user appropriately.

  2. SQL error when trying to fetch record - unexpected failure, the record may or may not be there, but you have a program error - this is good place for an exception - log error in error log, email the administrator the stack trace, and display a polite error message to the user advising him that something went wrong and you're working on it.

Exceptions are expensive, but unless you handle your whole program flow using them, any performance difference should not be human-noticeable.

I have not found anything on Try/Catch performance on Google but a simple test with a loop throwing error instead of a IF statement produce 329ms vs 6ms in a loop of 5000.

I updated Brilliand's test code to make it's report more understandable and also statitistically truthfully by adding more randomness. Since I changed some of its tests to make them more fair the results will be different, therefore I write it as different answer.

My tests executed by: PHP 7.4.4 (cli) (built: Mar 20 2020 13:47:45) ( NTS )

<?php

function shuffle_assoc($array) {
    $keys = array_keys($array);
    shuffle($keys);
    return array_merge(array_flip($keys), $array);
}

$c_e = new Exception('n');

function do_nothing($a, $b) {
    return $a + $b;
}
function new_exception_but_not_throw($a, $b) {
    try {
        new Exception('k');
    } catch (Exception $e) {
        return $a + $b;
    }
    return $a + $b;
}
function new_exception_and_throw($a, $b) {
    try {
        throw new Exception('k');
    } catch (Exception $e) {
        return $a + $b;
    }
    return $a + $b;
}
function constant_exception_and_throw($a, $b) {
    global $c_e;
    try {
        throw $c_e;
    } catch (Exception $e) {
        return $a + $b;
    }
    return $a + $b;
}

$tests = array(
    'do_nothing with no surrounding try'=>function() {
        do_nothing(5, 7);
    },
    'do_nothing with surrounding try'=>function() {
        try {
            do_nothing(5, 7);
        } catch (Exception $e) {}
    },
    'new_exception_but_not_throw with no surrounding try'=>function() {
        new_exception_but_not_throw(5, 7);
    },
    'new_exception_but_not_throw with surrounding try'=>function() {
        try {
            new_exception_but_not_throw(5, 7);
        } catch (Exception $e) {}
    },
    'new_exception_and_throw with no surrounding try'=>function() {
        new_exception_and_throw(5, 7);
    },
    'new_exception_and_throw with surrounding try'=>function() {
        try {
            new_exception_and_throw(5, 7);
        } catch (Exception $e) {}
    },
    'constant_exception_and_throw with no surrounding try'=>function() {
        constant_exception_and_throw(5, 7);
    },
    'constant_exception_and_throw with surrounding try'=>function() {
        try {
            constant_exception_and_throw(5, 7);
        } catch (Exception $e) {}
    },
);
$results = array_fill_keys(array_keys($tests), 0);
$testCount = 30;
const LINE_SEPARATOR = PHP_EOL; //"<br>";

for ($x = 0; $x < $testCount; ++$x) {
    if (($testCount-$x) % 5 === 0) {
        echo "$x test cycles done so far".LINE_SEPARATOR;
    }
    $tests = shuffle_assoc($tests);
    foreach ($tests as $k => $f) {
        $start = microtime(true);
        for ($i = 0; $i < 1000000; ++$i) {
            $f();
        }
        $results[$k] += microtime(true) - $start;
    }
}
echo LINE_SEPARATOR;
foreach ($results as $type => $result) {
    echo $type.' = '.number_format($result/$testCount, 4).LINE_SEPARATOR;
}

The results are following:

do_nothing with no surrounding try = 0.1873
do_nothing with surrounding try = 0.1990
new_exception_but_not_throw with no surrounding try = 1.1046
new_exception_but_not_throw with surrounding try = 1.1079
new_exception_and_throw with no surrounding try = 1.2114
new_exception_and_throw with surrounding try = 1.2208
constant_exception_and_throw with no surrounding try = 0.3214
constant_exception_and_throw with surrounding try = 0.3312

Conclusions are:

  • adding extra try-catch adds ~0.01 microsecond per 1000000 iterations
  • exception throwing and catching adds ~0.12 microsecond (x12 compared to previous when nothing were thrown and nothing were catched)
  • exception creation adds ~0.91 microsecond (x7.6 compared to execution of try-catch mechanism calculated in previous line)

So the most expensive part is exception creation - not the throw-catch mechanism, however latter made this simple routine 2 times slower compared to do_nothing scenarion.

* all measurements in conclusions are rounded and are not pretends to be scientifically accurate.

Generally speaking, they're expensive and not worthwhile in PHP.

Since it is a checked expressions language, you MUST catch anything that throws an exception.

When dealing with legacy code that doesn't throw, and new code that does, it only leads to confusion.

Good luck!

Related