0

Function performance — inlining, overhead, and profiling

Advanced5 min read·php-06-016
performance

Concept

Understanding PHP function call overhead and how to profile it is essential for writing fast code in hot paths. Modern PHP is much faster than PHP 5, but function calls still have measurable cost.

Function call overhead: Each PHP function call involves: looking up the function by name (hash map lookup, O(1)), creating a new call frame on the VM stack, copying arguments (with COW for arrays), executing the opcodes, returning the value, and freeing the frame. For a function that does no real work, this is ~50–200ns on modern hardware. For a tight loop of 1 million calls, that's 50–200ms of pure overhead.

Closures are slower than named functions: A closure object must be created and its binding checked on each invocation. The overhead is small (~2-5%) but measurable in tight loops. Prefer named functions for performance-critical inner loops.

Built-in functions vs userland: PHP's built-in functions (array_map, strlen, in_array) are implemented in C and execute much faster than equivalent PHP code. strlen() is basically just reading the len field of zend_string — it's close to free. A PHP implementation of strlen would be orders of magnitude slower.

Profiling tools:

  • Xdebug profiler: Generates cachegrind files, analyzed with KCachegrind/QCacheGrind. Exact call counts and timings.
  • Blackfire.io: Production-safe sampling profiler with a web UI and comparison between runs.
  • microtime(true): Quick-and-dirty benchmarks for isolated comparisons.
  • SPX (open source): Low-overhead sampling profiler.

Inlining: OPcache will NOT inline PHP functions (unlike JIT compilers in Java or C). The JIT can optimize tight loops but won't eliminate function call overhead in the general case.

Code Example

php
<?php
declare(strict_types=1);

// Benchmark helper
function bench(string $label, callable $fn, int $iterations = 100_000): void
{
    $start = hrtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn($i);
    }
    $ns = (hrtime(true) - $start) / $iterations;
    echo sprintf("%-30s %.1f ns/call\n", $label, $ns);
}

// Named function vs closure vs method
function namedDouble(int $n): int { return $n * 2; }
class Doubler { public function double(int $n): int { return $n * 2; } }
$obj = new Doubler();

bench('Named function', 'namedDouble');
bench('Closure',        fn($n) => $n * 2);
bench('Method',         [$obj, 'double']);
// Named: ~50ns, Closure: ~60ns, Method: ~55ns (approximate)

// PHP built-in vs equivalent userland
function myStrlen(string $s): int
{
    $n = 0;
    foreach (str_split($s) as $_) $n++;
    return $n;
}
$str = str_repeat('x', 100);
bench('strlen() built-in',    fn($_) => strlen($str));
bench('myStrlen() userland',  fn($_) => myStrlen($str));
// strlen: ~40ns, myStrlen: ~20,000ns (500× slower)

// Function call overhead in hot loops — worth inlining
$data = range(1, 1_000_000);

// With function call
$t = microtime(true);
$sum = array_reduce($data, fn($c, $v) => $c + $v, 0);
echo "array_reduce closure: " . round((microtime(true)-$t)*1000) . "ms\n";

// Built-in
$t = microtime(true);
$sum = array_sum($data);
echo "array_sum built-in:   " . round((microtime(true)-$t)*1000) . "ms\n";
// array_sum is dramatically faster — C implementation, no PHP call overhead