Продуктивність 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.