在PHP中基准测试内存使用情况

22

假设我们有一个问题,至少有两个解决方案。我们想要的是比较它们的效率。如何做到这一点?显然,最好的答案是:进行测试。对于特定于语言的问题(例如“对于PHP,echo 'foo','bar'echo('foo'. 'bar')哪个更快”),我怀疑还有更好的方法。

现在,如果我们想测试一些代码,假设它等同于测试某些函数。为什么呢?因为我们可以将该代码封装到函数中,并将它的上下文(如果有)作为其参数传递。因此,我们所需要的只是拥有一个基准函数,它将完成所有工作。下面是一个非常简单的例子:

function benchmark(callable $function, $args=null, $count=1)
{
   $time = microtime(1);
   for($i=0; $i<$count; $i++)
   {
      $result = is_array($args)?
                call_user_func_array($function, $args):
                call_user_func_array($function);
   }
   return [
      'total_time'   => microtime(1) - $time,
      'average_time' => (microtime(1) - $time)/$count,
      'count'        => $count
   ];
}

-这将适用于我们的问题,可以用来进行比较基准测试。在“比较”中,我指的是我们可以先使用上述函数测试代码X,然后测试代码Y,然后我们可以说代码X比代码Y快/慢Z%

问题

好的,我们可以轻松地测量时间。但内存呢?我们之前的假设“如果我们想测试一些代码,就等于测试一些函数”似乎在这里不成立。为什么?因为这个假设虽然从形式上是正确的,但如果我们把代码隐藏在函数内部,就永远无法在此之后测量内存。例如:

function foo($x, $y)
{
   $bar = array_fill(0, $y, str_repeat('bar', $x));
   //do stuff
}

function baz($n)
{
   //do stuff, resulting in $x, $y
   $bee = foo($x, $y);
   //do other stuff
}

-并且我们想要测试baz -也就是它在执行过程中会使用多少内存。所谓“如何”是指“函数执行期间最大内存使用量是多少”。很明显,我们不能像测量执行时间那样行事 -因为我们对函数之外的内容一无所知-它是一个黑盒子。实际上,我们甚至不能确定函数是否能够成功执行(想象一下,如果某种方式下baz内的$ x $ y 分配为1E6,会发生什么情况)。因此,将我们的代码包装在函数内可能不是一个好主意。但是,如果代码本身包含其他函数/方法调用呢?

我的方案

我当前的想法是创建一个函数,可以在每个输入代码行后测量内存。这意味着像这样的东西:假设我们有以下代码:

$x = foo();
echo($x);
$y = bar();

-做完某事后,测量函数会执行:

$memory = memory_get_usage();
$max    = 0;

$x = foo();//line 1 of code
$memory = memory_get_usage()-$memory;
$max    = $memory>$max:$memory:$max;
$memory = memory_get_usage();

echo($x);//second line of code
$memory = memory_get_usage()-$memory;
$max    = $memory>$max:$memory:$max;
$memory = memory_get_usage();

$y = bar();//third line of code
$memory = memory_get_usage()-$memory;
$max    = $memory>$max:$memory:$max;
$memory = memory_get_usage();

//our result is $max

-但看起来很奇怪,而且它没有回答一个问题-如何测量函数的内存使用情况。

用例

该用例是:在大多数情况下,复杂度理论可以为某些代码提供至少big-O估计值。但:

  • 首先,代码可能非常庞大-我希望尽可能地避免手动分析。这就是为什么我的当前想法不好的原因:它可以应用,是的,但仍然需要手动处理代码。而且,为了深入代码的结构,我需要递归应用它:例如,在应用了顶层后,我发现一些foo()函数占用了太多内存。我要做什么?是的,转到此foo()函数,并且...在其中重复我的分析。以此类推。
  • 其次-正如我所提到的,有些特定于语言的事情只能通过进行测试来解决。这就是为什么像时间测量那样拥有一些自动方式是我的目标。

另外,启用了垃圾回收。我正在使用PHP 5.5(我相信这很重要)

问题

我们如何有效地测量某个函数的内存使用情况?在PHP中是否可行?也许可以使用一些简单的代码(如上面用于时间测量的benchmark函数)吗?


正如你所说,这高度取决于内存管理,如果使用垃圾回收,那么它非常重要:http://php.net/manual/de/features.gc.php - ZoolWay
@ZoolWay 你说得对。我已经更新了。我正在使用 PHP 5.5,并且启用了垃圾回收(gc)。 - Alma Do
FYI:echo(a,b); 是语法错误,而 echo a,b; 不是。 - bwoebi
是的,复制粘贴自 echo('foo'.'bar') - 已修复,谢谢。 - Alma Do
1
你可以使用 PROFILER 或类似 NEW RELIC 的工具来监控内存等方面(按类、文件、函数、线程等)。这些工具在较低的软件层面上跟踪,因此您甚至不需要触及代码。 - Sliq
5个回答

11
@bwoebi提出使用ticks的好主意后,我进行了一些研究。现在我有了这个类的答案:
class Benchmark
{
   private static $max, $memory;

