في
الجزء الأول من القصة ، استنادًا إلى
عرض تقديمي قدمه ديمتري ستوغوف من Zend Technologies على HighLoad ++ ، فهمنا البنية الداخلية لـ PHP. لقد تعلمنا بالتفصيل وبصورة مباشرة التغييرات التي طرأت على هياكل البيانات الأساسية والتي سمحت لـ PHP 7 بالتسريع أكثر من مرتين. يمكن أن نتوقف عند هذا ، ولكن بالفعل في الإصدار 7.1 ، ذهب المطورون إلى أبعد من ذلك ، حيث لا يزال لديهم العديد من الأفكار لتحسين الأداء.
يمكن الآن تفسير الخبرة المتراكمة للعمل على JIT قبل السبعة ، بالنظر إلى النتائج في 7.0 دون JIT ونتائج HHVM مع JIT. في PHP 7.1 ، تقرر عدم العمل مع JIT ، ولكن مرة أخرى للانتقال إلى المترجم الفوري. إذا كان التحسين يتعلق في وقت سابق بالمترجم الفوري ، فسنبحث في هذه المقالة عن تحسين الرمز الفرعي باستخدام الاستنتاج النوعي المطبق على JIT لدينا.

تحت الخفض ، سيوضح ديمتري ستوغوف كيف يعمل كل هذا ، باستخدام مثال بسيط.
Bytecode الأمثل
يوجد أدناه الرمز الفرعي الذي يقوم فيه مترجم PHP القياسي بترجمة الوظيفة. إنه تمريرة واحدة - سريعة وغبية ، ولكنها قادرة على أداء مهمتها على كل طلب HTTP مرة أخرى (إذا لم يكن OPcache متصلًا).

تحسينات OPcache
مع ظهور OPcache ، بدأنا في تحسينه.
منذ فترة طويلة تم دمج بعض أساليب التحسين
في OPcache ، على سبيل المثال ، طرق تحسين الشق - عندما ننظر إلى الكود من خلال ثقب الباب ، ابحث عن الأنماط المألوفة ، واستبدالها بالاستدلال. تستمر هذه الطرق ليتم استخدامها في 7.0. على سبيل المثال ، لدينا عمليتان: الجمع والتعيين.

يمكن دمجها في عملية تعيين مركبة واحدة ، والتي تؤدي عملية الإضافة مباشرة على النتيجة:
ASSIGN_ADD $sum, $i
. مثال آخر هو متغير ما بعد الزيادة الذي يمكن أن يُرجع نظريًا نوعًا من النتائج.

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

في النهاية ، يوجد بيانان
RETURN
: الأول هو انعكاس مباشر لبيان RETURN في النص المصدر ، ويتم إضافة الثاني بواسطة مترجم غبي مع قوس إغلاق. لن يتم الوصول إلى هذا الرمز ويمكن حذفه.
هناك أربعة تعليمات فقط اليسار في الحلقة. يبدو أنه لا يوجد شيء آخر للتحسين ، ولكن ليس بالنسبة لنا.
انظر إلى رمز
$i++
والتعليمات المقابلة -
PRE_INC
المسبقة
PRE_INC
. في كل مرة يتم تنفيذه:
- بحاجة إلى التحقق من أي نوع من المتغيرات جاء ؛
- سواء كانت
is_long
- أداء الزيادة ؛
- تحقق من الفائض
- انتقل إلى التالي ؛
- ربما تحقق الاستثناء.
لكن الشخص الذي ينظر فقط إلى شفرة PHP ، سيرى أن المتغير
$i
يقع في النطاق من 0 إلى 100 ، ولا يمكن أن يكون هناك تجاوزات ، وفحص النوع ليس ضروريًا ، ولا يمكن أن يكون هناك استثناءات أيضًا.
في PHP 7.1 ، حاولنا تعليم المترجم لفهم ذلك .
تعظيم التحكم في تدفق الرسم البياني

للقيام بذلك ، تحتاج إلى استنتاج أنواع ، وإدخال أنواع ، يجب أولاً إنشاء تمثيل رسمي لتدفقات البيانات التي يفهمها الكمبيوتر. لكننا سنبدأ ببناء رسم بياني للتحكم في التدفق ، وهو رسم بياني تابع للرقابة. في البداية ، نقوم بتقسيم الكود إلى كتل أساسية - مجموعة من التعليمات مع إدخال واحد ومخرج واحد. لذلك ، نقوم بقص الشفرة في الأماكن التي يحدث فيها الانتقال ، أي التسميات L0 و L1. كما قمنا بقصه بعد مشغلي الفرع الشرطي وغير المشروط ، ثم وصله بالأقواس التي تظهر التبعيات للتحكم.

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

