Improve performace: check your loops

by Arnold Daniels on 01/24/2008

Before I start
I’ve always stayed away from writing a performance topic. There seems to be a lot of performance enthusiasts, benchmarking anything they can, in order to scoop off a few more percent of performance time. To my opinion, if your system is already reacting within an acceptable fashion, there is no need to try to further improve performance. Maintainability is a much more important topic to look at, at that point.

Finding out what is the problem
So you have a script which is not performing the way you want to. The first thing you should do it try to find out what the problem is. There are some tools out there that can help you. If you’re using Zend Studio, there is a profiler which shows you a tree of files and functions with the amount of processing time. (Warning: With Zend Studio 5, you need to close your project and all open files, otherwise you’ll get incorrect values). There is also a profiler in XDebug, which is a bit harder to set up, but works just as well (or even a bit better actually).

If you don’t have the option to install any of these tools, you can echo (or use FirePHP to output) the execution times, using the following script:

1
2
3
4
5
$_ENV['exec_start_time'] = microtime(true);
....
echo "<!-- ", "After doing XYZ: ", (microtime(true) - $_ENV['exec_start_time']) * 1000, " ms", " -->\n"
....
echo "<!-- ", "After doing SOMETHING: ", (microtime(true) - $_ENV['exec_start_time']) * 1000, " ms", " -->\n"
$_ENV['exec_start_time'] = microtime(true);
....
echo "<!-- ", "After doing XYZ: ", (microtime(true) - $_ENV['exec_start_time']) * 1000, " ms", " -->\n"
....
echo "<!-- ", "After doing SOMETHING: ", (microtime(true) - $_ENV['exec_start_time']) * 1000, " ms", " -->\n"

Most likely the problem will be one of these three (in order of likeliness):

  1. A slow database query. Solving this is outside the scope of this article, but read this PDF about Query tuning for starters.
  2. To much is executed within a loop and/or the number of items for the loop is to big. Read the rest of the article.
  3. To much code is executed. This is probably cause of an abstraction layer that is simply to big or of to many abstraction layers. Try to do some refactoring, removing unnecessary abstraction or bypass abstraction on bottleneck positions.

Get the data ready before going into the loop
Looping through a large list in PHP can be very expensive performance wise, even if code in the loop is limited. If you’re looping through a mysql result, see if you can get the data as you want to use it from the query by using string, date and numeric functions and things like GROUP_CONCAT. Also try not to have to filter the data in PHP, you should only get the rows out the DB you actually need.

Don’t do things in the loop, you can also do outside it
Try to do as much stuff as possible outside of the loop. If an operation gives the same result for each item, you can surely put is outside of the loop.

Don’t do this:

1
2
3
4
5
6
7
8
for ($i=0; $i<sizeof($array); $i++) {
  if ($mark) {
     list($pre, $post) = explode('~', $tpl, 2);
     echo $pre, $array[$i]['desc'], $post; 
  } else {
     echo " - ", $array[$i]['desc'];
  }
}
for ($i=0; $i<sizeof($array); $i++) {
  if ($mark) {
     list($pre, $post) = explode('~', $tpl, 2);
     echo $pre, $array[$i]['desc'], $post; 
  } else {
     echo " - ", $array[$i]['desc'];
  }
}

Instead do this:

1
2
3
4
5
6
7
8
9
10
if ($mark) {
  list($pre, $post) = explode('~', $tpl, 2);
} else {
  $pre = " - ";
  $post = "";
}
 
for ($i=0, $n=sizeof($array); $i<$n; $i++) {
   echo $pre, $array[$i]['desc'], $post; 
}
if ($mark) {
  list($pre, $post) = explode('~', $tpl, 2);
} else {
  $pre = " - ";
  $post = "";
}

for ($i=0, $n=sizeof($array); $i<$n; $i++) {
   echo $pre, $array[$i]['desc'], $post; 
}

Use create_function to aid you
Sometimes you know you should put things outside of the loop, however the logic is so complex it can only be done in code. In that case you can use create_function together with array_map to help you out.

