نحن نستعد PHP. كيف حالك ، foreach ، array_walk وبعض الكلمات المخيفة الأخرى



كان المساء ، لم يكن هناك شيء للقيام به. حان الوقت لترتيب بعض التحليلات حول كيفية اختلاف بعض أساليب فرز المصفوفات داخل PHP داخليًا.

مصادر من الفرع الرئيسي (هذا الآن 7.4 تتخللها 8)
مولد شفرة التشغيل من php 7.3.0.
تم إجراء القياسات على 7.3.6.

إخلاء المسئولية عن المهووسين: إن ذكر بضع دورات من النانوثانية ودورات المعالجات تعتبر خدعة جدلية تسمى "hyperbole".

ربما ، في الواقع ، هناك عشرات أو المئات من النانوثانية والآلاف من التدابير ، لكنها لا تزال صغيرة جدًا بحيث تشير الحاجة إلى الحفظ عليها إلى أن هناك شيئًا ما في الشفرة.

مرحلة التجميع


بالنسبة إلى ، foreach ، do و بينما هي الكلمات الأساسية للغة ، في حين أن وظائف array_ * هي وظائف للمكتبة القياسية. لذلك ، وفقًا للفقرة الأولى ، فإن المحلل اللغوي سيقوم بتنفيذ بضع نانوثانية بشكل أسرع.

محلل


حتى العبارة المميزة ، سيكون المسار هو نفسه للجميع

start -> top_statement_list -> top_statement -> statement 

يتم تعريف الحلقات على مستوى البيان :
 ->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 

الفرق بين for_exprs و expr فقط هو أنه بالنسبة لأول واحد ، يسمح بكتابة عدة expr مفصولة بفواصل.

foreach_variable عبارة عن بنية ، بالإضافة إلى متغير فقط ، تقوم أيضًا بتتبع إزالة الضغط باستخدام القائمة أو [] .

for_statement ، foreach_statement ، بينما يختلف_statement عن العبارة القياسية في أنها تضيف القدرة على تحليل بناء الجملة البديل:

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

تم استدعاء وظيفة الدعوة أعمق بكثير:

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

callable_variable ، هم ... مضحك ، أليس كذلك؟ :)

دعنا ننتقل إلى opcodes.


كمثال ، لنأخذ تكرارًا بسيطًا لصفيف مفهرس مع طباعة كل مفتاح وقيمة. من الواضح أن استخدام هذه المهمة وأثناءها والقيام بها ليس له ما يبرره ، ولكن هدفنا هو ببساطة عرض الجهاز الداخلي.

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 

ما يجري هنا:


FE_RESET_R : يقوم بإنشاء تكرار $ 4 للصفيف ! 0 . أو ، إذا كانت المصفوفة فارغة ، فإنها تنتقل مباشرة إلى التعليمات 7 .
FE_FETCH_R : ينفذ خطوة التكرار ، ويسترد المفتاح الحالي في ~ 5 ، والقيمة في ! 1 . أو ، إذا تم الوصول إلى نهاية المصفوفة ، فإنها تنتقل إلى التعليمة 7 .
التعليمات 3-6 ليست مثيرة للاهتمام بشكل خاص. يوجد هنا الاستنتاج والعودة إلى FE_FETCH_R .
FE_FREE : يدمر التكرار.

ل ، بينما ، ...


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

هذا هو في الواقع حالة خاصة من حين

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

هذا هو في الواقع حالة خاصة إذا + غوتو

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

سوف تكون شفرة التشغيل للحالات الثلاث متطابقة تقريبًا. ما لم يكن في حالة إذا ، JMPNZ سيتغير إلى زوج من JMPZ + JMP بسبب الدخول إلى نص if .
بالنسبة إلى حلقة " do" ، ستختلف رموز الشفرة قليلاً بسبب طبيعتها بعد التحقق.

 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 

كما هو متوقع ، هناك المزيد من الشفرات.
0-2 : حساب طول $ .
3: $ i = 0
4 ، 10 ، 11 : التحقق الأولي من حالة الخروج ، وفي الواقع ، إما الخروج أو الانتقال إلى نص الدورة.
5 (FETCH_DIM_R): $ arr [$ i]
6-7 : الخاتمة
8-9: $ i ++ (لاحظ أن شفرة التشغيل الإضافية في أي حال تُرجع قيمة ، حتى إذا لم تكن هناك حاجة إليها. لذلك ، يلزم توفير تعليمة مجانية إضافية لمسحها).
10-11 : التحقق من حالة الخروج بعد الزيادة.

أو لا يزال بإمكانك التكرار


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

ورقة السرير حتى تحت المفسد
 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 


هذا الخيار جيد لأنه مناسب للتكرار على صفيف مع أي مفاتيح ، وليس فقط مع الأعداد الصحيحة المتزايدة رتابة.

تعتبر وظائف إعادة التعيين والالتقاط والمفتاح خفيفة الوزن إلى حد ما ، لكن لا يزال هناك بعض النفقات العامة للاتصال بها. وكما سنرى لاحقًا ، فإن هذه التكاليف مرتفعة.

على الرغم من أن هذا النهج مشابه جدًا لمبدأ التطلع ، إلا أن هناك اختلافين أساسيين بينهما.

1) في حين أن إعادة الضبط ، تعمل المفتاح التالي والمفتاح ( والحالي أيضًا) مباشرة مع المؤشر الداخلي للصفيف ، يستخدم foreach أداة التكرار الخاصة به ولا يغير حالة المؤشر الداخلي.

