Showing posts with label benchmark. Show all posts
Showing posts with label benchmark. Show all posts

Recursion with PHP 7, HHVM 3.8, Javascript, Java and C/C++ (Update: Go, Zephir)

Lessons learned:
  • HHVM runs recursive function calls 20 times faster than PHP
  • HHVM runs recursive function calls as fast as Javascript
  • HHVM runs recursive function calls 3 times slower than Java
  • HHVM runs recursive function calls 5 times slower than C
  • HHVM runs recursive function calls 2 times slower than Go

Here is a sample script using the Ackermann function:

<?php

function ack($n, $m) {
if ($n == 0) return $m + 1;
else if ($m == 0) return ack($n - 1, 1);
else return ack($n - 1, ack($n, $m - 1));
}

function ack_while($n, $m) {
while ($n != 0) {
if ($m == 0) $m = 1;
else $m = ack_while($n, $m - 1);
$n--;
}
return $m + 1;
}

$start = microtime(true);
ack_while(3, 10);
echo number_format(microtime(true)-$start, 4).'s'.PHP_EOL;

$start = microtime(true);
ack(3, 10);
echo number_format(microtime(true)-$start, 4).'s'.PHP_EOL;

Here is a sample script using the Fibonacci function:

<?php

function fib_it($n) {
$a = 0;
$b = 1;
for ($i = 0; $i < $n; $i++){
$sum = $a+$b;
$a = $b;
$b = $sum;
}
return $a;
}

function fib_rec($n) {
if ($n < 3) return 1;
return fib_rec($n - 1) + fib_rec($n - 2);
}

$start = microtime(true);
fib_it(40);
echo number_format(microtime(true)-$start, 4).'s'.PHP_EOL;

$start = microtime(true);
fib_rec(40);
echo number_format(microtime(true)-$start, 4).'s'.PHP_EOL;

Results: (AMD Opteron 6128 3Ghz virtualized, 64bit)

fib(40) PHP 5.5.9:
0.0000s
45.0391s

fib(40) PHP 7.0.0:
0.0000s
19.4653s

fib(40) with HHVM 3.8.1:
0.0060s
1.7428s

fib(40) with Go 1.2.1:
0.0000017s
0.9577085s

fib(40) with Java OpenJDK 1.7:
0.0s
0.565s

fib(40) with C (gcc 4.7):
0.358s

fib(40) with Javascript (node.js 0.10.25):
0.007s
1.667s

fib(40) with Zephir (0.7.1b):
0.000s
did not finish.


ack(3,10) PHP 5.5.9:
14.5458s
16.1864s

ack(3,10) PHP 7.0.0:
3.5186s
6.0263s

ack(3,10) with HHVM 3.8.1:
Fatal error: Stack overflow in /ack.php on line 12

ack(3,10) with Go 1.2.1:
0.292307s
0.346981s

ack(3,10) with Java OpenJDK 1.7:
0.121
0.222

ack(3,10) with C (gcc 4.7):
0.090s

ack(3,10) with Javascript (node.js 0.10.25):
0.378s
0.657s

ack(3,10) with Zephir (0.7.1b):
PHP Fatal error: Maximum recursion depth exceeded in Command line code on line 1

ack.c (run: gcc -O3 -o ack.out ack.c && time ./ack.out)

unsigned int ack(unsigned int n, unsigned int m) {
if (n == 0) return m + 1;
else if (m == 0) return ack(n - 1, 1);
else return ack(n - 1, ack(n, m - 1));
}

unsigned int ack_while(unsigned int n, unsigned int m) {
while (n != 0) {
if (m == 0) {
m = 1;
} else {
m = ack_while(n, m - 1);
}
n--;
}
return m + 1;
}

int main(int argc, char* argv[]) {
ack_while(3, 10);
ack(3, 10);
}

fib.c (run: gcc -O3 -o fib.out fib.c && time ./fib.out)

unsigned int fib_it(unsigned int n) {
unsigned int a = 0;
unsigned int b = 1;
unsigned int sum;
unsigned int i;
for (i = 0; i < n; i++){
sum = a + b;
a = b;
b = sum;
}
return a;
}

unsigned int fib_rec(unsigned int n) {
if (n < 3) return 1;
return fib_rec(n - 1) + fib_rec(n - 2);
}

int main(int argc, char* argv[]) {
fib_it(40);
fib_rec(40);
}

ack.js (run: time nodejs ack.js)

