Kinerja foreach, array_map dengan lambda dan array_map dengan fungsi statis

144

Apa perbedaan kinerja (jika ada) antara ketiga pendekatan ini, keduanya digunakan untuk mengubah array ke array lain?

  1. Menggunakan foreach
  2. Menggunakan array_mapdengan fungsi lambda / penutupan
  3. Menggunakan array_mapdengan fungsi / metode 'statis'
  4. Apakah ada pendekatan lain?

Untuk memperjelas diri saya, mari kita lihat contoh-contohnya, semuanya melakukan hal yang sama - mengalikan array angka dengan 10:

$numbers = range(0, 1000);

Untuk setiap

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

Peta dengan lambda

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

Peta dengan fungsi 'statis', diteruskan sebagai referensi string

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

Apakah ada pendekatan lain? Saya akan senang mendengar sebenarnya semua perbedaan antara kasus-kasus dari atas, dan setiap masukan mengapa seseorang harus digunakan daripada yang lain.

Pavel S.
sumber
10
Mengapa Anda tidak melakukan benchmark saja dan melihat apa yang terjadi?
Jon
17
Baiklah, saya bisa membuat patokan. Tapi saya masih belum tahu cara kerjanya secara internal. Bahkan jika saya menemukan satu lebih cepat, saya masih tidak tahu kenapa. Apakah karena versi PHP? Apakah ini tergantung pada data? Apakah ada perbedaan antara array asosiatif dan array biasa? Tentu saja saya dapat membuat seluruh rangkaian tolok ukur tetapi mendapatkan beberapa teori menghemat banyak waktu di sini. Saya harap Anda mengerti ...
Pavel S.
2
Komentar terlambat, tetapi bukankah sementara (daftar ($ k, $ v) = masing-masing ($ array)) lebih cepat dari semua yang di atas? Saya belum membuat tolok ukur ini di php5.6, tetapi sudah ada di versi sebelumnya.
Owen Beresford

Jawaban:

121

FWIW, saya hanya melakukan benchmark karena poster tidak melakukannya. Berjalan di PHP 5.3.10 + XDebug.

UPDATE 2015-01-22 dibandingkan dengan jawaban mcfedr di bawah ini untuk hasil tambahan tanpa XDebug dan versi PHP yang lebih baru.


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";
}

Saya mendapatkan hasil yang cukup konsisten dengan angka 1M di selusin upaya:

  • Foreach: 0,7 dtk
  • Peta pada penutupan: 3,4 detik
  • Peta pada nama fungsi: 1,2 detik.

Andaikata kecepatan peta yang kurang bersemangat pada penutupan disebabkan oleh penutupan yang mungkin dievaluasi setiap kali, saya juga menguji seperti ini:


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

  return array_map($closure, $numbers);
}

Tetapi hasilnya identik, mengkonfirmasi bahwa penutupan hanya dievaluasi sekali.

2014-02-02 UPDATE: dump opcodes

Berikut adalah kesedihan opcode untuk tiga panggilan balik. Pertama 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

Kemudian 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

dan penutupan yang disebutnya:


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

maka useMapNamed()fungsinya:


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

dan fungsi menamakannya panggilan, _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
FGM
sumber
Terima kasih atas tolok ukurnya. Namun, saya ingin tahu mengapa ada perbedaan seperti itu. Apakah karena overhead panggilan fungsi?
Pavel S.
4
Saya menambahkan kesedihan opcode dalam masalah ini. Hal pertama yang dapat kita lihat adalah bahwa fungsi dan penutupan bernama memiliki dump yang persis sama, dan mereka dipanggil melalui array_map dengan cara yang sama, dengan hanya satu pengecualian: panggilan penutupan menyertakan satu lagi opcode DECLARE_LAMBDA_FUNCTION, yang menjelaskan mengapa menggunakannya sedikit lebih lambat daripada menggunakan fungsi yang disebutkan. Sekarang, membandingkan panggilan array vs array_map, semua yang ada di array array diinterpretasikan sebaris, tanpa panggilan ke suatu fungsi, yang berarti tidak ada konteks untuk push / pop, hanya JMP di akhir loop, yang kemungkinan menjelaskan perbedaan besar .
FGM
4
Saya baru saja mencoba ini menggunakan fungsi bawaan (strtolower), dan dalam hal ini, useMapNamedsebenarnya lebih cepat daripada useArray. Pikir itu layak disebut.
DisgruntledGoat
1
Dalam lap, Anda tidak ingin range()panggilan di atas panggilan microtime pertama? (Meskipun mungkin tidak signifikan dibandingkan dengan waktu untuk loop.)
contrebis
1
@billynoah PHP7.x memang jauh lebih cepat. Akan menarik untuk melihat opcodes yang dihasilkan oleh versi ini, terutama membandingkan dengan / tanpa opcache karena ia melakukan banyak optimasi selain kode caching.
FGM
232

