Showing posts with label coding style. Show all posts
Showing posts with label coding style. Show all posts

For or Foreach? PHP vs. Javascript, C++, Java, HHVM (update: Go, Zephir)

Lessons learned:
  • Foreach is 4-5 times faster than For
  • Nested Foreach is 2-3 times faster than nested For
  • Foreach with key lookup is 2-3 times slower than Foreach without
  • C++ is 5-300 times faster than PHP running For/Foreach on Arrays
  • HHVM is 2-3 times faster than PHP
  • PHP 7 is 2-4 times faster than PHP 5.5
  • HHVM is currently no alternative to C++
  • Javascript is 2-20 times slower than C++/Java running For on nested Arrays
  • Go is 4-20 times faster than HHVM

Here is a sample script:

<?php
function test(){
// init arrays
$array = array();
for ($i=0; $i<50000; $i++) $array[] = $i*2;

$array2 = array();
for ($i=20000; $i<21000; $i++) $array2[] = $i*2;

// test1: foreach big-array (foreach small-array)
$start = microtime(true);
foreach ($array as $val) {
foreach ($array2 as $val2) if ($val == $val2) {}
}
echo number_format(microtime(true)-$start, 2)."s\n";

// test1b: foreach big-array (foreach small-array)
$start = microtime(true);
foreach ($array as $val) {
foreach ($array2 as $val2) if ($val === $val2) {}
}
echo number_format(microtime(true)-$start, 2)."s\n";

// test2: foreach small-array (foreach big-array)
$start = microtime(true);
foreach ($array2 as $val2) {
foreach ($array as $val) if ($val == $val2) {}
}
echo number_format(microtime(true)-$start, 2)."s\n";

// test3: foreach big-array (foreach small-array) with key lookup
$start = microtime(true);
foreach ($array as $key=>$val) {
foreach ($array2 as $key2=>$val2) if ($array[$key] == $array2[$key2]) {}
}
echo number_format(microtime(true)-$start, 2)."s\n";

// test4: foreach small-array (foreach big-array) with key lookup
$start = microtime(true);
foreach ($array2 as $key=>$val2) {
foreach ($array as $val) if ($array[$key] == $array2[$key2]) {}
}
echo number_format(microtime(true)-$start, 2)."s\n";

// test5: for big-array (for small-array)
$start = microtime(true);
$count = count($array);
$count2 = count($array2);
for ($key=0; $key<$count; $key++) {
for ($key2=0; $key2<$count2; $key2++) if ($array[$key] == $array2[$key2]) {}
}
echo number_format(microtime(true)-$start, 2)."s\n";

// test6: for small-array (for big-array)
$start = microtime(true);
$count = count($array);
$count2 = count($array2);
for ($key2=0; $key2<$count2; $key2++) {
for ($key=0; $key<$count; $key++) if ($array[$key] == $array2[$key2]) {}
}
echo number_format(microtime(true)-$start, 2)."s\n";

$array = array();
for ($i=0; $i<1000000; $i++) $array[] = $i*2;

// test7: foreach big-array
$start = microtime(true);
foreach ($array as &$val) $val++;
echo number_format(microtime(true)-$start, 2)."s\n";

// test8: for big-array
$start = microtime(true);
for ($key=0; $key<count($array); $key++) $array[$key]++;
echo number_format(microtime(true)-$start, 2)."s\n";

// test8b: for big-array, doing count() outside the loop!
$start = microtime(true);
$count = count($array);
for ($key=0; $key<$count; $key++) $array[$key]++;
echo number_format(microtime(true)-$start, 2)."s\n";
}
test();

Here are some results from PHP 5.4.4 and HHVM (2014-05-04, QEMU 2.3 GHz, 64bit):
php hhvm
2.78s0.47s
2.90s0.44s
2.97s0.44s
6.90s1.36s
6.27s1.33s
5.83s1.13s
6.24s1.15s
0.07s0.04s
0.24s0.04s
0.11s0.03s
Using HHVM instead of PHP gives big improvements.

