Продуктивність foreach, array_map з лямбда та array_map зі статичною функцією


144

Яка різниця в продуктивності (якщо є) між цими трьома підходами, обидва використовуються для перетворення масиву в інший масив?

  1. Використання foreach
  2. Використання array_mapз функцією лямбда / закриття
  3. Використання array_mapзі статичною функцією / методом
  4. Чи є якийсь інший підхід?

Щоб зрозуміти себе, давайте подивимось на приклади, які роблять те саме - множення масиву чисел на 10:

$numbers = range(0, 1000);

Для кожного

$result = array();
foreach ($numbers as $number) {
    $result[] = $number * 10;
}
return $result;

Карта з лямбда

return array_map(function($number) {
    return $number * 10;
}, $numbers);

Карта з "статичною" функцією, передається як посилання на рядок

function tenTimes($number) {
    return $number * 10;
}
return array_map('tenTimes', $numbers);

Чи є якийсь інший підхід? Я буду радий почути фактично всі відмінності між справами зверху та будь-якими введеннями, чому один слід використовувати замість інших.


10
Чому ви просто не орієнтуєтесь і не бачите, що відбувається?
Джон

17
Ну, я можу зробити орієнтир. Але я досі не знаю, як це працює всередині. Навіть якщо я дізнаюся, що швидше, я все одно не знаю, чому. Це через версію PHP? Це залежить від даних? Чи є різниця між асоціативними та звичайними масивами? Звичайно, я можу скласти цілий набір орієнтирів, але отримання певної теорії економить тут багато часу. Сподіваюся, ви зрозуміли ...
Павло С.

2
Пізній коментар, але хіба поки (список ($ k, $ v) = кожен ($ масив)) швидше, ніж усе вищезазначене? Я цього не орієнтував у php5.6, але це було в попередніх версіях.
Оуен Бересфорд

Відповіді:


121

FWIW, я щойно робив тест, оскільки плакат цього не робив. Працює на PHP 5.3.10 + XDebug.

ОНОВЛЕННЯ 2015-01-22 порівняйте з відповіддю mcfedr нижче для отримання додаткових результатів без XDebug та новітньої версії PHP.


function lap($func) {
  $t0 = microtime(1);
  $numbers = range(0, 1000000);
  $ret = $func($numbers);
  $t1 = microtime(1);
  return array($t1 - $t0, $ret);
}

function useForeach($numbers)  {
  $result = array();
  foreach ($numbers as $number) {
      $result[] = $number * 10;
  }
  return $result;
}

function useMapClosure($numbers) {
  return array_map(function($number) {
      return $number * 10;
  }, $numbers);
}

function _tenTimes($number) {
    return $number * 10;
}

function useMapNamed($numbers) {
  return array_map('_tenTimes', $numbers);
}

foreach (array('Foreach', 'MapClosure', 'MapNamed') as $callback) {
  list($delay,) = lap("use$callback");
  echo "$callback: $delay\n";
}

Я отримую досить стійкі результати з 1М числами через десяток спроб:

  • Передбачення: 0,7 сек
  • Карта при закритті: 3,4 сек
  • Карта назви функції: 1,2 сек.

Припустимо, що низька швидкість карти при закритті була викликана закриттям, можливо, щоразу оцінюваним, я також перевіряв так:


function useMapClosure($numbers) {
  $closure = function($number) {
    return $number * 10;
  };

  return array_map($closure, $numbers);
}

Але результати однакові, що підтверджує, що закриття оцінюється лише один раз.

ОНОВЛЕННЯ 2014-02-02: скидання опкодів

Ось дамп-коди для трьох зворотних викликів. По-перше useForeach():



compiled vars:  !0 = $numbers, !1 = $result, !2 = $number
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  10     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  11     2      EXT_STMT                                                 
         3      INIT_ARRAY                                       ~0      
         4      ASSIGN                                                   !1, ~0
  12     5      EXT_STMT                                                 
         6    > FE_RESET                                         $2      !0, ->15
         7  > > FE_FETCH                                         $3      $2, ->15
         8  >   OP_DATA                                                  
         9      ASSIGN                                                   !2, $3
  13    10      EXT_STMT                                                 
        11      MUL                                              ~6      !2, 10
        12      ASSIGN_DIM                                               !1
        13      OP_DATA                                                  ~6, $7
  14    14    > JMP                                                      ->7
        15  >   SWITCH_FREE                                              $2
  15    16      EXT_STMT                                                 
        17    > RETURN                                                   !1
  16    18*     EXT_STMT                                                 
        19*   > RETURN                                                   null