function ack(n, m) {
if (n == 0) return m + 1;
else if (m == 0) return ack(n - 1, 1);
else return ack(n - 1, ack(n, m - 1));
}

function ack_while(n, m) {
while (n != 0) {
if (m == 0) {
m = 1;
} else {
m = ack_while(n, m - 1);
}
n--;
}
return m + 1;
}

var start = new Date().getTime();
ack_while(3, 10);
console.log((new Date().getTime() - start) / 1000 + 's');

start = new Date().getTime();
ack(3, 10);
console.log((new Date().getTime() - start) / 1000 + 's');

fib.js (run: time nodejs fib.js)

function fib_it(n) {
var a = 0;
var b = 1;
var sum = 0;
for (var i = 0; i < n; i++){
sum = a + b;
a = b;
b = sum;
}
return a;
}

function fib_rec(n) {
if (n < 3) return 1;
return fib_rec(n - 1) + fib_rec(n - 2);
}

var start = new Date().getTime();
fib_it(40);
console.log((new Date().getTime() - start) / 1000 + 's');

start = new Date().getTime();
fib_rec(40);
console.log((new Date().getTime() - start) / 1000 + 's');

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

public class ack {
public static int ack(int n, int m) {
if (n == 0) return m + 1;
else if (m == 0) return ack(n - 1, 1);
else return ack(n - 1, ack(n, m - 1));
}

public static int ack_while(int n, int m) {
while (n != 0) {
if (m == 0) {
m = 1;
} else {
m = ack_while(n, m - 1);
}
n--;
}
return m + 1;
}

public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
ack_while(3, 10);
System.out.println((float) (System.currentTimeMillis() - start) / 1000);

start = System.currentTimeMillis();
ack(3, 10);
System.out.println((float)(System.currentTimeMillis() - start) / 1000);
}
}

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

public class fib {
public static int fib_it(int n) {
int a = 0;
int b = 1;
int sum;
for (int i = 0; i < n; i++){
sum = a + b;
a = b;
b = sum;
}
return a;
}

public static int fib_rec(int n) {
if (n < 3) return 1;
return fib_rec(n - 1) + fib_rec(n - 2);
}

public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
fib_it(40);
System.out.println((float) (System.currentTimeMillis() - start) / 1000);

start = System.currentTimeMillis();
fib_rec(40);
System.out.println((float)(System.currentTimeMillis() - start) / 1000);
}
}

fib.go (run: go run fib.go)

package main

import "fmt"
import "time"

func main() {
t := time.Now()
fib_it(40)
fmt.Println(time.Now().Sub(t))

t = time.Now()
fib_rec(40)
fmt.Println(time.Now().Sub(t))
}

func fib_it(n int) int {
a := 0
b := 1
var sum int

for i := 0; i < n; i++ {
sum = a + b
a = b
b = sum
}
return a
}

func fib_rec(n int) int {
if n < 3 {
return 1
}
return fib_rec(n - 1) + fib_rec(n - 2)
}

ack.go (run: go run ack.go)

package main

import "fmt"
import "time"

func main() {
t := time.Now()
ack_while(3, 10)
fmt.Println(time.Now().Sub(t))

t = time.Now()
ack(3, 10)
fmt.Println(time.Now().Sub(t))
}

func ack_while(n int, m int) int {
for i := n; i > 0; i-- {
if m == 0 {
m = 1
} else {
m = ack_while(i, m - 1)
}
}
return m + 1
}

func ack(n int, m int) int {
if n == 0 {
return m + 1
} else if m == 0 {
return ack(n - 1, 1)
}
return ack(n - 1, ack(n, m - 1));
}

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() {
var start;
let start = microtime(true);
self::ack(3, 8);
echo (microtime(true) - start) . PHP_EOL;

let start = microtime(true);
self::ack_while(3, 8);
echo (microtime(true) - start) . PHP_EOL;
}

public static function ack(int n, int m) {
if (n == 0) {
return m + 1;
} elseif (m == 0) {
return self::ack(n - 1, 1);
} else {
return self::ack(n - 1, self::ack(n, m - 1));
}
}

public static function ack_while(int n, var m) {
while (n != 0) {
if (m == 0) {
let m = 1;
} else {
let m = self::ack_while(n, m - 1);
}
let n--;
}
return m + 1;
}
}

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() {
var start;
let start = microtime(true);
self::fib_it(40);
echo (microtime(true) - start) . PHP_EOL;

let start = microtime(true);
self::fib_rec(40);
echo (microtime(true) - start) . PHP_EOL;
}