لكل متغير ، نضيف فهرس ، أو رقم التناسخ. في كل مكان تحدث فيه المهمة ، نضع فهرسًا جديدًا ، ونستخدمه - حتى علامات الاستفهام ، لأنه غير معروف دائمًا في كل مكان. على سبيل المثال ، في التعليمة
IS_SMALLER
$ ، يمكنني الحصول على كل من الكتلة L0 برقم 4 ، ومن المجموعة الأولى برقم 2.
لحل هذه المشكلة ، تقدم SSA
وظيفة Phi pseudo ، والتي ، إذا لزم الأمر ، يتم إدراجها في بداية الكتلة الأساسية -> ، تأخذ جميع أنواع مؤشرات المتغير الوحيد الذي جاء إلى الكتلة الأساسية من أماكن مختلفة ، ويخلق تجسيدًا جديدًا للمتغير. ومن هذه المتغيرات التي تستخدم في وقت لاحق للقضاء على الغموض.

استبدال جميع علامات الاستفهام بهذه الطريقة ، سنبني SSA.
اكتب التحسين
الآن نستنتج أنواعًا - كما لو كنا نحاول تنفيذ هذا الكود مباشرةً على الإدارة.

في الكتلة الأولى ، يتم تعيين قيم ثابتة للمتغيرات - الأصفار ، ونحن نعرف بالتأكيد أن هذه المتغيرات ستكون من النوع الطويل. يصل المقبل هو وظيفة فاي. يصل منذ فترة طويلة إلى المدخلات ، ونحن لا نعرف قيم المتغيرات الأخرى التي جاءت من الفروع الأخرى.

نعتقد أن الإخراج فاي () سيكون لدينا طويلة.

نوزع المزيد نأتي إلى وظائف محددة ، على سبيل المثال ،
ASSIGN_ADD
و
PRE_INC
. تضيف اثنين طويلة. يمكن أن تكون النتيجة طويلة أو مزدوجة في حالة حدوث تجاوز سعة.

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

حصلنا على مجموعة ممكنة من قيم الكتابة في كل نقطة في البرنامج. هذا جيد بالفعل. يعرف الكمبيوتر بالفعل أن
$i
لا يمكن أن يكون طويلًا أو مزدوجًا ، ويمكنه استبعاد بعض الفحوصات غير الضرورية. لكننا نعرف أن ضعف
$i
لا يمكن أن يكون. كيف نعرف؟ ونحن نرى شرطًا يقيد نمو
$i
في الدورة إلى تجاوز سعة محتمل. سوف نعلم الكمبيوتر أن نرى هذا.
تحسين انتشار النطاق
في تعليمة
PRE_INC
لم نكتشف أبدًا أنه لا يمكنني سوى أن أكون عددًا صحيحًا - فهو يكلف طويلًا أو مزدوجًا. يحدث هذا لأننا لم نحاول استنتاج النطاقات الممكنة. ثم يمكننا الإجابة على السؤال عما إذا كان الفائض سيحدث أم لا.
يرصد هذا الناتج من النطاقات بطريقة مماثلة ، ولكن أكثر تعقيدا قليلا. نتيجة لذلك ، نحصل على مجموعة ثابتة من المتغيرات
$i
مع المؤشرات 2 و 4 و 6 7 ، والآن يمكننا أن نقول بثقة أن الزيادة
$i
لن تؤدي إلى تجاوز السعة.

من خلال الجمع بين هاتين النتيجتين ، يمكننا أن نقول على وجه اليقين أن المتغير المزدوج
$i
أصبحه أبدًا.

كل ما حصلنا عليه ليس التحسين بعد ، فهذه معلومات للتحسين! النظر في
ASSIGN_ADD
. بعبارات عامة ، يمكن أن تكون القيمة القديمة للمبلغ الذي جاء إلى هذه التعليمة ، على سبيل المثال ، كائنًا. بعد ذلك ، بعد الإضافة ، يجب إزالة القيمة القديمة. لكن في حالتنا ، نحن نعرف بالتأكيد أن هناك قيمة طويلة أو مزدوجة ، أي قيمة عددية. لا يلزم التدمير ، يمكننا استبدال
ASSIGN_ADD
بـ
ADD
- وهي تعليمات أسهل. يستخدم
ADD
المتغير
sum
كحجة وقيمة.

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

الآن قارن المتغير في نهاية الحلقة. نحن نعلم أن قيمة المتغير ستكون طويلة فقط - يمكنك التحقق من هذه القيمة على الفور بمقارنتها بالمائة. إذا قمنا في وقت سابق بتسجيل نتيجة التحقق في متغير مؤقت ، ثم تحققنا مرة أخرى من المتغير المؤقت لـ true / false ، يمكن القيام بذلك الآن مع تعليمة واحدة ، وهي التبسيط.

نتيجة Bytecode مقارنة الأصلي.