   public static function memoryTick()
   {
      self::$memory = memory_get_usage() - self::$memory;
      self::$max    = self::$memory>self::$max?self::$memory:self::$max;
      self::$memory = memory_get_usage();
   }

   public static function benchmarkMemory(callable $function, $args=null)
   {
      declare(ticks=1);
      self::$memory = memory_get_usage();
      self::$max    = 0;

      register_tick_function('call_user_func_array', ['Benchmark', 'memoryTick'], []);
      $result = is_array($args)?
                call_user_func_array($function, $args):
                call_user_func($function);
      unregister_tick_function('call_user_func_array');
      return [
         'memory' => self::$max
      ];
   }
}

//var_dump(Benchmark::benchmarkMemory('str_repeat', ['test',1E4]));
//var_dump(Benchmark::benchmarkMemory('str_repeat', ['test',1E3]));

- 所以它完全符合我的要求:
- 它是一个黑盒子 - 它测量了传递函数的最大使用内存 - 它与上下文无关
现在,一些背景知识。在PHP中,可以从函数内部声明ticks,并且我们可以使用回调函数register_tick_function()。所以我的想法是-使用匿名函数,它将使用我的基准函数的局部上下文。我已经成功创建了这个函数。然而,我不想影响全局上下文,所以我想使用unregister_tick_function()取消注册ticks处理程序。这就是问题所在:这个函数期望传递字符串。所以你无法取消注册闭包的ticks处理程序(因为它会尝试将其转换为字符串,这将导致致命错误,因为在PHP中的Closure class中没有__toString()方法)。为什么会这样呢?这只是一个错误。我希望尽快修复。
有其他选择吗?我最容易想到的选择是使用全局变量global。但它们很奇怪,而且这也是我想要避免的“副作用”。我不想影响上下文。但是,实际上,我们可以将所有需要的内容封装在某个类中,然后通过call_user_func_array()调用tick函数。而call_user_func_array只是一个字符串,所以我们可以克服这个有缺陷的PHP行为,并成功完成整个过程。 更新:我已经根据此实现了测量工具。我在那里添加了时间测量和自定义回调定义的测量。请随意使用。 更新:在这个答案中提到的错误现在已经修复,所以不再需要使用call_user_func()作为tick函数的技巧。现在可以直接创建和使用闭包。
更新:由于功能请求,我已经为这个测量工具添加了composer package

为什么不使用 register_tick_function([$this, 'memoryTick']);unregister_tick_function([$this, 'memoryTick']); 呢? - Chloe
1
由于存在错误,这个问题之前是无法解决的(现在已经修复了)。 - Alma Do

8
declare(ticks=1); // should be placed before any further file loading happens

这已经涵盖了我要说的所有内容。

使用一个勾选处理程序,并在每次执行时将内存使用情况打印到文件中,该文件行如下:

function tick_handler() {
    $mem = memory_get_usage();
    $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[0];
    fwrite($file, $bt["file"].":".$bt["line"]."\t".$mem."\n");
}
register_tick_function('tick_handler'); // or in class: ([$this, 'tick_handler']);

然后查看文件,逐行查看内存如何随时间变化。您也可以稍后通过一个独立的程序解析该文件以分析峰值等内容。(要查看如何调用内部函数来查看可能的峰值,您需要将结果存储到一个变量中,否则在时钟处理程序测量内存之前便会被释放)

为了让这段代码正常工作,我需要从以$bt开头的行中删除[1],并将fwrite调用更改为$bt$bt[0]['line'](还更改了"')。 - MikeiLL
1
@MikeiLL 是的,那么你正在使用一些较旧版本的PHP。 - bwoebi
PHP 5.5.3 (cli) (built: Sep 18 2013 14:31:13)。服务器更新的频率是否有标准?谢谢。 - MikeiLL
这是所讨论内容的更新文档链接:http://php.net/manual/en/control-structures.declare.php @PauloFreitas 您提供的链接已失效。 - Chloe

2

不,我不能使用memory_get_usage(),因为如果我连续使用比较,就会失败。 示例:var_dump(benchmarkMemory('str_repeat', ['test',1E4])); var_dump(benchmarkMemory('str_repeat', ['test',1E2])); - 它将从第一次调用中取得上限,并跳过第二个调用中的任何内容,因为第一次调用已经达到峰值限制。 - Alma Do
即使您在其中调用了 gc_collect_cycles - ZoolWay
它将如何帮助?逻辑上,我看不出任何区别,但为了测试而测试 - 请参见fiddle0是第二次调用的预期结果) - Alma Do
XDebug加上补丁呢? - Laurynas Mališauskas

0

0

刚刚偶然发现

http://3v4l.org/

虽然他们没有提供有关如何实施基准测试和采取措施的详细信息 - 我认为很少有人在他们桌子下面的虚拟机上并行运行超过100个PHP版本 ;)


我相信他们正在使用一些外部功能(如分析器),我也能够使用,但就问题而言,我对如何从PHP内部正确地测量内存很感兴趣。 - Alma Do

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