PHP 闭包表现出奇怪的性能行为

4
今天早些时候,我正在处理一个 PHP 5.3+ 应用程序,这意味着我可以自由使用 PHP 闭包。我觉得太棒了!然后我遇到了一段代码,在这段代码中,使用函数式的 PHP 代码将使事情变得更加容易,但是,尽管我有一个逻辑上的答案,但它让我想知道在直接调用 array_map() 中的闭包和将其作为变量传递下去之间的性能影响。也就是以下两个示例:
$test_array = array('test', 'test', 'test', 'test', 'test' );
array_map( function ( $item ) { return $item; }, $test_array );

并且

$test_array = array('test', 'test', 'test', 'test', 'test' );
$fn = function ( $item ) { return $item; };
array_map( $fn, $test_array );

正如我所想,后者确实更快,但差别并不是很大。事实上,在重复这些相同的测试10000次并取平均值时,差别只有0.05秒。可能是偶然事件。
这让我更加好奇。那么create_function()和闭包呢?根据经验,create_function()在处理像array_map()这样的函数时应该会更慢,因为它会创建一个函数,对其进行评估,然后将其存储下来。而正如我所想,create_function()确实更慢。这都是使用array_map()完成的。
然后,我不知道为什么要这样做,但我确实这样做了,我检查了create_function()和闭包之间的差异,同时将其保存并只调用一次。没有处理,没有其他操作,只是简单地传递一个字符串,并返回该字符串。
测试结果如下:
$fn = function($item) { return $item; };
$fn('test');

并且

$fn = create_function( '$item', 'return $item;' );
$fn('test');

我运行了这两个测试各10000次,并查看结果并得出平均值。结果让我很惊讶。
结果表明,闭包函数这一次比较慢,大约慢了4倍。我觉得这不可能。我的意思是,通过array_map()运行闭包函数要快得多,而通过变量运行相同的函数,然后通过array_map()运行也更快,这几乎和这个测试相同。
结果如下:
array
  0 => 
    array
      'test' => string 'Closure test' (length=12)
      'iterations' => int 10000
      'time' => float 5.1327705383301E-6
  1 => 
    array
      'test' => string 'Anonymous test' (length=14)
      'iterations' => int 10000
      'time' => float 1.6745710372925E-5

好奇为什么会这样,我检查了CPU使用情况和其他系统资源,并确保没有运行不必要的内容,一切都正常了,所以我再次运行测试,但结果相似。
于是我尝试了相同的测试,只运行了一次,并多次运行它(当然每次都计时)。结果发现闭包确实比create_function()慢4倍,除非偶尔有两到三倍速度比create_function()更快,我猜这只是偶然事件,但似乎足以将测试1000次的时间缩短一半。
下面是我用来进行这些测试的代码。有人能告诉我到底发生了什么吗?是我的代码问题还是PHP出了问题?
<?php

/**
 * Simple class to benchmark code
 */
class Benchmark
{
    /**
     * This will contain the results of the benchmarks.
     * There is no distinction between averages and just one runs
     */
    private $_results = array();

    /**
     * Disable PHP's time limit and PHP's memory limit!
     * These benchmarks may take some resources
     */
    public function __construct() {
        set_time_limit( 0 );
        ini_set('memory_limit', '1024M');
    }

    /**
     * The function that times a piece of code
     * @param string $name Name of the test. Must not have been used before
     * @param callable|closure $callback A callback for the code to run.
     * @param boolean|integer $multiple optional How many times should the code be run,
     * if false, only once, else run it $multiple times, and store the average as the benchmark
     * @return Benchmark $this
     */
    public function time( $name, $callback, $multiple = false )
    {
        if($multiple === false) {
            // run and time the test
            $start = microtime( true );
            $callback();
            $end = microtime( true );

            // add the results to the results array
            $this->_results[] = array(
                'test' => $name,
                'iterations' => 1,
                'time' => $end - $start
            );
        } else {
            // set a default if $multiple is set to true
            if($multiple === true) {
                $multiple = 10000;
            }

            // run the test $multiple times and time it every time
            $total_time = 0;
            for($i=1;$i<=$multiple;$i++) {
                $start = microtime( true );
                $callback();
                $end = microtime( true );
                $total_time += $end - $start;
            }
            // calculate the average and add it to the results
            $this->_results[] = array(
                'test' => $name,
                'iterations' => $multiple,
                'time' => $total_time/$multiple
            );
        }
        return $this; //chainability
    }

    /**
     * Returns all the results
     * @return array $results
     */
    public function get_results()
    {
        return $this->_results;
    }
}

$benchmark = new Benchmark();