Тоді useMapClosure()


compiled vars:  !0 = $numbers
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  18     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  19     2      EXT_STMT                                                 
         3      EXT_FCALL_BEGIN                                          
         4      DECLARE_LAMBDA_FUNCTION                                  '%00%7Bclosure%7D%2Ftmp%2Flap.php0x7f7fc1424173'
  21     5      SEND_VAL                                                 ~0
         6      SEND_VAR                                                 !0
         7      DO_FCALL                                      2  $1      'array_map'
         8      EXT_FCALL_END                                            
         9    > RETURN                                                   $1
  22    10*     EXT_STMT                                                 
        11*   > RETURN                                                   null

і закриття, яке він називає:


compiled vars:  !0 = $number
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  19     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  20     2      EXT_STMT                                                 
         3      MUL                                              ~0      !0, 10
         4    > RETURN                                                   ~0
  21     5*     EXT_STMT                                                 
         6*   > RETURN                                                   null

то useMapNamed()функція:


compiled vars:  !0 = $numbers
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  28     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  29     2      EXT_STMT                                                 
         3      EXT_FCALL_BEGIN                                          
         4      SEND_VAL                                                 '_tenTimes'
         5      SEND_VAR                                                 !0
         6      DO_FCALL                                      2  $0      'array_map'
         7      EXT_FCALL_END                                            
         8    > RETURN                                                   $0
  30     9*     EXT_STMT                                                 
        10*   > RETURN                                                   null

і названа функція, яку вона викликає _tenTimes():


compiled vars:  !0 = $number
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  24     0  >   EXT_NOP                                                  
         1      RECV                                                     1
  25     2      EXT_STMT                                                 
         3      MUL                                              ~0      !0, 10
         4    > RETURN                                                   ~0
  26     5*     EXT_STMT                                                 
         6*   > RETURN                                                   null

Дякуємо за орієнтири. Однак я хотів би знати, чому існує така різниця. Це через виклик функції над головою?
Павло Сергійович

4
Я додав скидки опкодів у випуску. Перше, що ми можемо побачити, - це те, що названа функція та закриття мають абсолютно однаковий дамп, і вони викликаються через array_map майже однаково, лише за одним винятком: виклик закриття включає ще один опкод DECLARE_LAMBDA_FUNCTION, який пояснює, чому його використовувати трохи повільніше, ніж використання названої функції. Тепер, порівнюючи цикл масиву з викликами array_map, все в циклі масиву інтерпретується вбудовано, без виклику функції, що означає відсутність контексту для push / pop, просто JMP в кінці циклу, що, ймовірно, пояснює велику різницю .
FGM

4
Я тільки що спробував це за допомогою вбудованої функції (strtolower), і в цьому випадку useMapNamedнасправді швидше, ніж useArray. Думав, що варто згадати.
НезадоволенняГота

1
В lap, ви не хочете , range()виклик вище першого виклику мікропори? (Хоча, ймовірно, незначно порівняно з часом для циклу.)
contrebis

1
@billynoah PHP7.x справді набагато швидше. Було б цікаво побачити опкоди, згенеровані цією версією, особливо порівнюючи з / без opcache, оскільки він робить багато оптимізацій, крім кешування коду.
ФГМ

231

Цікаво запустити цей бенчмарк із відключеним xdebug, оскільки xdebug додає досить багато накладних витрат, особливо функціональних викликів.

Це сценарій запуску FGM, використовуючи 5.6 З xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Без xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Тут є лише дуже невелика різниця між версією foreach і закриття.

Також цікаво додати версію із закриттям з a use

function useMapClosureI($numbers) {
  $i = 10;
  return array_map(function($number) use ($i) {
      return $number * $i++;
  }, $numbers);
}

Для порівняння додаю:

function useForEachI($numbers)  {
  $result = array();
  $i = 10;
  foreach ($numbers as $number) {
    $result[] = $number * $i++;
  }
  return $result;
}

Тут ми можемо побачити, що це впливає на версію закриття, тоді як масив помітно не змінився.

19/11/2015 Я також додав результати, використовуючи PHP 7 та HHVM для порівняння. Висновки схожі, хоча все відбувається набагато швидше.

PHP 5.6

