<?php
namespace Lavoiesl\PhpBenchmark;
class Profiler
{
private $start_memory = 0;
private $max_memory = 0;
private $start_time = null;
private $end_time = null;
public function start()
{
$this->start_memory = $this->max_memory = memory_get_usage(true);
$this->start_time = microtime(true);
register_tick_function( array( $this, "tick" ) );
}
public function tick()
{
$this->max_memory = max($this->max_memory, memory_get_usage(true));
}
public function stop()
{
$this->tick();
$this->end_time = microtime(true);
unregister_tick_function( array( $this, "tick" ) );
}
function getMemoryUsage()
{
return $this->max_memory - $this->start_memory;
}
function getTime()
{
return $this->end_time - $this->start_time;
}
}
abstract class AbstractTest
{
/**
* @var string
*/
private $name;
private $profiler;
public function __construct($name)
{
$this->name = $name;
$this->profiler = new Profiler;
}
public function getName()
{
return $this->name;
}
public function run($n = 1)
{
$this->prepare();
gc_collect_cycles(); // clear memory before start
$this->profiler->start();
for ($i=0; $i < $n; $i++) {
// Store the result so it appears in memory profiling
$result = $this->execute();
unset($result);
}
$this->profiler->stop();
$results = array(
'time' => $this->profiler->getTime(),
'memory' => $this->profiler->getMemoryUsage(),
'n' => $n,
);
$this->cleanup();
return $results;
}
protected function prepare()
{
}
abstract protected function execute();
protected function cleanup()
{
}
public function guessCount($max_seconds = 1)
{
$this->run(); // warmup
$once = $this->run();
if ($once['time'] >= $max_seconds) {
return 1;
} else {
return round($max_seconds / $once['time']);
}
}
}
class SimpleTest extends AbstractTest
{
/**
* @var \Closure
*/
private $prepare = null;
/**
* @var \Closure
*/
private $execute;
/**
* @var \Closure
*/
private $cleanup = null;
public function __construct($name, \Closure $execute)
{
parent::__construct($name);
$this->execute = $execute;
}
public function setPrepare(\Closure $prepare)
{
$this->prepare = $prepare;
return $this;
}
protected function prepare()
{
if ($prepare = $this->prepare) {
$prepare();
}
}
protected function execute()
{
return call_user_func($this->execute);
}
public function setCleanup(\Closure $cleanup)
{
$this->cleanup = $cleanup;
return $this;
}
protected function cleanup()
{
if ($cleanup = $this->cleanup) {
$cleanup();
}
}
}
class Benchmark
{
/**
* @var array [Test]
*/
private $tests = array();
private $n = null;
private $base_results;
public function addTest(AbstractTest $test)
{
$this->tests[$test->getName()] = $test;
}
/**
* Utility method to create tests on the fly
* You may chain the test:
*
* @param string $name
* @param \Closure $closure function to execute
* @return SimpleTest
*/
public function add($name, \Closure $closure)
{
$test = new SimpleTest($name, $closure);
$this->addTest($test);
return $test;
}
/**
* Runs an empty test to determine the benchmark overhead and run each test once
*/
private function warmup()
{
$warmup = new SimpleTest('warmup', function(){});
$warmup->run();
foreach ($this->tests as $test) {
$test->run();
}
$this->base_results = $warmup->run($this->n);
}
public function run($output = true)
{
$results = array();
if (null === $this->n) {
$this->guessCount(2); // aim for around 2 seconds per test
}
if ($output) {
echo "Running tests {$this->n} times.\n";
}
$this->warmup();
$i = 0;
foreach ($this->tests as $name => $test) {
if ($output) {
echo "Testing ".++$i."/".count($this->tests)." : $name\r";
}
$results[$name] = $test->run($this->n);
}
if ($output) {
echo "\n\n";
self::outputTable(self::formatResults($results));
}
return $results;
}
public function setCount($n)
{
$this->n = $n;
}
/**
* Average the guessCount of each test, determining the best n
*
* @param float $max_seconds
* @return int
*/
public function guessCount($max_seconds)
{
if (!$this->tests) {
throw new \RuntimeException('No test in Benchmark.');
}
$n = INF;
foreach ($this->tests as $test) {
$n = min($n, $test->guessCount($max_seconds));
}
return $this->n = Util::round($n);
}
/**
* Output results in columns, padding right if values are string, left if numeric
*
* @param array $lines array(array('Name' => 'Value'));
* @param integer $padding space between columns
*/
public static function outputTable(array $lines, $padding = 3)
{
if (!$lines) {
return;
}
$pad = function ($string, $width) use ($padding) {
if ($width > 0) {
return str_pad($string, $width, " ") . str_repeat(' ' , $padding);
} else {
return str_pad($string, -$width, " ", STR_PAD_LEFT) . str_repeat(' ' , $padding);
}
};
// init width with keys' length
$cols = array_combine(array_keys($lines[0]), array_map('strlen', array_keys($lines[0])));
foreach ($cols as $col => $width) {
foreach ($lines as $line) {
$width = max($width, strlen($line[$col]));
}
// pad left if numeric
if (preg_match('/^[0-9]/', $line[$col])) {
$width = -$width;
}
echo $pad($col, $width);
$cols[$col] = $width;
}
echo "\n";
foreach ($lines as $line) {
foreach ($cols as $col => $width) {
echo $pad($line[$col], $width);
}
echo "\n";
}
}
/**
* Format the results, rounding numbers, showing difference percentages
* and removing a flat time based on the benchmark overhead
*
* @param array $results array($name => array('time' => 1.0))
* @return array array(array('Test' => $name, 'Time' => '1000 ms', 'Perc' => '100 %'))
*/
public function formatResults(array $results)
{
uasort($results, function($a, $b) {
if ($a['time'] == $b['time'])
return 0;
else
return ($a['time'] < $b['time']) ? -1 : 1;
});
$min_time = INF;
$min_memory = INF;
foreach ($results as $name => $result) {
$time = $result['time'];
$time -= $this->base_results['time']; // Substract base_time
$time *= 1000; // Convert to ms
$time = round($time);
$time = max(1, $time); // min 1 ms
$min_time = min($min_time, $time);
$results[$name]['time'] = $time;
$min_memory = min($min_memory, $results[$name]['memory']);
}
$output = array();
foreach ($results as $name => $result) {
$output[] = array(
'Test' => $name,
'Time' => $result['time'] . ' ms',
'Time (%)' => Util::relativePerc($min_time, $result['time']),
'Memory' => Util::convertToSI($result['memory']),
'Memory (%)' => Util::relativePerc($min_memory, $result['memory']),
);
}
return $output;
}
}
class Util
{
public static function round($number, $significant = 0)
{
$order = floor(log($number) / log(10));
return round($number / pow(10, $order), $significant) * pow(10, $order);
}
/**
* Converts 1024 to 1K, etc.
*
* @param double $number i.e.: 1280
* @param integer $precision i.e.: 1.25 for precision = 2
* @param string $unit suffix of the unit, may be empty
* @param integer $factor change base to 1000 or 1024
* @return string i.e.: 1.25 kB
*/
public static function convertToSI($number, $precision = 2, $unit = 'B', $factor = 1024)
{
static $sizes = array(
'-3' => 'n',
'-2' => 'ยต',
'-1' => 'm',
'0' => '',
'1' => 'k',
'2' => 'M',
'3' => 'G',
'4' => 'T'
);
$scale = $number == 0 ? 0 : floor(log($number, $factor));
return round($number / pow($factor, $scale), $precision) . ' ' . $sizes[$scale] . $unit = 'B';
}
public static function relativePerc($min, $value) {
if ($min == 0 || $min == $value) {
return '';
} else {
return round(($value - $min) / $min * 100) . ' %';
}
}
}
$benchmark = new Benchmark;
function multiexplode ($delimiters,$string) {
$ready = str_replace($delimiters, $delimiters[0], $string);
$launch = explode($delimiters[0], $ready);
return $launch;
}
function homemadeexplode($splitter, $str)
{
$return = [];
if (is_array($splitter) == true) {
foreach ($splitter as $sp) {
$str = homemadeexplode($sp, $str);
}
$return = $str;
} else {
if (is_array($str) == true) {
foreach ($str as $st) {
$tmp = explode($splitter, $st);
$return = array_merge($return, $tmp);
}
} else {
$return = explode($splitter, $str);
}
}
return $return;
}
$text = "here is a sample: this text, and this will be exploded. this also | this one too :)";
$benchmark->add('multiexplode', function() use($text) { return multiexplode( [",",".","|",":"], $text); });
$benchmark->add('homemadeExplode', function() use($text) { return homemadeexplode([",",".","|",":"], $text); });
$benchmark->add('preg_split', function() use($text) { return preg_split( '/[,.|:]/', $text); });
$benchmark->add('mb_split', function() use($text) { return mb_split( '[,.|:]', $text); });
$benchmark->setCount(50000);
$benchmark->run();
preferences:
38.41 ms | 402 KiB | 5 Q