Don't do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo "<table>";
foreach ($rows as $row) {
  echo "<tr>";
  foreach ($fields as $field) {
    echo "<td>";
    switch ($field['type']) {
      case 'website': echo "<a href="http://", $row[$field['name']], "">", $row[$field['name']], "</a>"; break;
      case 'email': echo "<a href="mailto:", $row[$field['name']], "">", $row[$field['name']], "</a>"; break;
      case 'currency': echo "€ ", number_format($row[$field['name']], 2, ',', '.'); break; // Dutch notation
      default: echo $row[$field['name']];
    }
    echo "</td>";
  }
  echo "</tr>";
}
echo "</table>";
echo "<table>";
foreach ($rows as $row) {
  echo "<tr>";
  foreach ($fields as $field) {
    echo "<td>";
    switch ($field['type']) {
      case 'website': echo "<a href="http://", $row[$field['name']], "">", $row[$field['name']], "</a>"; break;
      case 'email': echo "<a href="mailto:", $row[$field['name']], "">", $row[$field['name']], "</a>"; break;
      case 'currency': echo "€ ", number_format($row[$field['name']], 2, ',', '.'); break; // Dutch notation
      default: echo $row[$field['name']];
    }
    echo "</td>";
  }
  echo "</tr>";
}
echo "</table>";

Instead do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$code = 'echo "<tr>"';
foreach ($fields as $field) {
  $code .= ', "<td>"';
  switch ($field['type']) {
      case 'website': $code .= ', "<a href="http://", $row["' . $field['name'] . '"], "">", $row["' . $field['name'] . '"], "</a>"'; break;
      case 'email': $code .= ', "<a href="mailto:", $row["' . $field['name'] . '"], "">", $row["' . $field['name'] . '"], "</a>"'; break;
      case 'currency': $code .= ', "€ ", number_format($row["' . $field['name'] . '"], 2, ",", ".")'; break; // Dutch notation
      default: $code .= ', $row["' . $field['name'] '"]';
  }
  $code .= ', "</td>"';
}
$code .= ', "</tr>";';
 
echo "<table>";
array_walk($rows, create_function('$row', $code));
echo "</table>";
$code = 'echo "<tr>"';
foreach ($fields as $field) {
  $code .= ', "<td>"';
  switch ($field['type']) {
      case 'website': $code .= ', "<a href="http://", $row["' . $field['name'] . '"], "">", $row["' . $field['name'] . '"], "</a>"'; break;
      case 'email': $code .= ', "<a href="mailto:", $row["' . $field['name'] . '"], "">", $row["' . $field['name'] . '"], "</a>"'; break;
      case 'currency': $code .= ', "€ ", number_format($row["' . $field['name'] . '"], 2, ",", ".")'; break; // Dutch notation
      default: $code .= ', $row["' . $field['name'] '"]';
  }
  $code .= ', "</td>"';
}
$code .= ', "</tr>";';

echo "<table>";
array_walk($rows, create_function('$row', $code));
echo "</table>";

Be careful with abstraction in loops
Things like iterators and overloading (__get/__set) are a lot slower than normal loops and getting and setting properties. When you have a lot of data it is probably better to stay a way of these kind of abstractions, especially if you only use them to make you code more pretty.
Also your own abstraction might be dangerous. The less code that is performed in a loop with lots of data, the better.

Sometimes you can prevent loops altogether
With PHP functions like join, array_sum and array_reduce and MySQL functions like GROUP_CONCAT, you may sometimes get out of using a PHP loop.

1
if (!empty($categories)) $db->query("INSERT INTO product_category (product_id, category_id) VALUES ($prod_id, " . join("), ($prod_id, ", $categories) . ")");
if (!empty($categories)) $db->query("INSERT INTO product_category (product_id, category_id) VALUES ($prod_id, " . join("), ($prod_id, ", $categories) . ")");

Last resort: write an extension
If you have a piece of code in a loop which simply can not be written differently, but is causing mayor performance issues, see if you can rewrite that in C. Compiled code is simply a lot faster and will most likely solve your problem. The syntax of C is much like PHP, though there are a lot of gotcha's. You can turn that C code into a PHP extension, so you can use it in your application. I've used the following articles to get started with PHP extensions:
Extension Writing Part I: Introduction to PHP and Zend
Extension Writing Part II: Parameters, Arrays, and ZVALs
Extension Writing Part II: Parameters, Arrays, and ZVALs [continued]
Extension Writing Part III: Resources