أي

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

2) عند استخدام foreach للتكرار حسب القيمة ، كل ما تفعله بالصفيف داخل الحلقة ، سيتم تكرار مجموعة البيانات الأصلية

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

ماذا سيحدث عند التكرار عبر الرابط يمكن قراءته في RFC هذا . كل شيء ليس بسيط جدا هناك.

array_walk مع وظيفة مجهولة


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

نظرًا لاستخدام وظيفة مخصصة ، ستكون هناك مجموعة إضافية من رموز التشغيل.

وظيفة


 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 

الرمز الرئيسي


 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 

نظرًا لأن array_walk ، مثل بقية وظائف المكتبة القياسية ، جوهرية ، لا توجد آلية تكرار في الشفرات المترجمة.

INIT_FCALL : تهيئة الدعوة إلى array_walk
SEND_REF : ضع مرجع صفيف في مكدس الاستدعاءات
DECLARE_LAMBDA_FUNCTION : إعلان وظيفة مجهولة
SEND_VAL : وضعنا وظيفة مجهولة المصدر في مكدس الاستدعاءات
DO_ICALL : تشغيل array_walk للتنفيذ

بعد ذلك ، يحدث السحر مع استدعاء لامدا لدينا لكل عنصر من عناصر المجموعة.

array_walk باستخدام دالة محددة مسبقا


لا يختلف كثيرًا عن الاتصال بمجهول ، باستثناء ربما أقل قليلاً من النفقات العامة لإنشاء لامدا في وقت التشغيل.

 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 

الاستنتاجات شائعة. تم تصميم foreach لتصفيف التكرار ، في حين أن بقية الحلقات هي مجرد غلاف على if + goto .

تعمل وظائف المكتبة القياسية على مبدأ الصندوق الأسود.

الغوص أعمق قليلا


أولاً ، ضع في اعتبارك الحالة بـ FETCH_DIM_R والرمز الخاص به ، والذي يُستخدم لاستخراج القيمة حسب المفتاح. يتم استخراج العنصر من خلال البحث في جدول التجزئة ( ZEND_HASH_INDEX_FIND ). في حالتنا ، يأتي الاستخراج من مجموعة معبأة (المفاتيح تسلسل رقمي مستمر) - هذه عملية سهلة وسريعة إلى حد ما. بالنسبة للصفائف التي تم فكها ، ستكون أغلى قليلاً.

الآن foreach ( FE_FETCH_R ). كل شيء مبتذلة هنا:

  1. التحقق من وجود مؤشر التكرار خارج الصفيف
  2. استرداد المفتاح الحالي والقيمة
  3. مؤشر الزيادة

حسنا ، ماذا يحدث لوظائف مثل array_walk ؟ على الرغم من أنهم جميعا يقومون بأشياء مختلفة ، إلا أنهم جميعا لديهم نفس المبدأ.

إذا تم تبسيطه تمامًا ، إذن (الرمز الزائف):

 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) 

في الواقع ، كل شيء أكثر تعقيدًا من الداخل ، لكن جوهره هو نفسه - هناك بحث سريع إلى حد ما عن جدول التجزئة دون مشاركة الجهاز الظاهري لـ PHP (لا يأخذ في الاعتبار استدعاء الوظيفة المعرفة من قبل المستخدم).

حسنا ، بعض القياسات


وبعد كل شيء ، ما هو المقال بدون قياسات (من الذاكرة اتضح على قدم المساواة بحيث أنه أزال قياسه).

كصفيف ، وفقًا للتقاليد ، نأخذ zend_vm_execute.h مقابل 70.108 سطرًا.

المفسد النصي القياس
 <?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); 


تم تشغيل كل قياس 10 مرات ، واختيار الأكثر شيوعا التي تحدث في أول 4 أرقام.

 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 

لتلخيص


عند تحليل النتائج ، لا تنس أن تأخذ في الاعتبار أنه تم الحصول عليها في 10 تمريرات من خلال مجموعة مكونة من 70 ألف عنصر.

وكان المطلق لمكافحة بطل "مضاهاة" foreach مع المقبل / مفتاح . لا تفعل ذلك ما لم يكن ذلك ضروريا للغاية.

array_walk مع امدا يتنفس في ظهره ، ولكن هناك فارق بسيط. JIT القادمة يمكن أن تغير الوضع بشكل كبير. أو ربما لا تتغير. سيكون من المثير للاهتمام أن نرى.
array_walk باستخدام وظيفة جاهزة هو اعتدال قوي.

نظرًا لأن foreach يعمل بشكل مختلف قليلاً عند التكرار عبر ارتباط (يستخدم شفرة التشغيل FE_FETCH_RW بدلاً من FE_FETCH_R ) ، فقد أجرى قياسًا منفصلًا لذلك. التفت حقا أسرع قليلا.

كما اتضح ، إنشاء lambda على الطاير ليست أرخص عملية. يبدو أنه تم إنشاؤه 10 مرات فقط. سيكون من الضروري للدراسة.

أظهرت جميع الطرق الأخرى نفس النتائج تقريبًا ، مع وجود فجوة بسيطة جدًا.

شكرا لاهتمامكم!

إذا كان لديك أي اقتراحات ، فماذا يمكنك "اختيار" - الكتابة في التعليقات. ما زلت أفكر في lambdas - مثل هذا الانخفاض في الأداء غريب جدًا.

UPD
واضاف القياس ل array_walk مع امدا ثابت. الفرق غير مرئي.

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


All Articles