With Javascript (node.js) you'll get similar values:
example.js (run: node example.js)

var array = [];
for (i=0; i<50000; i++) array.push(i*2);

var array2 = [];
for (i=20000; i<21000; i++) array2.push(i*2);

// js-test1: for big-array (for small array)
var start = new Date().getTime();
var length = array.length;
var length2 = array2.length;
for (key=0; key<length; key++) {
for (key2=0; key2<length2; key2++) if (array[key] == array2[key2]) {}
}
console.log((new Date().getTime() - start) / 1000); // 1.53s

// js-test2: foreach big-array (foreach small array)
start = new Date().getTime();
for (key in array) {
for (key2 in array2) if (array[key] == array2[key2]) {}
}
console.log((new Date().getTime() - start) / 1000); // 6.32s

var array3 = [];
for (i=0; i<1000000; i++) array3.push(i*2);

// js-test3: for big-array
start = new Date().getTime();
length3 = array3.length;
for (key=0; key<length3; key++) array3[key]++;
console.log((new Date().getTime() - start) / 1000); // 0.03s
tested with QEMU 2.3 GHz, node.js v0.10

With C++ (gcc 4.6 win32) you'll also get similar values:
example.cpp (run: g++ -o example example.cpp && ./example)

#include <sys/time.h>
#include <stdio.h>
#include <vector>
using namespace std;

main() {
struct timeval start, end;

vector<int> array;
for(int i=0; i < 50000; i++) array.push_back(i*2);

vector<int> array2;
for(int i=20000; i < 21000; i++) array2.push_back(i*2);

gettimeofday(&start, NULL);
int array_size = array.size();
int array2_size = array2.size();
for (int key=0; key<array_size; key++)
for (int key2=0; key2<array2_size; key2++)
if (array[key] == array2[key2]) {}

gettimeofday(&end, NULL);
printf("%lf\n", (float)(end.tv_sec - start.tv_sec +
(end.tv_usec - start.tv_usec)/1000000.0)); // 0.61s, 0.00s (-O3)

vector<int> array3;
for(int i=0; i < 1000000; i++) array3.push_back(i*2);

gettimeofday(&start, NULL);
int array3_size = array3.size();
for(int i=0; i < array3_size; i++) array3[i]++;
gettimeofday(&end, NULL);
printf("%lf\n", (float)(end.tv_sec - start.tv_sec +
(end.tv_usec - start.tv_usec)/1000000.0)); // 0.009s, 0.001s (-O3)
}
tested with QEMU 2.3 GHz, gcc 4.7

And Java (Java 1.7 win64):

example.java (run: javac -g:none example.java && java example)

import java.util.ArrayList;
import java.util.Vector;

public class example {
public static void main(String[] args) throws Exception {

ArrayList<Integer> array = new ArrayList<Integer>();
for (int i = 0; i < 50000; i++) array.add(i*2);

ArrayList<Integer> array2 = new ArrayList<Integer>();
for (int i = 20000; i < 21000; i++) array2.add(i*2);

long start = System.currentTimeMillis();
int array_size = array.size();
int array2_size = array2.size();
for (int key = 0; key < array_size; key++)
for (int key2 = 0; key2 < array2_size; key2++)
if (array.get(key).equals(array2.get(key2))) {}
System.out.println((float) (System.currentTimeMillis() - start) / 1000);
// 0.066s

Vector<Integer> varray = new Vector<Integer>();
for (int i = 0; i < 50000; i++) varray.add(i*2);

Vector<Integer> varray2 = new Vector<Integer>();
for (int i = 20000; i < 21000; i++) varray2.add(i*2);

start = System.currentTimeMillis();
int varray_size = varray.size();
int varray2_size = varray2.size();
for (int key = 0; key < varray_size; key++)
for (int key2 = 0; key2 < varray2_size; key2++)
if (varray.get(key).equals(varray2.get(key2))) {}
System.out.println((float) (System.currentTimeMillis() - start) / 1000);
// 1.652s

ArrayList<Integer> array3 = new ArrayList<Integer>();
for (int i = 0; i < 1000000; i++) array3.add(i*2);

start = System.currentTimeMillis();
int array3_size = array3.size();
for (int i = 0; i < array3_size; i++) array3.set(i, array3.get(i)+1);
System.out.println((float)(System.currentTimeMillis() - start) / 1000);
// 0.164s

Vector<Integer> varray3 = new Vector<Integer>();
for (int i = 0; i < 1000000; i++) varray3.add(i*2);

start = System.currentTimeMillis();
int varray3_size = varray3.size();
for (int i = 0; i < varray3_size; i++) varray3.set(i, varray3.get(i)+1);
System.out.println((float)(System.currentTimeMillis() - start) / 1000);
// 0.074s
}
}
tested with QEMU 2.3 GHz, OpenJDK 1.6, 64bit

