Monday, June 29, 2009

another post of PHP performance

http://php100.wordpress.com/2009/06/26/php-performance-google/

PHP performance tips from Google

Posted by Stas on June 26, 2009

I saw a link on twitter referring to PHP optimization advice from Google. There are a bunch of advices there, some of them are quite sound, if not new – like use latest versions if possible, profile your code, cache whatever can be cached, etc. Some are of doubtful value – like the output buffering one, which could be useful in some situations but do nothing or be worse in others, and if you’re a beginner generally it’s better for you to leave it alone until you’ve solved the real performance problems.

However some of the advices make no sense at best and are potentially harmful at worst. Let’s get to it:

First one: Don’t copy variables for no reason. I don’t know what the author intended to describe there, but PHP engine is refcounting copy-on-write, and there’s absolutely no copying going on when assigning variables as they described it:
$description = strip_tags($_POST['description']);
echo $description;

I don’t know where it comes from but it’s just not so, unless maybe in some prehistoric version of PHP. Which means unless you’re going back to 1997 in a time machine this advice is no good for you.

Next one: Avoid doing SQL queries within a loop. This actually might make sense in some situations, however the code examples they give there is missing one important detail that makes it potentially harmful for beginners (see if you can spot it):
$userData = [];
foreach ($userList as $user) {
$userData[] = '("' . $user['first_name'] . '", "' . $user['last_name'] . '")';
}
$query = 'INSERT INTO users (first_name,last_name) VALUES' . implode(',', $userData);
mysql_query($query);

Please repeat after me – DO NOT INSERT USER DATA INTO SQL WITHOUT SANITIZING IT!
Of course, I can not know that $user was not sanitized. Maybe the intent was that it was. But if you give such example and target beginners, you should say so explicitly, every time! People tend to copy/paste examples, and then you get SQL injection in a government site.

Another thing: most of real-life PHP applications usually do not insert data in bulk, except for some very special scenarios (bulk data imports, etc.) – so actually in most cases one would be better off using PDO and prepared statements. Or some higher-level frameworks which will do it for you. But if you roll your own SQL – sanitize the data! This is much more important than any performance tricks.

Next one: Use single-quotes for long strings. PHP code is parsed and compiled, and any possible difference in speed between parsing “” and ” is really negligible unless you operate with hundreds of megabyte-size strings embedded in your code. If you do so, your quotes probably aren’t where you should start optimizing. And of course, using caching (see below) eliminates this difference altogether.

Next one: Use switch/case instead of if/else. This makes no sense since switch does essentially the same things as if’s do. See for yourself, here is the “if” code:
0 2 A(0) = FETCH_R(C("_POST")) [global]
1 2 A(1) = FETCH_DIM_R(A(0), C("action")) [Standard]
2 2 T(2) = IS_EQUAL(A(1), C("add"))
3 2 JMPZ(T(2), 7)
4 3 INIT_FCALL_BY_NAME(function_table, C("addUser"))
5 3 Au(3) = DO_FCALL_BY_NAME() [0 arguments]
6 4 JMP(16)
7 4 A(4) = FETCH_R(C("_POST")) [global]
8 4 A(5) = FETCH_DIM_R(A(4), C("action")) [Standard]
9 4 T(6) = IS_EQUAL(A(5), C("delete"))
10 4 JMPZ(T(6), 14)
11 5 INIT_FCALL_BY_NAME(function_table, C("deleteUser"))
12 5 Au(7) = DO_FCALL_BY_NAME() [0 arguments]
13 6 JMP(16)
14 7 INIT_FCALL_BY_NAME(function_table, C("defaultAction"))
15 7 Au(8) = DO_FCALL_BY_NAME() [0 arguments]
16 9 RETURN(C(1))
17 9 HANDLE_EXCEPTION()

Here is the “switch” code:
0 2 A(0) = FETCH_R(C("_POST")) [global]
1 2 A(1) = FETCH_DIM_R(A(0), C("action")) [Standard]
2 3 T(2) = CASE(A(1), C("add"))
3 3 JMPZ(T(2), 8 )
4 4 INIT_FCALL_BY_NAME(function_table, C("addUser"))
5 4 Au(3) = DO_FCALL_BY_NAME() [0 arguments]
6 5 BRK(0, C(1))
7 6 JMP(10)
8 6 T(2) = CASE(A(1), C("delete"))
9 6 JMPZ(T(2), 14)
10 7 INIT_FCALL_BY_NAME(function_table, C("deleteUser"))
11 7 Au(4) = DO_FCALL_BY_NAME() [0 arguments]
12 8 BRK(0, C(1))
13 9 JMP(15)
14 9 JMP(19)
15 10 INIT_FCALL_BY_NAME(function_table, C("defaultAction"))
16 10 Au(5) = DO_FCALL_BY_NAME() [0 arguments]
17 11 BRK(0, C(1))
18 12 JMP(20)
19 12 JMP(15)
20 12 SWITCH_FREE(A(1))
21 13 RETURN(C(1))
22 13 HANDLE_EXCEPTION()
No. CONT BRK Parent
0 20 20 -1

You can see there’s a little difference – the latter has CASE/BRK opcodes, which act more or less like IS_EQUAL and JMP, but their plumbing is a bit different, but in general, code is the same (you could even argue “switch” code is a bit less optimal, but that is really the area you shouldn’t be concerned with before you can read and understand the code in zend_vm_def.h – which is not exactly a beginner stuff.

Another thing that the author absolutely failed to mention and which should be one of the very first things anybody who cares about performance should do – is to use a bytecode cache. There are plenty of free ones (shameless plug: Zend Server CE includes one of them – all the performance improvements for $0 and you don’t have to change a bit of code to run it.

Now, I understand Google is not a PHP shop like Yahoo or Facebook or many others. But this article is signed “Eric Higgins, Google Webmaster” and one would expect something much more sound from such source. And in fact there are a lot of blogs and conference talks on the topic and lots of community folks around that I am sure would be ready to help with such article – I wonder why wasn’t it done? Why apparently the best advice we can find from Google is either trivial or useless or wrong?

I think they can do much better, and they should if they take “making the web faster” seriously.

No comments: