
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):
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'];
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 = 04, 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;
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);
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_walkSEND_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:
- Memeriksa apakah pointer iterator berada di luar array
- Ambil kunci dan nilai saat ini
- 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
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++) {
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.
UPDMenambahkan pengukuran untuk
array_walk dengan lambda statis. Perbedaannya tidak terlihat.