لا يوجد سوى 3 تعليمات المتبقية في الدورة ، واثنان منهم المتخصصة للغاية. نتيجة لذلك ، الكود الموجود على اليمين
أسرع 3 مرات من الأصل.
معالجات متخصصة للغاية
أي
معالج تتبع ارتباطات PHP هو مجرد دالة C. على اليسار هو معالج قياسي ، وفي الجزء العلوي الأيمن هو أحد المتخصصين للغاية. يتحقق الشخص الأيسر: نوع المعامل ، إذا حدث تجاوز سعة ، في حالة حدوث استثناء. الحق واحد فقط يضيف واحد وهذا كل شيء. إنه يترجم إلى 4 تعليمات الجهاز. إذا ذهبنا أبعد من ذلك ونفذنا JIT ، فسنحتاج فقط إلى تعليمة لمرة واحدة فقط.

ما التالي؟
نستمر في زيادة سرعة PHP branch 7 دون JIT.
سيكون PHP 7.1 مرة أخرى أسرع بنسبة 60٪ في الاختبارات التركيبية النموذجية ، ولكن في التطبيقات الحقيقية لا يكاد هذا يكسب الفوز - 1-2٪ فقط على WordPress. هذه ليست مثيرة للاهتمام بشكل خاص. منذ أغسطس 2016 ، عندما تم تجميد الفرع 7.1 لإجراء تغييرات كبيرة ، بدأنا مرة أخرى العمل على JIT لـ PHP 7.2 أو بالأحرى PHP 8.
في محاولة جديدة ، نستخدم
DynAsm لإنشاء الشفرة ، التي تم تطويرها بواسطة Mike Paul من
أجل LuaJIT-2 . إنه جيد لأنه
يولد الكود بسرعة كبيرة : حقيقة أن الدقائق تم تجميعها في إصدار JIT على LLVM يحدث الآن في 0.1-0.2 ثانية. بالفعل ، أصبح
التسارع على bench.php في JIT أسرع بـ75 مرة من PHP 5.
لا يوجد تسارع في التطبيقات الحقيقية ، وهذا هو التحدي التالي بالنسبة لنا. في جزء منه ، حصلنا على الكود الأمثل ، ولكن بعد تجميع عدد كبير من البرامج النصية لـ PHP ، نسد ذاكرة التخزين المؤقت للمعالج ، لذلك لم يعمل بشكل أسرع. وليس رمز السرعة كان عنق الزجاجة في التطبيقات الحقيقية ...
ربما يمكن استخدام DynAsm لتجميع وظائف معينة فقط يتم اختيارها إما عن طريق مبرمج أو عن طريق الاستدلال القائم على العدادات - كم مرة تم استدعاء وظيفة ، وعدد مرات تكرار الدورات ، وما إلى ذلك.
يوجد أدناه رمز الجهاز الذي تنشئه JIT لنفس المثال. يتم تجميع العديد من الإرشادات على النحو الأمثل: الزيادة في تعليمة CPU واحدة ، التهيئة المتغيرة للثوابت إلى قسمين. عندما لا يتم فقس الأنواع ، عليك أن تزعجك أكثر من ذلك بقليل.

بالعودة إلى صورة العنوان ، يُظهر PHP ، مقارنة باللغات المماثلة في اختبار Mandelbrot ، نتائج جيدة جدًا (على الرغم من أن البيانات ذات صلة بنهاية عام 2016).
يُظهر الرسم البياني وقت التنفيذ بالثواني ، والأقل هو الأفضل.ربما
ماندلبروت ليس هو أفضل اختبار. إنه حسابي ، لكنه بسيط ويتم تنفيذه على قدم المساواة بجميع اللغات. سيكون من الجيد معرفة مدى سرعة عمل WordPress في C ++ ، ولكن لا يكاد يكون هناك أي غرابة جاهزة لإعادة كتابتها لمراجعتها ، وحتى تكرار جميع الانحرافات في كود PHP. إذا كان لديك أفكار لمجموعة معايير أكثر ملاءمة - توحي.
سنلتقي في PHP Russia في 17 مايو ، وسوف نناقش آفاق وتطوير النظام البيئي وتجربة استخدام PHP لمشاريع معقدة حقا. بالفعل معنا:
بالطبع ، هذا أبعد ما يكون عن كل شيء. ولا تزال لعبة Call for Papers مغلقة ، حتى 1 أبريل ، ونحن ننتظر طلبات من أولئك الذين يمكنهم تطبيق الأساليب الحديثة وأفضل الممارسات لتنفيذ خدمات PHP الرائعة. لا تخف من المنافسة مع متحدثين بارزين - نحن نبحث عن خبرة في استخدام ما يفعلونه في مشاريع حقيقية وسنساعد في إظهار فوائد قضاياك.