Warning: None of the code in this article is tested (sorry)

Arnold Daniels

I've spend a big part of my life behind a computer, learning about databases (MySQL), programming (PHP) and system administration (Linux). Currently I playing with HTML5, jquery and node.js.

E-mailTwitterLinkedInGithubGittip

There are 12 comments in this article:

  1. 24 January 2008ctx2002 says:

    please read “create_function” doc page in http://www.php.net before recommend it as a tool to increase performance.

  2. 24 January 2008Arnold Daniels says:

    ctx2002,

    What are you pointing at? I’ve already read it, but I’ve now read it again and I have no idea what is wrong. Please be more specific.

  3. 24 January 2008Edward Z. Yang says:

    What ctx2002 is referring to is the fact that when the PHP interpreter hits a create_function block, it needs to shift back into parser mode, parse the PHP code, generate the opcodes, and then execute them.

  4. 24 January 2008Arnold Daniels says:

    Sure, but this isn’t really a problem. This will only cause you a few milliseconds, hardly something to worry about. Again, I’m not a performance purist, removing all abstraction (so not using field definitions) will give you even better results. However if you loop through a large set of data and still want to use the abstraction, this will give you good results. And that is what this article is about, solving performance problems, not trying to squeeze the best performance out of any script. To my opinion, that is nonsense.

  5. 25 January 2008ctx2002 says:

    have you read user’s comments under that page?

    “You really should avoid using this(create_function) as well as you should avoid using eval(). Not only will there be a performance decrease but can it lead to obfuscation and bad coding habits. There is almost always an alternative solution to self modifying code.”

    “Beware when using anonymous functions in PHP as you would in languages like Python, Ruby, Lisp or Javascript. As was stated previously, the allocated memory is never released; they are not objects in PHP — they are just dynamically named global functions — so they don’t have scope and are not subject to garbage collection.

    So, if you’re developing anything remotely reusable (OO or otherwise), I would avoid them like the plague. They’re slow, inefficient and there’s no telling if your implementation will end up in a large loop. Mine ended up in an iteration over ~1 million records and quickly exhasted my 500MB-per-process limit”

    also check out http://blog.libssh2.org/index.php?/archives/60-create_function-is-not-your-friend.html

    regards

  6. 25 January 2008Arnold Daniels says:

    I can agree with you up to some point. Yes the it won’t make the code more beautiful (butt ugly is a better term) and it will perform worse than code that is parsed normally, however you shouldn’t dismiss it so quickly.

    You are talking about a 1 million record loop. That is exactly the kind of big loops I’m taking about. Within such a loop there should be as little as possible. No big if or switch statements, no calling all kind of abstract functions and most definitely no using expensive stuff like create_function(). If you’re able to do that without use create_function, good! use that. However, if I have the choice between putting lots of stuff in the loop and having performance problems, or preparing for the loop and using create_function and have good performance, I choose the latter.

    The fact that code like this might end up anywhere is not really a valid statement. You’ll use this is at a point where you’re going to loop through tens of thousands (or more) rows, It is highly unlikely that that code is in yet another big loop.

    Last, the example given, of creating an HTML table, is quite reusable. It works for any result set, simply pass data and meta-data.

  7. 28 January 2008Unomi says:

    Also try to figure out what will be processed presumably the most of a switch / if statement. Put that as the first statement. Second most second etc.

    Do a for loop with an iterator backwards ($i++ will be $i–), it’s slightly faster. If you’re desperate about your loop, use a Duff’s device or a revision of it. Like this:

    $i = 1933999; // any amount
    $n = (int)($i % 8); // first do a modulo
    $c = 0; // just to check the result

    if ($n > 0) {
      while ($n–) {
        //do some stuff
        $c++;
      }
    }

    $n = (int)($i / 8);

    if ($n > 0) {
      while ($n–) {
        //do some stuff
        $c++;
        //do some stuff
        $c++;
        //do some stuff
        $c++;
        //do some stuff
        $c++;
        //do some stuff
        $c++;
        //do some stuff
        $c++;
        //do some stuff
        $c++;
        //do some stuff
        $c++;
      }
    }

    It is much more code than usual, but it works like 1/3 faster than a normal loop. You have to do the last stuff 8 times, but only if $n > 0; The first does a modulo, so everything not dividable by 8 comes first. The remaining comes after that.

    You could even do query results this way, but I won’t go into that.

    For people saying it is less readable, I agree. But you could get used to it and take this method for faster results.

    - Unomi -

  8. 28 January 2008Arnold Daniels says:

    The code for the loop might be 3x faster, but probably that isn’t in comparison to the processing time of //do some stuff. So in the end I don’t think this will actually help you a lot when you are having performance problems.

    < ?php
      $time = microtime(true);
      for ($i=0; $i<1000000; $i++) {
        // Do nothing
      }
      echo (microtime(true) - $time) * 1000, ' ms';
    ?>

    This script will take about 120ms to run on my computer. A third of that is 40ms. If I replace //Do nothing with something simple as $s .= $i;, the script will take about 500ms.

    A script that actually has a function will probably take a few seconds to process a million items. Saving 40ms than hardly seems worth the trouble.

  9. 29 January 2008Unomi says:

    Arnold,

    Sorry, I can’t follow you…. How do you compare 40ms on seconds, while before you take 120ms and take a third (40ms)?

    A third of a few seconds (say 6) is still (2) seconds gain.

    Instead of:

    for ($i = 1933999; $i > 0; $i–) {
      // do some stuff
    }

    You could use a Duff’s device and save some seconds. I benchmarked it with real functions and gained a third in speed.

    So, if it doesn’t seems worth the trouble… don’t use it.

    - Unomi -

  10. 29 January 2008Arnold Daniels says:

    For other readers: http://en.wikipedia.org/wiki/Duff%27s_device

    If the processing time of the following code is 120ms.
      for ($i=0; $i<1000000; $i++) {
        // Do nothing
      }

    And the processing time of this code is 80ms.
      $i = 1000000;
      $n = (int)($i % 8); // first do a modulo
      $c = 0; // just to check the result
      
      if ($n > 0) {
        while ($n–) {
          // Do nothing
          $c++;
        }
      }
      
      $n = (int)($i / 8);
      
      if ($n > 0) {
        while ($n–) {
          // Do nothing
          $c++;
          // Do nothing
          $c++;
          // Do nothing
          $c++;
          // Do nothing
          $c++;
          // Do nothing
          $c++;
          // Do nothing
          $c++;
          // Do nothing
          $c++;
          // Do nothing
          $c++;
        }
      }
    You would indeed save a third of the processing time.

    But that is only on the processing time for the loop itself. The processing time of the code in the loop doesn’t change.

    Lets say the following code would take about 1120ms to process (forgetting the overhead of calling time_nanosleep):
      for ($i=0; $i<1000000; $i++) {
        time_nanosleep(0, 1000); //sleep for 1ms
      }

    If we would compare it to the processing time of this code:
      $i = 1000000;
      $n = (int)($i % 8); // first do a modulo
      $c = 0; // just to check the result
      
      if ($n > 0) {
        while ($n–) {
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
        }
      }
      
      $n = (int)($i / 8);
      
      if ($n > 0) {
        while ($n–) {
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
          time_nanosleep(0, 1000); //sleep 1 ms
          $c++;
        }
      }
    This function would take 1080ms to process and not 750ms, saving you only 3.5%.

    This is part of the point I wanted to make. Don’t take things out of context and go and benchmark them, because it won’t help you in real-life situations. A Duff’s device is a nice method of squeezing some extra performance out of code that is already optimized. But simply by using PHP that is hardly the case. If you’re you want great performance you should just program that part in C, that will probably increase the performance by an order of magnitude.

  11. 1 February 2008Ian says:

    The code that you use demonstrate building an HTML table with create_function() has several parse errors. The final line that is supposed to append to $code actually overwrites the entire variable, and I believe all of the commas you use to join strings within the $code string you are building should actually be the dot (concat) character.

  12. 1 February 2008Arnold Daniels says:

    Thanks Ian, I’ve been lazy… :/ I should have tested the code.
    The comma’s however are correct. If you look closely you see that they are only within the string and relate to the echo in the line of code created for create_function.