Skip to content

Instantly share code, notes, and snippets.

@donquixote
Last active June 13, 2023 02:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save donquixote/85efcca90056111e967dd14cb1f9de9c to your computer and use it in GitHub Desktop.
Save donquixote/85efcca90056111e967dd14cb1f9de9c to your computer and use it in GitHub Desktop.
Performance comparison for php callables
Horse Implementations Med./ns Avg/ns Dev/%
convert-only-firstclass class.closure.traditional.array 33.10 33.28 0.315
convert-only-firstclass class.closure.firstclass.array 33.12 33.17 0.272
convert-only-firstclass class.closure.traditional.string 33.13 33.22 0.260
convert-only-firstclass class.closure.firstclass.1 33.15 33.31 0.340
convert-only-firstclass function.closure.traditional 33.18 33.33 0.303
convert-only-firstclass class.closure.firstclass.0 33.23 33.31 0.273
convert-only-firstclass function.closure.firstclass 33.39 33.69 0.404
convert-only-traditional class.closure.traditional.array 36.07 36.30 0.242
convert-only-traditional class.closure.traditional.string 36.08 36.32 0.276
convert-only-traditional class.closure.firstclass.1 36.08 36.36 0.310
convert-only-traditional class.closure.firstclass.array 36.10 36.33 0.263
convert-only-traditional class.closure.firstclass.0 36.20 36.31 0.231
convert-only-traditional function.closure.traditional 36.27 36.39 0.262
convert-only-traditional function.closure.firstclass 36.29 36.39 0.224
direct class.closure.firstclass.1 63.86 64.49 0.456
direct class.closure.firstclass.array 63.91 64.42 0.316
direct class.closure.traditional.array 63.98 64.34 0.301
direct class.closure.traditional.string 63.99 64.38 0.280
direct class.closure.firstclass.0 64.00 64.55 0.323
direct function.closure.traditional 64.16 64.82 0.480
direct function.closure.firstclass 64.26 65.06 0.451
direct function.string 87.36 88.67 0.442
convert-only-firstclass function.string 92.60 93.39 0.343
direct class.array 102.60 103.47 0.342
convert-only-firstclass class.array 109.36 109.74 0.298
convert-only-traditional function.string 112.35 112.83 0.239
direct class.string 122.17 122.93 0.314
convert-only-firstclass class.string 129.49 130.08 0.298
orig class.closure.firstclass.array 136.80 137.53 0.318
orig class.closure.firstclass.1 136.89 137.63 0.348
orig1 class.closure.firstclass.1 136.91 138.18 0.406
orig class.closure.traditional.string 136.91 137.82 0.322
orig1 class.closure.traditional.string 136.95 138.79 0.408
orig class.closure.traditional.array 136.96 137.85 0.311
orig1 class.closure.traditional.array 137.01 138.29 0.392
orig class.closure.firstclass.0 137.05 138.04 0.317
orig1 class.closure.firstclass.array 137.06 138.47 0.434
orig1 class.closure.firstclass.0 137.21 138.23 0.315
orig1 function.closure.traditional 137.33 138.59 0.389
orig function.closure.firstclass 137.36 138.11 0.347
orig function.closure.traditional 137.36 138.36 0.356
orig1 function.closure.firstclass 137.89 138.69 0.289
convert-only-traditional class.array 139.83 140.21 0.199
firstclass.direct function.closure.traditional 152.16 153.10 0.293
firstclass.direct class.closure.traditional.string 152.24 153.35 0.332
firstclass.direct class.closure.firstclass.1 152.33 153.63 0.335
firstclass.direct class.closure.firstclass.array 152.39 152.99 0.239
firstclass.direct class.closure.traditional.array 152.39 152.97 0.224
firstclass.direct function.closure.firstclass 152.62 153.90 0.302
firstclass.direct class.closure.firstclass.0 152.66 153.34 0.272
firstclass class.closure.firstclass.array 160.00 161.49 0.391
firstclass class.closure.firstclass.1 160.10 161.34 0.351
firstclass class.closure.traditional.string 160.12 161.73 0.377
firstclass class.closure.traditional.array 160.13 161.05 0.293
firstclass function.closure.traditional 160.15 161.16 0.304
firstclass class.closure.firstclass.0 160.38 161.45 0.338
firstclass function.closure.firstclass 160.85 161.50 0.292
closure class.closure.traditional.string 169.79 170.63 0.234
closure class.closure.traditional.array 169.84 171.36 0.239
closure class.closure.firstclass.array 169.84 171.08 0.264
closure function.closure.traditional 169.89 171.70 0.398
closure class.closure.firstclass.1 169.93 171.44 0.348
closure class.closure.firstclass.0 170.16 171.18 0.258
closure function.closure.firstclass 170.44 171.58 0.303
convert-only-traditional class.string 202.15 202.92 0.182
orig function.string 205.03 205.99 0.287
orig1 function.string 205.38 206.92 0.268
firstclass.direct function.string 232.42 233.66 0.289
firstclass function.string 244.25 245.52 0.272
orig1 class.array 244.33 246.22 0.318
orig class.array 244.34 245.67 0.276
firstclass.direct class.array 247.99 249.52 0.317
firstclass class.array 258.56 259.90 0.324
closure function.string 266.95 268.51 0.288
firstclass.direct class.string 271.74 272.62 0.252
firstclass class.string 283.21 284.20 0.255
closure class.array 294.09 295.59 0.269
orig1 class.string 332.14 333.45 0.246
orig class.string 333.77 335.65 0.269
closure class.string 365.58 366.83 0.227
<?php
namespace N;
(static function () use ($argv) {
// Number of generated functions and classes.
// With very high numbers like 10000, the result of the experiment changes in
// unexpected ways.
$nFunctions = 300;
$paramlist = '$a, $b, $c';
$implementationss = [];
for ($i = 0; $i < $nFunctions; ++$i) {
$function = 'N\\foo_' . $i;
$class = 'N\\C_' . $i;
$php = <<<EOT
namespace N;
function foo_$i($paramlist) {}
class C_$i {
static function f($paramlist) {}
static function getCallback() {
return static::f(...);
}
}
EOT;
eval($php);
foreach ([
'function.string' => $function,
'function.closure.firstclass' => $function(...),
// This should be the same.
'function.closure.traditional' => \Closure::fromCallable($function),
'class.array' => [$class, 'f'],
'class.string' => $class . '::f',
// These are probably all the same.
'class.closure.firstclass.0' => $class::f(...),
'class.closure.firstclass.1' => $class::getCallback(),
'class.closure.firstclass.array' => [$class, 'f'](...),
'class.closure.traditional.array' => \Closure::fromCallable([$class, 'f']),
'class.closure.traditional.string' => \Closure::fromCallable($class . '::f'),
] as $k => $callback) {
$implementationss[$k][] = $callback;
}
}
$args = [1, 2, 3];
$callback = static function (callable $hook) use ($args, &$return) {
# $result = call_user_func_array($hook, $args);
$hook(...$args);
};
$horses = [
'direct' => static function (array $implementations) use ($args) {
foreach ($implementations as $f) {
$f(...$args);
}
},
'orig' => static function (array $implementations) use ($args, $callback) {
foreach ($implementations as $f) {
$callback($f);
}
},
'firstclass' => static function (array $implementations) use ($args, $callback) {
foreach ($implementations as $f) {
$ff = $f(...);
$callback($ff);
}
},
'firstclass.direct' => static function (array $implementations) use ($args, $callback) {
foreach ($implementations as $f) {
$callback($f(...));
}
},
'closure' => static function (array $implementations) use ($args, $callback) {
foreach ($implementations as $f) {
$ff = \Closure::fromCallable($f);
$callback($ff);
}
},
'convert-only-firstclass' => static function (array $implementations) use ($args, $callback) {
foreach ($implementations as $f) {
$f(...);
}
},
'convert-only-traditional' => static function (array $implementations) use ($args, $callback) {
foreach ($implementations as $f) {
\Closure::fromCallable($f);;
}
},
// Register the same one again to detect side effects from ordering.
'orig1' => static function (array $implementations) use ($args, $callback) {
foreach ($implementations as $f) {
$callback($f);
}
},
];
// Warm up. Without this, results depend on the order of horses.
foreach ($horses as $horse) {
foreach ($implementationss as $implementations) {
$horse($implementations);
}
}
$nanotimess = [];
// Odd number is better for median.
$n = 501;
for ($i = 0; $i < $n; ++$i) {
foreach ($horses as $horsename => $horse) {
foreach ($implementationss as $k => $implementations) {
$t0_nanos = hrtime(TRUE);
$horse($implementations);
$dur_nanos = hrtime(TRUE) - $t0_nanos;
// Convert to nanoseconds for better readability.
// Divide by number of functions.
$nanotimess[$horsename . ' | ' . $k][] = $dur_nanos / $nFunctions;
}
}
}
$weights = [];
$rows = [];
foreach ($nanotimess as $key => $nanotimes) {
$n = count($nanotimes);
[$horsename, $k] = explode(' | ', $key);
$average = array_sum($nanotimes) / $n;
$deviation = sqrt(array_sum(array_map(static function ($dt) use ($average) {
return ($dt - $average) * ($dt - $average);
}, $nanotimes))) / $n;
$dev_percent = $deviation / $average * 100;
sort($nanotimes);
$median = $nanotimes[floor($n / 2)];
$weights[] = $median;
$rows[] = [
$horsename,
$k,
number_format($median, 2),
number_format($average, 2),
number_format($dev_percent, 3),
];
}
array_multisort($weights, $rows);
$headers = ['Horse', 'Implementations', 'Med./ns', 'Avg/ns', 'Dev/%'];
$markdown = '| ' . implode(' | ', $headers) . " |\n"
. str_repeat('|-------', count($headers)) . " |\n";
foreach ($rows as $row) {
$markdown .= '| ' . implode(' | ', $row) . " |\n";
}
print $markdown;
})();
<?php
namespace N;
use Drupal\Component\Utility\NestedArray;
(static function () use ($argv) {
$iMax = 10000;
$paramlist = '$a, $b, $c';
$implementationss = [];
for ($i = 0; $i < $iMax; ++$i) {
$function = 'N\\foo_' . $i;
$class = 'N\\C_' . $i;
$php = <<<EOT
namespace N;
function foo_{$i}($paramlist) {}
class C_{$i} {
static function f($paramlist) {}
static function getCallback() {
return static::f(...);
}
}
EOT;
eval($php);
foreach ([
'function' => $function,
'class' => [$class, 'f'],
'class.str' => $class . '::f',
'class.first0' => $class::f(...),
'class.first1' => $class::getCallback(),
] as $k => $callback) {
$implementationss[$k][] = $callback;
$implementationss["$k.closure"][] = \Closure::fromCallable($callback);
$implementationss["$k.firstclass"][] = $callback(...);
}
}
$mode = (string) ($argv[1] ?? 'orig');
$k = (string) ($argv[2] ?? 'function');
$implementations = $implementationss[$k] ?? throw new \Exception("Unexpected '$k'.");
print $mode . ' | ' . $k . "\n";
$args = [1, 2, 3];
$callback = static function (callable $hook) use ($args, &$return) {
# $result = call_user_func_array($hook, $args);
$hook(...$args);
};
$t0 = microtime(TRUE);
for ($j = 0; $j < 10; ++$j) {
switch ($mode) {
case 'direct':
foreach ($implementations as $f) {
$f(...$args);
}
break;
case 'debug':
var_export($implementations[0]);
exit();
case 'orig':
foreach ($implementations as $f) {
$callback($f);
}
break;
case 'map_orig':
array_map($callback, $implementations);
break;
case 'walk_orig':
array_walk($implementations, $callback);
break;
case 'closure':
foreach ($implementations as $f) {
$ff = \Closure::fromCallable($f);
$callback($ff);
}
break;
case 'firstclass':
foreach ($implementations as $f) {
$ff = $f(...);
$callback($ff);
}
break;
default:
print "NOT ACCEPTED: '" . $mode . "'\n";
return;
}
}
print ((microtime(TRUE) - $t0) * 1000) . ' ms' . "\n";
})();
@donquixote
Copy link
Author