public static function fib_it(int n) {
int a = 0;
int b = 1;
int sum;

int i = 0;
while (i < n) {
let sum = a + b;
let a = b;
let b = sum;
let i++;
}
return a;
}

public static function fib_rec(int n) {
if (n < 3) {
return 1;
}
return self::fib_rec(n - 1) + self::fib_rec(n - 2);
}
}

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!

strpos() vs. preg_match() vs. stripos()

Lessons learned:
  • strpos() is 3-16 times faster than preg_match()
  • stripos() is 2-30 times slower than strpos()
  • stripos() is 20-100 percent faster than preg_match() with the caseless modifier "//i"
  • using a regular expression in preg_match() is not faster than using a long string
  • using the utf8 modifier "//u" in preg_match() makes it 2 times slower

Here is a sample script to compare the functions with different string sizes:

<?php

function loop(){

$str_50 = str_repeat('a', 50).str_repeat('b', 50);
$str_100 = str_repeat('a', 100).str_repeat('b', 100);
$str_500 = str_repeat('a', 250).str_repeat('b', 250);
$str_1k = str_repeat('a', 1024).str_repeat('b', 1024);
$str_10k = str_repeat('a', 10240).str_repeat('b', 1024);
$str_100k = str_repeat('a', 102400).str_repeat('b', 1024);
$str_500k = str_repeat('a', 1024*500).str_repeat('b', 1024);
$str_1m = str_repeat('a', 1024*1024).str_repeat('b', 1024);

$b = 'b';
$b_10 = str_repeat('b', 10);
$b_100 = str_repeat('b', 100);
$b_1k = str_repeat('b', 1024);

echo str_replace(',', "\t", ',strpos,preg,preg U,preg S,preg regex,stripos,preg u,'.
'preg i,preg u i,preg i regex,stripos uc,preg i uc,preg i uc regex').PHP_EOL;

foreach (array($b, $b_10, $b_100, $b_1k) as $needle) {
foreach (array($str_50, $str_100, $str_500, $str_1k, $str_10k,
$str_100k, $str_500k, $str_1m) as $str) {

echo strlen($needle).'/'.strlen($str);

$start = mt();
for ($i=0; $i<25000; $i++) $j = strpos($str, $needle); // strpos
echo "\t".mt($start);

$regex = '!'.$needle.'!';
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg
echo "\t".mt($start);

$regex = '!'.$needle.'!U';
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg Ungreedy
echo "\t".mt($start);

$regex = '!'.$needle.'!S';
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg extra analysiS
echo "\t".mt($start);

$regex = "!b{".strlen($needle)."}!";
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg regex
echo "\t".mt($start);

$start = mt();
for ($i=0; $i<25000; $i++) $j = stripos($str, $needle); // stripos
echo "\t".mt($start);

$regex = '!'.$needle.'!u';
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg utf-8
echo "\t".mt($start);

$regex = '!'.$needle.'!i';
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg i
echo "\t".mt($start);

$regex = '!'.$needle.'!ui';
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg i utf-8
echo "\t".mt($start);

$regex = "!b{".strlen($needle)."}!i";
$start = mt();
for ($i=0; $i<25000; $i++) $j = preg_match($regex, $str); // preg i regex
echo "\t".mt($start);

echo PHP_EOL;
}
echo PHP_EOL;
}
}

function mt($start=null){
if ($start === null) return microtime(true);
return number_format(microtime(true)-$start, 4);
}

loop();