Go (1.4.2):

example.go (run: go build example.go && ./example)

package main

import "fmt"
import "time"

func main() {
var array [50000]int
for i := 0; i < 50000; i++ { array[i] = i*2 }

var array2 [1000]int
for i := 20000; i < 21000; i++ { array2[i-20000] = i*2 }

t := time.Now()
length := len(array)
length2 := len(array2)
for key := 0; key < length; key++ {
for key2 := 0; key2 < length2; key2++ {
if (array[key] == array2[key2]) {}
}
}
fmt.Println(time.Now().Sub(t)) // 157.855682ms

var array3 [1000000]int
for i := 0; i < 1000000; i++ { array3[i] = i*2 }

t2 := time.Now()
length3 := len(array3)
for key := 0; key < length3; key++ { array3[key]++ }
fmt.Println(time.Now().Sub(t2)) // 4.363528ms
}

Zephir (0.7.1b):

example.zep (run: zephir init utils && vi utils/example.zep && zephir build && php -d extension=utils.so -r 'echo Utils\Example::run();')

namespace Utils;

class Example {

public static function run() {
int i, i2;
array array1 = [];
let i = 0;
while (i < 50000) {
let array1[] = i*2;
let i++;
}

array array2 = [];
let i = 20000;
while (i < 21000) {
let array2[] = i*2;
let i++;
}

var start;
let start = microtime(true);
int length, length2;
let length = count(array1);
let length2 = count(array2);
let i = 0, i2 = 0;
while (i < length) {
while (i2 < length2) {
if (array1[i] == array2[i2]) {}
let i2++;
}
let i++;
}
echo (microtime(true) - start) . PHP_EOL;

array array3 = [];
let i = 0;
while (i < 1000000) {
let array3[] = i*2;
let i++;
}

let start = microtime(true);
int length3;
let length3 = count(array3);
let i = 0;
while (i < length3) {
let array3[i] += 1;
let i++;
}
echo (microtime(true) - start) . PHP_EOL;
}
}

New results (AMD Opteron 6128 2GHz virtualized):

php
5.5.9

4.71
4.77
6.43
9.20
10.81
8.76
11.09
0.15
0.37
0.20

php 7.0
2015-8-1

1.34
1.69
1.39
3.79
3.36
3.51
3.69
0.11
0.12
0.08

hhvm
3.8.1

0.65
0.61
0.68
1.34
1.44
1.06
1.14
0.09
0.07
0.05

node.js
0.10.25

1.953
9.218





0.051

c++
gcc 4.8.4

0.91601






0.01149

c++ -O3
gcc 4.8.4

0.00000






0.00101

go
1.4.2

0.15866






0.00427

OpenJDK
1.7.0_79

0.114
3.732





0.124
0.253

Zephir
0.7.1b

0.0001






0.1510

Note:

int len = array.size(); for (int key=0; key < len; key++)
instead of

for (int key=0; key < array.size(); key++)
makes the code 30 percent faster!

Decorator or Subclassing?

Using anonymous functions in PHP is very nice to implement a decorator, but what about performance? Results:
  • Subclassing is 40 percent faster than using a decorator
  • Subclassing might require a bit more code