Sangat menarik untuk menjalankan benchmark ini dengan xdebug dinonaktifkan, karena xdebug menambahkan cukup banyak overhead, terutama untuk panggilan fungsi.

Ini adalah skrip FGM yang dijalankan menggunakan 5.6 Dengan xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Tanpa xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Di sini hanya ada perbedaan yang sangat kecil antara versi foreach dan closure.

Juga menarik untuk menambahkan versi dengan penutupan dengan a use

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

Sebagai perbandingan, saya tambahkan:

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

Di sini kita bisa melihat itu membuat dampak pada versi penutupan, sedangkan array belum terasa berubah.

19/11/2015 Saya juga sekarang menambahkan hasil menggunakan PHP 7 dan HHVM untuk perbandingan. Kesimpulannya serupa, meskipun semuanya jauh lebih cepat.

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
mcfedr
sumber
2
Saya menyatakan Anda sebagai pemenang dengan memutuskan hubungan dan memberi Anda upvote ke-51. SANGAT penting untuk memastikan tes tidak mengubah hasil! Namun, pertanyaannya, waktu hasil Anda untuk "Array" adalah metode loop foreach, kan?
Buttle Butkus
2
Respone sangat baik. Senang melihat seberapa cepat 7 itu. Harus mulai menggunakannya di waktu pribadi saya, masih di 5,6 di tempat kerja.
Dan
1
Jadi mengapa kita harus menggunakan array_map, bukan foreach? Mengapa ditambahkan ke PHP jika kinerjanya buruk? Apakah ada kondisi khusus yang membutuhkan array_map bukan foreach? Apakah ada logika khusus yang tidak dapat ditangani oleh foreach dan array_map dapat atasi?
HendraWD
3
array_map(dan fungsinya yang terkait array_reduce, array_filter) memungkinkan Anda menulis kode yang indah. Jika array_mapjauh lebih lambat itu akan menjadi alasan untuk digunakan foreach, tetapi sangat mirip, jadi saya akan menggunakan di array_mapmana saja itu masuk akal.
mcfedr
3
Senang melihat PHP7 jauh lebih baik. Akan beralih ke bahasa backend yang berbeda untuk proyek saya tapi saya akan tetap menggunakan PHP.
realnsleo
8

Ini menarik. Tetapi saya mendapatkan hasil yang berlawanan dengan kode-kode berikut yang disederhanakan dari proyek saya saat ini:

// 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;
}

Ini data dan kode pengujian saya:

$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);
}

Hasilnya adalah:

0,0098: array_map
0,0114: foreach
0,0114: array_map_use_local
0,0115: foreach_use_local

Tes saya berada di lingkungan produksi LAMP tanpa xdebug. Saya mengembara xdebug akan memperlambat kinerja array_map.

Clarence
sumber
Tidak yakin apakah Anda kesulitan membaca jawaban @mcfedr, tetapi ia menjelaskan dengan jelas bahwa XDebug memang melambat array_map;)
igorsantos07
Saya memiliki pengujian kinerja array_mapdan foreachmenggunakan Xhprof. Dan itu menarik array_mapmenghabiskan lebih banyak memori daripada `foreach`.
Gopal Joshi