Использование памяти Benchmark в PHP

SO,

Особенности

Предположим, что у нас есть некоторая проблема и по крайней мере два решения для нее. И то, что мы хотим достичь - это сравнить эффективность для них. Как это сделать? Очевидно, лучший ответ: делать тесты. И я сомневаюсь, что есть лучший способ, когда речь заходит о специфических для языка вопросах (например, "что быстрее для 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 равен Z% быстрее/медленнее, чем код Y.

Проблема

Хорошо, поэтому мы можем легко измерить время. Но как насчет памяти? Наше предыдущее предположение "если мы хотим протестировать некоторый код, равный проверке некоторой функции", похоже, здесь неверно. Зачем? Потому что - это верно из формальной точки, но если мы будем скрывать код внутри функции, мы никогда не сможем измерить память после этого. Пример:

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 - то есть сколько памяти он будет использовать. Под "насколько" я имею в виду "сколько будет максимального использования памяти во время выполнения функции". И очевидно, что мы не можем действовать, когда измеряем время исполнения - потому что мы ничего не знаем о функции за ее пределами - это черный ящик. Если факт, мы даже не можем быть уверены, что функция будет успешно выполнена (представьте, что произойдет, если каким-то образом $x и $y внутри baz будет присвоено, например, 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 для измерения времени выше)?

Ответы

Ответ 1

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']);

Затем посмотрите файл, чтобы посмотреть, как меняется память во времени, строка за строкой.

Вы также можете проанализировать этот файл позже отдельной программой для анализа пиков и т.д.

(И чтобы увидеть, как возможные пики вызваны внутренними функциями, вам нужно сохранить результаты в переменной, иначе она будет уже освобождена до того, как обработчик тика будет измерять память)

Ответ 2

После @bwoebi предлагаемая отличная идея с использованием тиков, я провел некоторое исследование. Теперь у меня есть мой ответ с этим классом:

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 объявление тиков возможно из внутренней функции, и мы можем использовать обратный вызов для register_tick_function(). Так что, хотя мне было - использовать анонимную функцию, которая будет использовать локальный контекст моей контрольной функции. И я успешно создал это. Тем не менее, я не хочу влиять на глобальный контекст и поэтому хочу отменить регистратор тиков с помощью unregister_tick_function(). И вот в чем проблемы: эта функция ожидает передачи строки. Таким образом, вы не можете отменить регистрацию обработчика тика, который является закрытием (поскольку он попытается свести его на нет, что приведет к фатальной ошибке, потому что нет метода __toString() в Closure class в PHP). Почему это так? Это не что иное, но bug. Я надеюсь, что исправление будет сделано в ближайшее время.

Какие другие варианты? Самый простой вариант, который я имел в виду, - это глобальные переменные. Но они странные, и это побочный эффект, которого я хочу избежать. Я не хочу влиять на контекст. Но, действительно, мы можем обернуть все, что нам нужно в каком-то классе, а затем вызвать функцию tick через call_user_func_array(). И call_user_func_array - это просто строка, поэтому мы можем преодолеть это ошибочное поведение PHP и сделать весь материал успешным.

Обновление: я внедрил инструмент измерения. Я добавил измерение времени и пользовательское определение, основанное на обратном вызове. Не стесняйтесь использовать его.

Обновление: ошибка, упомянутая в этом ответе, теперь исправлена, поэтому нет необходимости в трюке с call_user_func(), зарегистрированным как функция tick. Теперь закрытие может быть создано и использовано напрямую.

Обновление: из-за запроса функции я добавил пакет композитора для этого инструмента измерения.

Ответ 3

Вы можете использовать XDebug и патч для XDebug, который предоставляет информацию об использовании памяти

Если это невозможно, вы всегда можете использовать memory_get_peak_usage(), который, по моему мнению, будет лучше, чем memory_get_usage()

Ответ 5

Просто наткнулся на

http://3v4l.org/

Хотя они не содержат подробностей о том, как контрольные показатели, соответственно, принимаются меры, - не думайте, что у многих людей более 100 версий PHP работают параллельно на VM под их столом;)