donquixote commented Jun 12, 2023

Run in cli as e.g.

php test.php orig function
php test.php firstclass class
php test.php closure function
php test.php orig function.firstclass

Always run each version multiple times to clear variations.
Really 10 times min!

Check the code how $argv[1] and $argv[2] are used.

@andypost
Copy link

Using this diff for fast callable makes class method calls are the same cost as function

-        $callback($ff);
+         $ff(...$args);

@donquixote
Copy link
Author

Using this diff for fast callable makes class method calls are the same cost as function

I'm a bit confused by this statement :)
What exactly you are comparing with "same cost as function"?

@andypost
Copy link

The fastest version is following, implementations can be returned as fisrt-class callables and function calls nearly x2 speed-up

<?php

namespace N;

use Drupal\Component\Utility\NestedArray;

(static function () use ($argv) {
  $iMax = 100000;
  $paramlist = '$a, $b, $c';

  $callbacks = [];
  for ($i = 0; $i < $iMax; ++$i) {
    $php = <<<EOT
namespace N;

function foo_{$i}($paramlist) {}

class C_{$i} {
  static function f($paramlist) {}
}
EOT;

    eval($php);
//    $callbacks['function'][] = \Closure::fromCallable('N\\foo_' . $i);
//    $callbacks['class'][] = \Closure::fromCallable(['N\\C_' . $i, 'f']);
    $callbacks['function'][] = ('N\\foo_' . $i)(...);
    $callbacks['class'][] = ('N\\C_' . $i)::f(...);
  }

  $mode = (string) ($argv[1] ?? 'orig');
  $k = !empty($argv[2]) ? 'class' : 'function';
  $the_callbacks = $callbacks[$k];

  print $mode . ' | ' . $k . "\n";
  $args = [1, 2, 3];
  $callback = static function (callable $hook) use ($args, &$return) {
    # $result = call_user_func_array($hook, $args);
    $hook(...$args);
  };
  $t0 = microtime(TRUE);
  switch ($mode) {
    case 'orig':
      foreach ($the_callbacks as $f) {
        $callback($f);
      }
      break;

    case 'closure':
      foreach ($the_callbacks as $f) {
        //$ff = \Closure::fromCallable($f);
        $callback($f);
      }
      break;

    case 'firstclass':
      foreach ($the_callbacks as $f) {
        //$ff = $f(...);
        $f(...$args);
      }
      break;

    default:
      print "NOT ACCEPTED: '" . $mode . "'\n";
      return;
  }

  print ((microtime(TRUE) - $t0) * 1000) . ' ms' . "\n";
})();