ForEach    : 0.57499806880951
MapClosure : 0.59327731132507
MapNamed   : 0.69694859981537
MapClosureI: 0.73265469074249
ForEachI   : 0.60068697929382

PHP 7

ForEach    : 0.11297199726105
MapClosure : 0.16404168605804
MapNamed   : 0.11067249774933
MapClosureI: 0.19481580257416
ForEachI   : 0.10989861488342

HHVM

ForEach    : 0.090071058273315
MapClosure : 0.10432276725769
MapNamed   : 0.1091267824173
MapClosureI: 0.11197068691254
ForEachI   : 0.092114186286926

2
Я оголошую вас переможцем, розірвавши краватку і даючи 51-й результат. ДУЖЕ важливо, щоб тест не змінив результати! Питання, однак, час вашого результату для "Array" - це метод циклу foreach, правда?
Buttle Butkus

2
Відмінний відгук. Приємно бачити, як швидко йде 7. Потрібно почати використовувати його в особистий час, ще в 5.6 на роботі.
Ден

1
То чому ми повинні використовувати array_map замість foreach? Чому він додається до PHP, якщо він поганий у роботі? Чи є якась конкретна умова, яка потребує array_map замість foreach? Чи є певна логіка, з якою foreach не може впоратися, і array_map може впоратися?
HendraWD

3
array_map(І пов'язана з ним функція array_reduce, array_filter) дозволяє писати красивий код. Якщо це array_mapбуло набагато повільніше, це було б приводом для використання foreach, але його дуже схоже, тому я буду користуватися array_mapскрізь, коли це має сенс.
mcfedr

3
Приємно бачити, що PHP7 значно покращився. Зібрався перейти на іншу мову бекенда для своїх проектів, але я буду дотримуватися PHP.
realnsleo

8

Це цікаво. Але я отримав протилежний результат із наступними кодами, які спрощені з моїх поточних проектів:

// test a simple array_map in the real world.
function test_array_map($data){
    return array_map(function($row){
        return array(
            'productId' => $row['id'] + 1,
            'productName' => $row['name'],
            'desc' => $row['remark']
        );
    }, $data);
}

// Another with local variable $i
function test_array_map_use_local($data){
    $i = 0;
    return array_map(function($row) use ($i) {
        $i++;
        return array(
            'productId' => $row['id'] + $i,
            'productName' => $row['name'],
            'desc' => $row['remark']
        );
    }, $data);
}

// test a simple foreach in the real world
function test_foreach($data){
    $result = array();
    foreach ($data as $row) {
        $tmp = array();
        $tmp['productId'] = $row['id'] + 1;
        $tmp['productName'] = $row['name'];
        $tmp['desc'] = $row['remark'];
        $result[] = $tmp;
    }
    return $result;
}

// Another with local variable $i
function test_foreach_use_local($data){
    $result = array();
    $i = 0;
    foreach ($data as $row) {
        $i++;
        $tmp = array();
        $tmp['productId'] = $row['id'] + $i;
        $tmp['productName'] = $row['name'];
        $tmp['desc'] = $row['remark'];
        $result[] = $tmp;
    }
    return $result;
}

Ось мої дані тестування та коди:

$data = array_fill(0, 10000, array(
    'id' => 1,
    'name' => 'test',
    'remark' => 'ok'
));

$tests = array(
    'array_map' => array(),
    'foreach' => array(),
    'array_map_use_local' => array(),
    'foreach_use_local' => array(),
);

for ($i = 0; $i < 100; $i++){
    foreach ($tests as $testName => &$records) {
        $start = microtime(true);
        call_user_func("test_$testName", $data);
        $delta = microtime(true) - $start;
        $records[] = $delta;
    }
}

// output result:
foreach ($tests as $name => &$records) {
    printf('%.4f : %s '.PHP_EOL, 
              array_sum($records) / count($records), $name);
}

Результат:

0,0098: array_map
0,0114: передм
0,0114: array_map_use_local
0,0115: foreach_use_local

Мої тести проходили у виробничому середовищі LAMP без xdebug. Я блукаю xdebug уповільнить продуктивність array_map.


Не впевнений, чи виникли проблеми з читанням відповіді @mcfedr, але він чітко пояснює, що XDebug дійсно сповільнює array_map;)
igorsantos07

У мене є тестування продуктивності array_mapта foreachвикористання Xhprof. І його цікавість array_mapвитрачає більше пам’яті, ніж «foreach».
Гопал Джоші
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.