Hiệu suất của foreach, Array_map với lambda và Array_map với hàm tĩnh


144

Sự khác biệt hiệu suất (nếu có) giữa ba cách tiếp cận này, cả hai được sử dụng để chuyển đổi một mảng sang mảng khác?

  1. Sử dụng foreach
  2. Sử dụng array_map với chức năng lambda / đóng
  3. Sử dụng array_map với phương thức / hàm 'tĩnh'
  4. Có cách tiếp cận nào khác không?

Để làm cho tôi rõ ràng, chúng ta hãy xem các ví dụ, tất cả đều làm như vậy - nhân mảng số với 10:

$numbers = range(0, 1000);

Cho mỗi

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

Bản đồ với lambda

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

Bản đồ với chức năng 'tĩnh', được truyền dưới dạng tham chiếu chuỗi

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

Có cách tiếp cận nào khác không? Tôi sẽ rất vui khi nghe thực sự tất cả sự khác biệt giữa các trường hợp từ phía trên, và bất kỳ đầu vào nào tại sao nên sử dụng một thay vì những người khác.


10
Tại sao bạn không chỉ điểm chuẩn và xem điều gì xảy ra?
Jon

17
Vâng, tôi có thể làm cho một điểm chuẩn. Nhưng tôi vẫn không biết làm thế nào nó hoạt động trong nội bộ. Ngay cả khi tôi phát hiện ra một thứ nhanh hơn, tôi vẫn không biết tại sao. Có phải vì phiên bản PHP? Có phụ thuộc vào dữ liệu? Có sự khác biệt giữa mảng kết hợp và mảng thông thường? Tất nhiên tôi có thể thực hiện toàn bộ bộ điểm chuẩn nhưng nhận được một số lý thuyết tiết kiệm ở đây rất nhiều thời gian. Tôi hy vọng bạn hiểu ...
Pavel S.

2
Nhận xét muộn, nhưng không phải trong khi (danh sách ($ k, $ v) = mỗi (mảng $)) nhanh hơn tất cả các mục trên? Tôi đã không điểm chuẩn này trong php5.6, nhưng nó đã có trong các phiên bản trước.
Owen Beresford

Câu trả lời:


121

FWIW, tôi vừa làm điểm chuẩn vì poster không làm điều đó. Chạy trên PHP 5.3.10 + XDebug.

CẬP NHẬT 2015-01-22 so sánh với câu trả lời của mcfedr bên dưới để có kết quả bổ sung mà không cần XDebug và phiên bản PHP mới hơn.


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

Tôi nhận được kết quả khá nhất quán với các số 1M qua hàng tá lần thử:

  • Nghiên cứu: 0,7 giây
  • Bản đồ đóng cửa: 3,4 giây
  • Bản đồ về tên chức năng: 1,2 giây.

Giả sử tốc độ mờ nhạt của bản đồ khi đóng cửa là do việc đóng có thể được đánh giá mỗi lần, tôi cũng đã thử nghiệm như sau:


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

  return array_map($closure, $numbers);
}

Nhưng kết quả là giống hệt nhau, xác nhận rằng việc đóng cửa chỉ được đánh giá một lần.

CẬP NHẬT 2014/02/02: opcodes dump

Dưới đây là các bãi chứa opcode cho ba cuộc gọi lại. Đầu tiên 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

Sau đó 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

và việc đóng nó gọi:


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

sau đó là useMapNamed()chức năng:


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

và hàm được đặt tên mà nó gọi , _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

Cảm ơn các điểm chuẩn. Tuy nhiên, tôi muốn biết tại sao có sự khác biệt như vậy. Có phải vì một chức năng gọi qua đầu?
Pavel S.

4
Tôi đã thêm các bãi chứa opcode trong vấn đề này. Điều đầu tiên chúng ta có thể thấy là hàm và hàm đóng có tên chính xác có cùng một kết xuất và chúng được gọi thông qua mảng_map theo cùng một cách, chỉ với một ngoại lệ: lệnh gọi bao gồm thêm một mã opcode DECLARE_LAMBDA_FUNCTION, giải thích tại sao sử dụng nó chậm hơn một chút so với sử dụng chức năng được đặt tên. Bây giờ, so sánh các vòng lặp mảng với các cuộc gọi mảng_map, mọi thứ trong vòng lặp mảng được diễn giải nội tuyến, không có bất kỳ lệnh gọi nào đến hàm, nghĩa là không có ngữ cảnh để đẩy / bật, chỉ là một JMP ở cuối vòng lặp, có khả năng giải thích sự khác biệt lớn .
FGM