Running with PHP 5.4.4, 64bit, 2.3GHz (QEMU):
strpospregpreg Upreg S preg regexstripospreg upreg i  preg u i preg i regex
1/1000.00520.01440.01470.01720.01680.01020.01800.01530.01950.0143
1/2000.00580.01770.01660.01590.01430.01210.02290.01600.02360.0159
1/5000.00610.02370.02460.02360.02190.02150.03940.02170.03830.0212
1/20480.00870.05890.05900.05800.03590.06450.12050.04990.10770.0472
1/112640.02210.26930.26720.26400.24430.31150.59390.36310.68030.3662
1/1034240.18672.34322.33842.35752.32112.93275.30403.46896.43913.4750
1/5130241.022811.400611.353012.068911.385814.502326.070117.265831.654316.9941
1/10496002.098823.356123.338223.381123.445930.211952.993734.965464.414135.1033
10/1000.00550.01670.01710.01660.01480.01040.01990.01580.02150.0150
10/2000.00530.01650.01550.01800.01510.01250.02220.01670.02640.0175
10/5000.00650.02470.02260.02190.01790.02070.03910.02220.03760.0236
10/20480.00880.05900.05780.06250.03710.06490.11920.05030.11080.0484
10/112640.02120.27010.26520.26170.24120.30680.58380.35530.66520.3491
10/1034240.15422.32742.29722.29382.27652.75865.17893.40966.31193.3939
10/5130240.723611.438811.488011.493211.331214.185125.792717.138832.190217.5287
10/10496001.495123.407223.200023.417523.243929.679453.052634.964164.986135.9626
100/1000.00630.19630.19560.01860.12640.01460.29220.20610.22810.1397
100/2000.00600.02340.02190.02190.02120.01800.02960.03090.04220.0235
100/5000.00710.03040.02970.02910.02500.02730.04520.03760.05590.0310
100/20480.01030.06710.06810.06760.04280.07620.13290.06650.12390.0516
100/112640.02100.26250.26230.26420.24220.30670.57620.36000.67740.3548
100/1034240.14742.28642.28562.31142.29682.78585.22613.45066.32823.4003
100/5130240.724111.405411.428611.343511.531314.150725.688517.170231.819517.1162
100/10496001.473623.225823.472423.259523.305229.427653.405135.201564.381834.6691
1024/1000.00470.20330.20410.05170.10420.00830.28990.20920.22900.1204
1024/2000.00490.05620.05540.05630.29000.01090.06150.53400.64960.3281
1024/5000.00490.06470.06380.06371.33580.01810.07612.50633.28111.4076
1024/20480.00690.09550.09540.09630.06880.08870.15260.15360.23670.0809
1024/112640.01950.29740.29820.30000.28170.33750.61680.47180.81030.3845
1024/1034240.15242.33562.33162.34772.33382.83515.27273.55356.47033.4583
1024/5130240.721711.428311.537711.498711.416214.286225.813117.176233.163318.1542
1024/10496001.465723.327223.220523.275223.681229.180454.996234.972064.752937.1609

Running with HHVM 3.0/2014.03.27, 64bit, 2.3GHz (QEMU):
strpospregpreg Upreg S preg regexstripospreg upreg i  preg u i preg i regex
1/1000.00280.00810.00850.00790.00790.00390.01240.00840.01250.0085
1/2000.00210.00930.00940.00920.00940.00450.01670.00910.01620.0088
1/5000.00210.01600.01830.01680.01210.01020.03410.01540.03090.0138
1/20480.00510.05410.05310.05230.03020.03740.11180.03960.09800.0394
1/112640.01760.25310.25370.25410.23730.34150.56820.34510.66020.3436
1/1034240.17812.29552.28462.28142.25353.38805.21393.37446.27483.4123
1/5130241.042511.347711.665311.414911.301816.922726.714217.259531.654117.2308
1/10496002.124123.263523.252523.305423.225634.950852.701234.902464.522134.9430
10/1000.00130.00840.00840.00860.00760.00400.01260.00860.01190.0077
10/2000.00140.00890.00910.00910.00880.00630.01580.01080.01670.0095
10/5000.00200.01550.01580.01570.01200.01290.03080.01560.03080.0147
10/20480.00730.05000.04960.05050.02950.04770.10880.04100.10320.0397
10/112640.01690.25360.25220.25190.23120.45720.56680.34980.66140.3443
10/1034240.18292.31302.29262.30032.28264.55265.23293.45116.42433.4623
10/5130241.061011.535811.524811.494811.419822.937225.916517.067431.421416.9746
10/10496002.078323.813123.731923.198823.209846.539853.548035.472865.456135.6284
100/1000.00120.00650.00620.00660.00580.00170.01120.00660.01010.0059
100/2000.00130.01020.01010.01030.01180.01060.01720.01730.02500.0126
100/5000.00180.01650.01630.01660.01490.01700.03200.02190.03830.0176
100/20480.00420.05050.05030.05080.03180.05100.10900.04730.10850.0432
100/112640.01780.25560.25350.25520.23560.46050.56900.35370.68250.3559
100/1034240.17642.29922.30042.29662.28814.55225.19243.43466.29673.4354
100/5130241.029611.365711.377611.304611.412522.588926.007116.988632.371016.9588
100/10496002.106423.423923.323623.217423.192946.475653.412934.854265.245735.4284
1024/1000.00130.01160.01170.01210.00600.00090.02160.01220.01540.0058
1024/2000.00090.01510.01510.01540.00710.00090.02790.01390.02010.0074
1024/5000.00090.02160.02160.02190.01040.00090.04260.01900.03310.0132
1024/20480.00300.05570.05560.05620.06320.09390.11970.11730.19470.0742
1024/112640.01610.26240.25900.25880.26590.49810.57890.41640.75760.3794
1024/1034240.14672.29022.31722.29942.28574.56485.21193.47266.36263.4485
1024/5130240.716011.387111.434511.303411.456122.756125.856417.033331.401416.9700
1024/10496001.486423.177123.103323.414323.336447.551355.756136.790064.784635.1457

The power of column stores

  • using column stores instead of row based stores can reduce access logs from 10 GB to 130 MB of disk space
  • reading compressed log files is 4 times faster than reading uncompressed files from hard disk
  • column stores can speed up analytical queries by a factor of 18-58

Normally, log files from a web server are stored in a single file. For archiving, log files get compressed with gzip. A typical line in a log file represents one request and looks like this:


173.15.3.XXX - - [30/May/2012:00:37:35 +0200] "GET /cms/ext/files/Sgs01Thumbs/sgs_pmwiki2.jpg HTTP/1.1" 200 14241 "http://www.simple-groupware.de/cms/ManualPrint" "Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20100101 Firefox/12.0"
Compression speeds up reading the log file:

$start = microtime(true);
$fp = gzopen("httpd.log.gz", "r");
while (!gzeof($fp)) gzread($fp, 8192);
gzclose($fp);
echo (microtime(true)-$start)."s\n"; // 26s

$start = microtime(true);
$fp = fopen("httpd.log", "r");
while (!feof($fp)) fread($fp, 8192);
fclose($fp);
echo (microtime(true)-$start)."s\n"; // 105s
(PHP 5.4.5, 2.5 GHz, hard disk with 7200rpm)

Having a log file of 10 GB gives a compressed file with 600 MB using gzip. This is already quite good, but can we make it better?

In the example line, we have different attributes (=columns) separated by " " and []. For example:


ip=173.15.3.XXX
date=30/May/2012:00:37:35 +0200
status=200
length=14241
url=http://www.simple-groupware.de/cms/ManualPrint
agent=Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20100101 Firefox/12.0
etc.

When we save each column in a separate file, we will get smaller files after the compression. Using a file for the HTTP status code column contains many similar values (mostly 200), which allows better compression.

Coming from 600 MB, we can reduce the total size to 280 MB by saving each column to a different file.

Analyzing files containing only one column is also much easier. For example, we can do a group by over all status codes on the shell with:


time zcat status.log.gz | sort | uniq -c
36689940 200
11880 206
124560 301
1142820 302
968040 304
3600 401
37080 403
784260 404
180 405
6480 408

real 0m31.600s
user 0m27.038s
sys 0m1.436s
Note: instead of sorting, it will be faster to fill a small hash table with the counts.

Counting is also very fast:


# get number of requests with status code equal to 200
time zcat status.log.gz | grep -E "^200$" | wc -l
36689940

real 0m3.078s
user 0m2.944s
sys 0m0.128s

# get number of requests with status code not equal to 200
time zcat status.log.gz | grep -Ev ^200$ | wc -l
3078900

real 0m1.799s
user 0m1.736s
sys 0m0.060s

Summing:


# sum up all transferred bytes (5.5951e+11 ~ 521 GB)
time zcat length.log.gz | awk '{s+=$1}END{print s}'
5.5951e+11

real 0m5.708s
user 0m5.556s
sys 0m0.144s

The biggest column is normally the one containing URLs (118 MB in our example). We can reduce the size by assigning a unique ID to each URL and save the list of URLs in a separate file.

Coming from 280 MB, we can reduce the total size to 130 MB by splitting URLs, referers and user agents into 2 files.

Here is the code for splitting a log file into column based files:

$urls = [];
$agents = [];
$split_urls = true;
$split_agents = true;

$fp_ip = gzopen("ip.log.gz", "w");
$fp_user = gzopen("user.log.gz", "w");
$fp_client = gzopen("client.log.gz", "w");
$fp_date = gzopen("date.log.gz", "w");
$fp_url = gzopen("urls.log.gz", "w");
$fp_url_list = gzopen("urls_list.log.gz", "w");
$fp_status = gzopen("status.log.gz", "w");
$fp_length = gzopen("length.log.gz", "w");
$fp_referer = gzopen("referer.log.gz", "w");
$fp_agent = gzopen("agent.log.gz", "w");
$fp_agent_list = gzopen("agent_list.log.gz", "w");

$fp = gzopen("httpd.log.gz", "r");
while (!gzeof($fp)) {
$line = gzgets($fp, 8192);
preg_match("!^([0-9\.]+) ([^ ]+) ([^ ]+) \[([^\]]+)\] \"(?:GET )?([^\"]*)".
" HTTP/1\.[01]\" ([^ ]+) ([^ ]+) \"([^\"]*)\" \"([^\"]*)\"\$!", $line, $m);
if (empty($m)) {
echo $line." ###"; // output broken lines
continue;
}
gzwrite($fp_ip, $m[1]."\n");
gzwrite($fp_user, ($m[2]=="-" ? "" : $m[2])."\n");
gzwrite($fp_client, ($m[3]=="-" ? "" : $m[3])."\n");
gzwrite($fp_date, $m[4]."\n");

if ($split_urls) {
if (!isset($urls[$m[5]])) $urls[$m[5]] = count($urls)-1;
gzwrite($fp_url, $urls[$m[5]]."\n");
} else {
gzwrite($fp_url, $m[5]."\n");
}
gzwrite($fp_status, $m[6]."\n");
gzwrite($fp_length, ($m[7]=="-" ? "0" : $m[7])."\n");

if ($m[8]=="-") {
gzwrite($fp_referer, "\n");
} else if ($split_urls) {
if (!isset($urls[$m[8]])) $urls[$m[8]] = count($urls)-1;
gzwrite($fp_referer, $urls[$m[8]]."\n");
} else {
gzwrite($fp_referer, $m[8]."\n");
}
if ($m[9]=="-") {
gzwrite($fp_agent, "\n");
} else if ($split_agents) {
if (!isset($agents[$m[9]])) $agents[$m[9]] = count($agents)-1;
gzwrite($fp_agent, $agents[$m[9]]."\n");
} else {
gzwrite($fp_agent, $m[9]."\n");
}
}
gzwrite($fp_url_list, implode("\n", array_keys($urls)));
gzwrite($fp_agent_list, implode("\n", array_keys($agents)));

gzclose($fp);
gzclose($fp_ip);
gzclose($fp_user);
gzclose($fp_client);
gzclose($fp_date);
gzclose($fp_url);
gzclose($fp_url_list);
gzclose($fp_status);
gzclose($fp_length);
gzclose($fp_referer);
gzclose($fp_agent);
gzclose($fp_agent_list);
Note: Reading data from disk is always slower than analyzing data in real-time when the data is in memory.

How to implement a real life benchmark with PHP

To determine the maximum capacity of a web page, Apache ab is often used in the first step. Fetching one URL very often is optimal for caching and gives a best case. To get the worst case for caching, it is necessary to fetch different URLs in a random order.

Here is a PHP script to walk randomly on a web page:

To get the average case concerning caching and response times, we need to choose the most relevant links. For example, we skip links from headers and footers. This can be done by using a different xpath expression in the code:

// fetch all links under <div id="content">...</div>
$xpath = '//div[@id="content"]//a';

// fetch all links under <div id="content"> and <div id="menu">
$xpath = '//div[@id="content" or @id="menu"]//a';

To make the benchmark more realistic, you can define a waiting period between two requests: Uncomment "// sleep(1)" at the end of the script.
To get the right values for $limit (number of pages per user) and $processes (number of users), you can consult your favorite analytics tool.

Example output:

php random_crawler.php >details.log

Testing http://www.spiegel.de/, 100 requests, 10 processes
#2393 start 10 requests
#2395 start 10 requests
#2396 start 10 requests
#2394 start 10 requests
#2399 start 10 requests
#2397 start 10 requests
#2398 start 10 requests
#2402 start 10 requests
#2401 start 10 requests
#2400 start 10 requests
#2398 end 188/815 KB 3.54s 0.35s/req
#2393 end 176/751 KB 3.78s 0.38s/req
#2396 end 153/562 KB 3.90s 0.39s/req
#2401 end 137/628 KB 4.19s 0.42s/req
#2397 end 149/456 KB 4.89s 0.49s/req
#2399 end 156/525 KB 4.90s 0.49s/req
#2402 end 171/619 KB 5.95s 0.60s/req
#2400 end 127/349 KB 7.40s 0.74s/req
#2394 end 157/465 KB 8.36s 0.84s/req
#2395 end 167/662 KB 10.62s 1.06s/req
(sizes shown as compressed/uncompressed)