running php 8.3

/var/www/html/web $ php hook.php orig 1
orig | class
50.432205200195 ms
/var/www/html/web $ php hook.php orig 0
orig | function
41.173934936523 ms
/var/www/html/web $ php hook.php firstclass 1
firstclass | class
21.111011505127 ms
/var/www/html/web $ php hook.php firstclass 0
firstclass | function
24.955987930298 ms
/var/www/html/web $ php hook.php closure 0
closure | function
37.466049194336 ms
/var/www/html/web $ php hook.php closure 1
closure | class
41.41902923584 ms

@andypost
Copy link

My numbers for new script
`/var/www/html/web $ php callable-performance-statistics.php``

Horse Implementations Duration
direct class.firstclass 20.177125930786 ms
direct class.first0.firstclass 20.490884780884 ms
direct class.first1.firstclass 21.463394165039 ms
direct class.closure 21.545886993408 ms
direct function.firstclass 21.63028717041 ms
direct class.str.firstclass 23.139953613281 ms
direct class.first0 23.145914077759 ms
direct class.first0.closure 23.294687271118 ms
direct class.first1.closure 25.778532028198 ms
direct function 29.096364974976 ms
firstclass function.closure 32.177925109863 ms
orig function.closure 32.53960609436 ms
firstclass function.firstclass 33.711433410645 ms
firstclass class.firstclass 33.937692642212 ms
firstclass class.str.closure 34.703969955444 ms
firstclass class.first1.firstclass 34.989833831787 ms
direct class.str.closure 35.659074783325 ms
firstclass class.first0.firstclass 36.133289337158 ms
orig class.str.closure 36.141872406006 ms
closure function.closure 36.442518234253 ms
orig class.firstclass 36.524295806885 ms
firstclass class.closure 37.166833877563 ms
orig class.first0.firstclass 37.866830825806 ms
firstclass class.first1 38.102149963379 ms
firstclass class.first1.closure 38.386106491089 ms
closure function.firstclass 38.406372070312 ms
orig function.firstclass 38.838386535645 ms
orig class.first1.firstclass 39.20578956604 ms
firstclass class.first0 39.78419303894 ms
orig class.str.firstclass 39.988279342651 ms
closure class.firstclass 40.032625198364 ms
orig class.first0 40.232181549072 ms
closure class.str.closure 40.549755096436 ms
orig class.first0.closure 40.996789932251 ms
orig class.first1.closure 41.669130325317 ms
orig class.closure 41.853189468384 ms
firstclass class.str.firstclass 41.891813278198 ms
direct function.closure 42.291164398193 ms
firstclass class.first0.closure 42.37174987793 ms
closure class.first1.firstclass 42.494297027588 ms
direct class.first1 42.759656906128 ms
orig function 43.730497360229 ms
closure class.first0.firstclass 44.094800949097 ms
closure class.first0.closure 44.163703918457 ms
closure class.str.firstclass 44.21854019165 ms
closure class.first1.closure 44.34609413147 ms
closure class.closure 44.489860534668 ms
orig class.first1 45.003890991211 ms
closure class.first0 45.227766036987 ms
closure class.first1 45.444488525391 ms
direct class.str 46.279430389404 ms
direct class 51.458835601807 ms
firstclass class 52.775859832764 ms
firstclass function 54.550647735596 ms
closure function 55.697202682495 ms
orig class 60.397863388062 ms
closure class 65.873622894287 ms
firstclass class.str 71.06614112854 ms
closure class.str 96.184492111206 ms
orig class.str 96.925020217896 ms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment