Kami sedang menyiapkan PHP. Bagaimana sementara, foreach, array_walk dan beberapa kata menakutkan lainnya



Itu malam, tidak ada yang bisa dilakukan. Sudah waktunya untuk mengatur sedikit analisis tentang bagaimana secara internal beberapa metode pengurutan array di PHP berbeda.

Sumber dari cabang utama (sekarang 7.4 diselingi dengan 8)
Generator opcode dari php 7.3.0.
Pengukuran dilakukan pada 7.3.6.

Penafian untuk kutu buku: menyebutkan beberapa nanodetik dan siklus prosesor adalah trik polemik yang disebut "hiperbola".

Mungkin, pada kenyataannya, ada puluhan atau ratusan nanodetik dan ribuan ukuran, tetapi masih sangat kecil sehingga kebutuhan untuk menyimpannya menunjukkan bahwa ada sesuatu dalam kode Anda yang salah.

Tahap kompilasi


untuk , foreach , do dan while adalah kata kunci dari bahasa tersebut, sedangkan fungsi array_ * adalah fungsi dari perpustakaan standar. Oleh karena itu, ceteris paribus, menurut yang pertama, parser akan mengeksekusi beberapa nanodetik lebih cepat.

Parser


Hingga token pernyataan , jalurnya akan sama untuk semua

start -> top_statement_list -> top_statement -> statement 

Loop didefinisikan pada level pernyataan :
 ->T_FOR '(' for_exprs ';' for_exprs ';' for_exprs ')' for_statement ->T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement ->T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')' foreach_statement ->T_DO statement T_WHILE '(' expr ')' ';' ->T_WHILE '(' expr ')' while_statement 

Perbedaan antara for_exprs dan just expr hanya untuk yang pertama, menulis beberapa expr diperbolehkan, dipisahkan oleh koma.

foreach_variable adalah konstruk yang, di samping hanya variabel , juga melacak dekompresi menggunakan daftar atau [] .

for_statement , foreach_statement , while_statement berbeda dari pernyataan standar karena mereka menambahkan kemampuan untuk mem-parsing sintaksis alternatif:

 foreach($array as $element): #do something endforeach; 

Panggilan fungsi terkubur lebih dalam:

 -> expr -> variable -> callable_variable -> function_call -> name argument_list 

callable_variable , hmm ... Lucu, bukan? :)

Mari kita beralih ke opcodes.


Sebagai contoh, mari kita lakukan iterasi sederhana dari array yang diindeks dengan mencetak setiap kunci dan nilai. Jelas bahwa menggunakan untuk , sementara dan melakukan untuk tugas seperti itu tidak dibenarkan, tetapi tujuan kami adalah hanya untuk menunjukkan perangkat internal.

foreach


 $arr = ['a', 'b', 'c']; //        foreach($arr as $key => $value) echo $key.$value; 

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 > FE_RESET_R $4 !0, ->7 2 > > FE_FETCH_R ~5 $4, !1, ->7 3 > ASSIGN !2, ~5 4 CONCAT ~7 !2, !1 5 ECHO ~7 6 > JMP ->2 7 > FE_FREE $4 5 8 > RETURN 1 

Apa yang terjadi di sini:


FE_RESET_R : membuat iterator $ 4 untuk array ! 0 . Atau, jika array kosong, langsung menuju ke instruksi 7 .
FE_FETCH_R : melakukan langkah iterasi, mengambil kunci saat ini di ~ 5 , dan nilainya di ! 1 . Atau, jika ujung array tercapai, ia melanjutkan ke instruksi 7 .
Instruksi 3-6 tidak terlalu menarik. Di sini ada kesimpulan dan kembali ke FE_FETCH_R .
FE_FREE : hancurkan iterator.

untuk, sementara, ...


 $length = count($arr); for($i = 0; $i < $length; $i++) echo $i.$arr[$i]; 

Ini sebenarnya kasus khusus sementara

 $i = 0; while($i < $length) { echo $i.$arr[$i]; $i++; } 

Ini sebenarnya adalah kasus khusus jika + goto

 $i = 0; goto check; body: echo $i.$arr[$i]; $i++; check: if($i < $length) goto body; 

Opcode untuk ketiga kasus akan hampir identik. Kecuali jika dalam kasus if , JMPNZ akan berubah menjadi sepasang JMPZ + JMP karena entri ke dalam tubuh if .
Untuk do loop, opcodes akan sedikit berbeda karena sifatnya pasca verifikasi.

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 COUNT ~4 !0 2 ASSIGN !1, ~4 6 3 ASSIGN !2, 0 4 > JMP ->10 5 > FETCH_DIM_R ~7 !0, !2 6 CONCAT ~8 !2, ~7 7 ECHO ~8 8 POST_INC ~9 !2 9 FREE ~9 10 > IS_SMALLER ~10 !2, !1 11 > JMPNZ ~10, ->5 12 > > RETURN 1 

Seperti yang diharapkan, ada lebih banyak opcodes.
0-2 : menghitung $ panjang .
3: $ i = 0
4, 10, 11 : pemeriksaan awal kondisi keluar dan, pada kenyataannya, baik keluar atau transisi ke tubuh siklus.
5 (FETCH_DIM_R): $ arr [$ i]
6-7 : kesimpulan
8-9: $ i ++ (perhatikan bahwa opcode kenaikan dalam kasus apa pun mengembalikan nilai, bahkan jika itu tidak diperlukan. Oleh karena itu, diperlukan instruksi tambahan GRATIS untuk menghapusnya).
10-11 : memeriksa kondisi keluar setelah kenaikan.

Atau Anda masih bisa beralih


 $value = reset($arr); $key = key($arr); while($key !== null) { echo $key.$value; $value = next($arr); $key = key($arr); } 

Sprei jadi di bawah spoiler
 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 INIT_FCALL 'reset' 2 SEND_REF !0 3 DO_ICALL $4 4 ASSIGN !1, $4 6 5 INIT_FCALL 'key' 6 SEND_VAR !0 7 DO_ICALL $6 8 ASSIGN !2, $6 7 9 > JMP ->20 8 10 > CONCAT ~8 !2, !1 11 ECHO ~8 9 12 INIT_FCALL 'next' 13 SEND_REF !0 14 DO_ICALL $9 15 ASSIGN !1, $9 10 16 INIT_FCALL 'key' 17 SEND_VAR !0 18 DO_ICALL $11 19 ASSIGN !2, $11 7 20 > IS_NOT_IDENTICAL ~13 !2, null 21 > JMPNZ ~13, ->10 11 22 > > RETURN 1 


Opsi ini baik karena cocok untuk iterasi melalui array dengan tombol apa saja, dan tidak hanya dengan bilangan bulat yang meningkat secara monoton.

Fungsi reset , next, dan key cukup ringan, tetapi masih ada beberapa overhead untuk memanggil mereka. Dan, seperti yang akan kita lihat nanti, biaya ini tinggi.

Meskipun pendekatan ini sangat mirip dengan prinsip pendahuluan , ada dua perbedaan mendasar di antara mereka.

1) Jika reset , selanjutnya dan kunci (dan juga saat ini ) bekerja langsung dengan pointer internal array, foreach menggunakan iteratornya sendiri dan tidak mengubah keadaan pointer internal.

Yaitu

 foreach($arr as $v) echo $v.' - '.current($arr).PHP_EOL; //---------------- a - a b - a c - a 

2) Saat menggunakan foreach untuk beralih berdasarkan nilai, apa pun yang Anda lakukan dengan array di dalam loop, set data asli akan diulang

 foreach($arr as $v) { $arr = ['X','Y','Z']; echo $v.PHP_EOL; } var_dump($arr); //---------------- a b c array(3) { [0]=> string(1) "X" [1]=> string(1) "Y" [2]=> string(1) "Z" } 

Apa yang akan terjadi ketika iterasi tautan dapat dibaca di RFC ini . Semuanya tidak terlalu sederhana di sana.

array_walk dengan fungsi anonim


 array_walk($arr, function($value, $key){ echo $key.$value;}); 

Karena fungsi khusus digunakan, akan ada satu set opcodes tambahan.

Fungsi


 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 0 E > RECV !0 1 RECV !1 2 CONCAT ~2 !1, !0 3 ECHO ~2 4 > RETURN null 

Kode utama


 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 INIT_FCALL 'array_walk' 2 SEND_REF !0 3 DECLARE_LAMBDA_FUNCTION '%00%7Bclosure%7D%2Fin%2FHuNEK0x7f9fa62d105a' 4 SEND_VAL ~2 5 DO_ICALL 6 > RETURN 1 

Karena array_walk , seperti fungsi perpustakaan standar lainnya, adalah intrinsik, tidak ada mekanisme iterasi dalam opcode yang dikompilasi.

INIT_FCALL : inisialisasi panggilan ke array_walk
SEND_REF : meletakkan referensi array pada tumpukan panggilan
DECLARE_LAMBDA_FUNCTION : mendeklarasikan fungsi anonim
SEND_VAL : Kami menempatkan fungsi anonim di tumpukan panggilan
DO_ICALL : jalankan array_walk untuk mengeksekusi

Selanjutnya, sihir terjadi dengan panggilan lambda kami untuk setiap elemen array.

array_walk menggunakan fungsi yang telah ditentukan


Tidak jauh berbeda dari menelepon dengan yang anonim, kecuali mungkin sedikit kurang dari biaya pembuatan lambda saat runtime.

 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 9 2 INIT_FCALL 'array_walk' 3 SEND_REF !0 4 SEND_VAL 'my_echo' 5 DO_ICALL 6 > RETURN 1 

Kesimpulannya biasa. foreach disesuaikan untuk iterating array, sedangkan sisanya dari loop hanyalah pembungkus jika + kebagian .

Fungsi pustaka standar bekerja berdasarkan prinsip kotak hitam.

Menyelam sedikit lebih dalam


Pertama, pertimbangkan kasus dengan untuk dan opcode FETCH_DIM_R , yang digunakan untuk mengekstraksi nilai dengan kunci. Item diekstraksi melalui pencarian di tabel hash ( ZEND_HASH_INDEX_FIND ). Dalam kasus kami, ekstraksi berasal dari array yang dikemas (kunci adalah urutan numerik berkelanjutan) - ini adalah operasi yang cukup mudah dan cepat. Untuk array yang tidak dibongkar, akan sedikit lebih mahal.

Sekarang prakiraan ( FE_FETCH_R ). Semuanya basi di sini:

  1. Memeriksa apakah pointer iterator berada di luar array
  2. Ambil kunci dan nilai saat ini
  3. Penunjuk kenaikan

Nah, apa yang terjadi pada fungsi-fungsi seperti array_walk ? Meskipun mereka semua melakukan hal yang berbeda, mereka semua memiliki satu dan prinsip yang sama.

Jika sepenuhnya disederhanakan, maka (pseudo-code):

 reset_pointer() do { value = get_current_value() if (value == NULL) break //  NULL,   zval  NULL key = get_current_key() call_function(myFunction, key, value) increment_pointer() } while(true) 

Sebenarnya, semuanya lebih rumit di dalam, tetapi intinya sama - ada pencarian yang lebih cepat dari tabel hash tanpa partisipasi mesin virtual PHP (tidak memperhitungkan panggilan ke fungsi pengguna).

Nah, beberapa pengukuran


Dan setelah semua, apa artikel tanpa pengukuran (dari memori ternyata sama-sama menghapus pengukurannya).

Sebagai sebuah array, menurut tradisi, kita mengambil zend_vm_execute.h untuk 70,108 baris.

Skrip pengukuran spoiler
 <?php $array = explode("\n", file_get_contents('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h')); function myEcho($key, $value){ echo $key . $value; } ob_start(); $startTime = microtime(true); for ($i = 0; $i < 10; $i++) { // foreach ($array as $key => $value) { // echo $key . $value; // } // foreach ($array as $key => &$value) { // echo $key . $value; // } // $length = count($array); // for ($j = 0; $j < $length; $j++) { // echo $j . $array[$j]; // } // array_walk($array, function($value, $key) { // echo $key . $value; // }); // array_walk($array, static function($value, $key) { // echo $key . $value; // }); // array_walk($array, 'myEcho'); // $value = reset($array); // $key = key($array); // while($key !== null) { // echo $key.$value; // $value = next($array); // $key = key($array); // } } $endTime = microtime(true); ob_clean(); echo "time: " . ($endTime - $startTime); 


Setiap pengukuran dijalankan 10 kali, memilih yang paling sering terjadi dalam 4 digit pertama.

 foreach time: 0.12159085273743 foreach(reference) time: 0.11191201210022 for, while time: 0.13130807876587 array_walk(lambda) time: 0.87953400611877 array_walk(static lambda) time: 0.87544417497211 array_walk(name) time: 0.50753092765808 next,key time: 1.06258893013 

Untuk meringkas


Menganalisis hasil, jangan lupa memperhitungkan bahwa mereka diperoleh pada 10 melewati melalui 70 ribu elemen.

Anti-hero absolut adalah "emulasi" foreach dengan next / key . Jangan lakukan itu kecuali benar-benar diperlukan.

array_walk dengan lambda bernafas di punggungnya, tetapi ada nuansa. JIT yang akan datang dapat secara dramatis mengubah situasi. Atau mungkin tidak berubah. Ini akan menarik untuk dilihat.
array_walk menggunakan fungsi siap pakai adalah lumayan kuat.

Karena foreach bekerja sedikit berbeda ketika melakukan iterasi melalui tautan (menggunakan opcode FE_FETCH_RW alih-alih FE_FETCH_R ), ia membuat pengukuran terpisah untuknya. Dia benar-benar berubah sedikit lebih cepat.

Ternyata, membuat lambda dengan cepat bukanlah operasi termurah. Tampaknya dibuat hanya 10 kali. Perlu belajar.

Semua metode lain menunjukkan hasil yang kira-kira sama, dengan celah yang sangat kecil.

Terima kasih atas perhatian anda!

Jika Anda memiliki saran, apa lagi yang bisa Anda "pilih" - tulis di komentar. Saya masih berpikir tentang lambdas - penurunan kinerja seperti itu sangat aneh.

UPD
Menambahkan pengukuran untuk array_walk dengan lambda statis. Perbedaannya tidak terlihat.

Source: https://habr.com/ru/post/id460285/


All Articles