$benchmark->time( 'Closure test', function () {
    $fn = function($item) { return $item; };
    $fn('test');
}, true);

$benchmark->time( 'Anonymous test', function () {
    $fn = create_function( '$item', 'return $item;' );
    $fn('test');
}, true);

$benchmark->time( 'Closure direct', function () {
    $test_array = array('test', 'test', 'test', 'test', 'test' );
    $test_array = array_map( function ( $item ) { return $item; }, $test_array );
}, true);

$benchmark->time( 'Closure stored', function () {
    $test_array = array('test', 'test', 'test', 'test', 'test' );
    $fn = function ( $item ) { return $item; };
    $test_array = array_map( $fn, $test_array );
}, true);

$benchmark->time( 'Anonymous direct', function () {
    $test_array = array('test', 'test', 'test', 'test', 'test' );
    $test_array = array_map( create_function( '$item', 'return $item;' ), $test_array );
}, true);

$benchmark->time( 'Anonymous stored', function () {
    $test_array = array('test', 'test', 'test', 'test', 'test' );
    $fn = create_function( '$item', 'return $item;' );
    $test_array = array_map( $fn, $test_array );
}, true);

var_dump($benchmark->get_results());

这段代码的结果是:

array
  0 => 
    array
      'test' => string 'Closure test' (length=12)
      'iterations' => int 10000
      'time' => float 5.4110765457153E-6
  1 => 
    array
      'test' => string 'Anonymous test' (length=14)
      'iterations' => int 10000
      'time' => float 1.6784238815308E-5
  2 => 
    array
      'test' => string 'Closure direct' (length=14)
      'iterations' => int 10000
      'time' => float 1.5178990364075E-5
  3 => 
    array
      'test' => string 'Closure stored' (length=14)
      'iterations' => int 10000
      'time' => float 1.5463256835938E-5
  4 => 
    array
      'test' => string 'Anonymous direct' (length=16)
      'iterations' => int 10000
      'time' => float 2.7537250518799E-5
  5 => 
    array
      'test' => string 'Anonymous stored' (length=16)
      'iterations' => int 10000
      'time' => float 2.8293371200562E-5

1
这并不让我太惊讶,因为那些匿名函数实际上是class的实例。 - bfavaretto
首先,你的基准代码虽然令人印象深刻,但本身可能会产生开销。我总是建议在for循环中运行代码多次,测量整个循环所需的时间。当你进入10^-6范围时,时间变得奇怪。 - Yarek T
3
你意识到5.1327705383301E-6比1.6745710372925E-5要小得多吗? - user102008
@user102008 哇,我简直不敢相信我错过了那个!确实如此,在稍微调整代码后,它现在给出了一个没有指数的时间字符串!你想把它作为答案添加进去,这样我就可以标记它为已接受吗? - GManz
2个回答

19

5.1327705383301E-6不是比1.6745710372925E-5慢4倍,而是快了大约3倍。你读错数字了。看起来在你的所有结果中,闭包函数都比create_function更快。


2

看这个基准测试:

<?php
$iter = 100000;


$start = microtime(true);
for ($i = 0; $i < $iter; $i++) {}
$end = microtime(true) - $start;
echo "Loop overhead: ".PHP_EOL;
echo "$end seconds".PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < $iter; $i++) {
    $fn = function($item) { return $item; };
    $fn('test');
}
$end = microtime(true) - $start;
echo "Lambda function: ".PHP_EOL;
echo "$end seconds".PHP_EOL;


$start = microtime(true);
for ($i = 0; $i < $iter; $i++) {
    $fn = create_function( '$item', 'return $item;' );
    $fn('test');
}
$end = microtime(true) - $start;
echo "Eval create function: ".PHP_EOL;
echo "$end seconds".PHP_EOL;

结果:

Loop overhead: 
0.011878967285156 seconds
Lambda function: 
0.067019939422607 seconds
Eval create function: 
1.5625419616699 seconds

有趣的是,如果你将函数声明放在循环之外

Loop overhead: 
0.0057950019836426 seconds
Lambda function: 
0.030204057693481 seconds
Eval create function: 
0.040947198867798 seconds

回答您最初的问题,将lambda函数分配给变量和直接使用它之间没有区别。除非您使用它超过一次,在这种情况下,使用变量可以更好地提高代码清晰度。


我认为你忽略了一个重要点,那就是我在循环内计时,因此循环开销并未被计算在内。 - GManz
当使用mircotime()计时这样的排序事件时,它变得相当不准确。即使循环开销被计算在内,你仍然可以清楚地看到这些函数需要预期的时间,而任何执行eval()的操作都会更慢。 - Yarek T

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接