Here is the code:

class App {
public static function route($pattern, $callback, $args) {
// evaluate $pattern ...
call_user_func_array($callback, $args);
}
}

class AppJson extends App {
public static function route($pattern, $callback, $args) {
// evaluate $pattern ...
$str = json_encode(call_user_func_array($callback, $args));
}
}


$start = microtime(true);
for ($i=0; $i<10000; $i++) {
AppJson::route('/json/range', 'range', [0,10]);
}
echo ' '.(microtime(true)-$start); // 0.1058s


$json = function ($func) {
return function() use (&$func) {
$str = json_encode(call_user_func_array($func, func_get_args()));
};
};

$start = microtime(true);
for ($i=0; $i<10000; $i++) {
App::route('/json/range', $json('range'), [0,10]);
}
echo ' '.(microtime(true)-$start); // 0.1763s

Array key lookup: isset() or array_key_exists() or @ ?

Lessons learned:
  • isset() is faster than array_key_exists()
  • array_key_exists() is faster than @
  • @ is slower than ignoring notices with error_reporting()

Here is the code running on a 1.4 GHz machine with PHP 5.4.0:

error_reporting(E_ALL & ~E_NOTICE);

$a = array();
for ($i=0; $i<100000; $i++) $a[] = $i*2;

$start = microtime(true);
for ($i=0; $i<100000; $i++) if (isset($a[$i])) {}
echo ' '.(microtime(true)-$start); // 0.017

$start = microtime(true);
for ($i=0; $i<100000; $i++) if (array_key_exists($i, $a)) {}
echo ' '.(microtime(true)-$start); // 0.064

$start = microtime(true);
for ($i=0; $i<100000; $i++) if (@$a[$i]) {}
echo ' '.(microtime(true)-$start); // 0.095

$start = microtime(true);
for ($i=0; $i<100000; $i++) if ($a[$i]) {}
echo ' '.(microtime(true)-$start); // 0.016

$a = array();

$start = microtime(true);
for ($i=0; $i<100000; $i++) if (isset($a[$i])) {}
echo ' '.(microtime(true)-$start); // 0.016

$start = microtime(true);
for ($i=0; $i<100000; $i++) if (array_key_exists($i, $a)) {}
echo ' '.(microtime(true)-$start); // 0.058

$start = microtime(true);
for ($i=0; $i<100000; $i++) if (@$a[$i]) {}
echo ' '.(microtime(true)-$start); // 0.29

$start = microtime(true);
for ($i=0; $i<100000; $i++) if ($a[$i]) {}
echo ' '.(microtime(true)-$start); // 0.20

Disadvantages of ORM

ORM has attracted a lot of attention in the last years. So let's get a bit deeper into it.

The biggest advantage of ORM is also the biggest disadvantage: queries are generated automatically
  • queries can't be optimized
  • queries select more data than needed, things get slower, more latency
    (some ORMs fetch all datasets of all relations of an object even though only 1 attribute is read)
  • compiling queries from ORM code is slow (ORM compiler written in PHP)
  • SQL is more powerful than ORM query languages
  • database abstraction forbids vendor specific optimizations

Other problems coming up with ORM
  • compiling ORM logic from phpDoc instructions or XML files is slow, but can be cached
  • ORM validates relations and field names outside the database, but can't keep relations consistent
  • ORM libraries are often used in projects without making a benchmark before
  • ORM libraries are often used because the documentation of the library says it is very fast
  • ORM libraries are often used by default without checking the project's needs
  • database abstraction is often required but changing the database never happens
  • databases are not object oriented
  • ORM violates the basic database performance principle: you get the best performance when your data is stored in the same structure it gets read

General coding problems with ORM
  • having objects instead of SQL, programmers tend to write joins directly in PHP
  • ORM code can be much longer than normal code with PHP and SQL
    (increase of complexity, error rates and maintenance efforts)
  • how to handle null values? (assign null => isset gives false)
  • people often document PHP code but not the database schemas
    (e.g. empty comments in MySQL fields and tables, docs not up-to-date)
  • new versions of ORM libraries often forbid reusing older ORM code
  • slow code is often wrapped with caching, so you always serve old data

Where can ORM be good?
  • avoid building SQL strings for simple insert, update, delete
  • using ORM with magic getters/setters in PHP
  • allow models to inherit attributes and methods from other models
  • separate models from views and controllers
  • centralize validation rules, save or delete methods to one class per entity
  • handle escaping and serialization of values automatically

Performance in numbers?
e.g. Doctrine 2, watch slide 50 and 54: Doctrine is >3 times slower than raw PHP on 20 inserts, imagine what happens with 20000 ... real numbers are much slower, see slide 47, here the authors only benchmarked flush() instead of the whole code

Coming soon: How to write a really small and fast O/R-mapper with PHP

Things you should not do in PHP (update: references)

Here is a list of things you should not do in PHP. Most of the stuff is pretty obvious, but over the years I've seen a lot of them. In most cases, these problems remain hidden until data grows above 10000 entries. So on a development system, things are always fast and there are no problems with memory limits :-)

Suppose we have a table with 100k entries:

$db->query('create table stats (c1 int(11) primary key, c2 varchar(255))');
$db->query('begin');
for ($i=0; $i<100000; $i++) {
$db->query('insert into stats values ('.$i.','.($i*2).')');
}
$db->query('commit');
Populate a big array instead of streaming results:

$result = $db->query('select * from stats');
$array = $result->fetch_all(); // 35M
// or
while ($row = $result->fetch_assoc()) $array[] = $row; // 35M
// or
while ($row = $result->fetch_array()) $array[] = $row; // 44.5M
// process $array ...

// instead of:
while ($row = $result->fetch_assoc()) { // 0.5M
// process $row
}
Sum with PHP instead of SQL:

$sum = 0;
foreach ($array as $val) $sum += $val[0]; // 44M, 1.2s

// instead of:
list($sum,) = $db->query('select sum(t1) from stats')->fetch_row(); // 0.2M, 0.1s
Sort with PHP instead of SQL:

usort($array, function ($a, $b) { return $a[0] > $b[0]; }); // 4.1s
// or
foreach ($array as $key=>$val) $helper[$key] = $val[0];
asort($helper); // 2.2s

// instead of:
$result = $db->query('select * from stats order by c1');
while ($row = $result->fetch_assoc()) { // 1.2s
Let's add a second table:

$db->query('create table stats2 (c1 int(11) primary key, c2 varchar(255))');
$db->query('begin');
for ($i=50000; $i<51000; $i++) {
$db->query('insert into stats2 values ('.$i.','.($i*2).')');
}
$db->query('commit');
Join with PHP instead of SQL (join result contains 1000 entries):

$array = $db->query("select * from stats")->fetch_all();
$array2 = $db->query("select * from stats2")->fetch_all();

foreach ($array as $key=>$val) {
foreach ($array2 as $key2=>$val2) { // 35.7M, 69s
if ($val[0] == $val2[0]) // do sth.
}
}

// instead of:
$result = $db->query('select * from stats a, stats2 b where a.t1=b.t1');
while ($row = $result->fetch_array()) { // 0.5M, 0.015s
Modify arrays without references:

$array = array();
for ($i=0; $i<1000000; $i++) $array[] = $i*2;

$start = microtime(true);
foreach ($array as &$val) $val++;
echo (memory_get_peak_usage(true)/1048576)."\n"; // 80M (32bit), 200M (64bit)
echo (microtime(true)-$start)."\n"; // 0.14s

$start = microtime(true);
foreach ($array as $key=>$val) $array[$key]++;
echo (memory_get_peak_usage(true)/1048576)."\n"; // 161M (32bit), 399M (64bit)
echo (microtime(true)-$start)."\n"; // 0.64s
more examples coming ...

Scripts running on a 1.4 GHz machine with PHP 5.4.0.