4
Tôi vừa thử điều này bằng cách sử dụng hàm tích hợp (strtolower), và trong trường hợp đó, useMapNamedthực sự nhanh hơn useArray. Nghĩ rằng đó là điều đáng nói.
DisgruntledGoat

1
Trong lap, bạn không muốn range()cuộc gọi trên cuộc gọi microtime đầu tiên? (Mặc dù có lẽ không đáng kể so với thời gian cho vòng lặp.)
contrebis 27/1/2015

1
@billynoah PHP7.x thực sự nhanh hơn rất nhiều. Sẽ rất thú vị khi xem các opcodes được tạo bởi phiên bản này, đặc biệt là so sánh với / không có opcache vì nó có rất nhiều tối ưu hóa bên cạnh bộ nhớ đệm mã.
FGM

231

Thật thú vị khi chạy điểm chuẩn này với xdebug bị vô hiệu hóa, vì xdebug thêm khá nhiều chi phí, đặc biệt cho các cuộc gọi chức năng.

Đây là tập lệnh của FGM chạy bằng 5.6 Với xdebug

ForEach   : 0.79232501983643
MapClosure: 4.1082420349121
MapNamed  : 1.7884571552277

Không có xdebug

ForEach   : 0.69830799102783
MapClosure: 0.78584599494934
MapNamed  : 0.85125398635864

Ở đây chỉ có một sự khác biệt rất nhỏ giữa phiên bản foreach và đóng cửa.

Thật thú vị khi thêm một phiên bản có đóng use

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

Để so sánh tôi thêm:

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

Ở đây chúng ta có thể thấy nó tạo ra ảnh hưởng đến phiên bản đóng cửa, trong khi mảng không thay đổi đáng kể.

19/11/2015 Bây giờ tôi cũng đã thêm kết quả bằng PHP 7 và HHVM để so sánh. Các kết luận là tương tự, mặc dù mọi thứ nhanh hơn nhiều.

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
Tôi tuyên bố bạn là người chiến thắng bằng cách phá vỡ cà vạt và đưa cho bạn upvote thứ 51. RẤT quan trọng để đảm bảo kiểm tra không làm thay đổi kết quả! Câu hỏi, mặc dù, thời gian kết quả của bạn cho "Mảng" là phương pháp vòng lặp foreach, phải không?
Butussy Butkus

2
Tuyệt vời hô hấp. Rất vui khi thấy 7 nhanh như thế nào. Phải bắt đầu sử dụng nó vào thời gian cá nhân của tôi, vẫn ở mức 5,6 tại nơi làm việc.
Dan

1
Vậy tại sao chúng ta phải sử dụng Array_map thay vì foreach? Tại sao nó được thêm vào PHP nếu nó kém về hiệu năng? Có điều kiện cụ thể nào cần Array_map thay vì foreach không? Có logic cụ thể nào mà foreach không thể xử lý và Array_map có thể xử lý không?
HendraWD

3
array_map(và các chức năng liên quan của nó array_reduce, array_filter) cho phép bạn viết mã đẹp. Nếu array_mapchậm hơn nhiều thì đó sẽ là một lý do để sử dụng foreach, nhưng nó rất giống nhau, vì vậy tôi sẽ sử dụng array_mapở mọi nơi có ý nghĩa.
mcfedr

3
Rất vui khi thấy PHP7 được cải thiện rất nhiều. Đã định chuyển sang một ngôn ngữ phụ trợ khác cho các dự án của tôi nhưng tôi sẽ gắn bó với PHP.
realnsleo

8

Thật thú vị. Nhưng tôi đã có một kết quả ngược lại với các mã sau được đơn giản hóa từ các dự án hiện tại của tôi:

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

Đây là dữ liệu và mã kiểm tra của tôi:

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

Kết quả là:

0,0098: mảng_map
0,0114: thuyết minh
0,0114: mảng_map_use_local
0,0115: foreach_use_local

Các thử nghiệm của tôi là trong môi trường sản xuất LAMP mà không có xdebug. Tôi đang lang thang xdebug sẽ làm chậm hiệu suất của mảng_map.


Không chắc bạn có gặp khó khăn khi đọc câu trả lời @mcfedr hay không, nhưng anh ấy giải thích rõ ràng rằng XDebug thực sự chậm lại array_map;)
igorsantos07

Tôi đã kiểm tra hiệu suất array_mapforeachsử dụng Xhprof. Và thú vị của nó array_maptiêu tốn nhiều bộ nhớ hơn `foreach`.
Gopal Joshi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.