الشيء الأكثر إثارة للاهتمام في PHP 8

تم الإعلان عن تثبيت PHP 7.4 ، وقد قدمنا ​​بالفعل المزيد من التحسينات. والأهم من ذلك كله ، ما ينتظره PHP يمكن أن يخبر ديمتري ستوغوف - أحد المطورين الرئيسيين لبرنامج Open Source PHP ، وربما أكبر المساهمين النشطين.

جميع تقارير ديمتري هي فقط حول تلك التقنيات والحلول التي يعمل عليها شخصيا. في أفضل تقاليد Ontiko ، في النهاية ، نسخة نصية من القصة حول الأكثر إثارة للاهتمام من وجهة نظر ابتكارات Dmitry من PHP 8 ، والتي يمكن أن تفتح حالات استخدام جديدة. بادئ ذي بدء ، JIT و FFI - ليس في مفتاح "الآفاق المذهلة" ، ولكن مع تفاصيل التنفيذ والمخاطر.


كمرجع: أصبح ديمتري ستوغوف على دراية بالبرمجة في عام 1984 ، عندما لم يولد جميع القراء ، وتمكنوا من تقديم مساهمة كبيرة في تطوير أدوات التطوير ، و PHP على وجه الخصوص (على الرغم من أن ديمتري يحسن أداء PHP ليس خصيصًا للمطورين الروس ، فقد عبروا عن شكري في شكل HighLoad ++ Award). ديميتري مؤلف كتاب Turck MMCache for PHP (eAccelerator) ، معاون Zend OPcache ، رائد مشروع PHPNG ، الذي شكل أساس PHP 7 ، والرائد في تطوير JIT for PHP.

تطوير أداء PHP


بدأت العمل على أداء PHP منذ 15 عامًا عندما انضممت إلى Zend. ثم أصدرنا الإصدار 5.0 - الأول الذي أصبحت فيه اللغة موجهة للكائنات حقًا. منذ ذلك الحين ، تمكنا من تحسين الأداء في الاختبارات الاصطناعية بنسبة 40 مرة ، وعلى التطبيقات الحقيقية بنسبة 6 مرات.



خلال هذا الوقت ، كانت هناك لحظات اختراق:

  • الإصدار 5.1 ، والتي تمكنا من زيادة كبيرة في سرعة التفسير. قمنا بتنفيذ مترجم متخصص ، وهذا أثر في المقام الأول على الاختبارات الاصطناعية.
  • الإصدار 7.0 ، الذي تمت فيه معالجة جميع هياكل البيانات الرئيسية وبالتالي تحسين العمل مع ذاكرة التخزين المؤقت للمعالج والمعالج (اقرأ المزيد عن هذه التحسينات هنا ). أدى هذا إلى أكثر من تسارع مزدوج في كل من الاختبارات الاصطناعية والتطبيقات الحقيقية.

جميع الإصدارات الأخرى زادت تدريجيا الإنتاجية من خلال تنفيذ العديد من الأفكار الأقل فعالية. في الإصدار 7.1 ، على سبيل المثال ، تم إيلاء الكثير من الاهتمام لتحسين رمز الكود ( مقال عن هذه الحلول).

يوضح الرسم البياني أنه في نهاية تطوير الإصدار الخامس وفي نهاية دورة تطوير الإصدار السابع ، نذهب إلى هضبة ونتباطأ. لذلك خلال العام الماضي من العمل على الإصدار v7.4 ، تم تحقيق زيادة بنسبة 2٪ فقط في الإنتاجية. وهذا ليس بالأمر السيئ ، لأن الميزات الجديدة مثل الخصائص المكتوبة وأنواع العناصر المتغيرة قد أظهرت تباطؤ PHP ( تحدث نيكيتا بوبوف عن هذه المنتجات الجديدة في PHP Russia).

والآن الجميع يتساءلون عما يمكن توقعه من الإصدار الثامن ، هل يمكن أن يكرر نجاح الإصدار السابع؟

إلى JIT أم لا إلى JIT


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

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

في الواقع ، فإن العمل على JIT for PHP مستمر منذ عام 2012. كان هناك 3 أو 4 تطبيقات ، لقد عملنا مع زملاء Intel ، والمتسللين JavaScript ، ولكن بطريقة ما لم يكن من الممكن إدراج JIT في الفرع الرئيسي. في النهاية ، في PHP 8 ، قمنا بإدراج JIT في المترجم وشاهدنا تسارعًا مزدوجًا ، ولكن فقط في الاختبارات الاصطناعية ، ولكن في التطبيقات الحقيقية ، على العكس من ذلك ، التباطؤ.



بالطبع ، هذا ليس ما نسعى إليه.

ما هو الموضوع؟ ربما نرتكب خطأً ، ربما يكون WordPress سيئًا للغاية ، ولن تساعده JIT (نعم ، إنه بالفعل). ربما جعلنا المترجم جيدًا بالفعل ، لكن في جافا سكريبت ، أصبح الوضع أسوأ. في الاختبارات الحسابية ، هذا صحيح: فمترجم PHP هو أحد الأفضل .



في اختبار Mandelbrot ، يتفوق حتى على اللؤلؤ مثل LuaJIT ، وهو مترجم مكتوب بلغة التجميع. في هذا الاختبار ، نحن نتخلف 4 مرات فقط عن المترجم الأمثل GCC-5.3. مع JIT ، يمكننا الحصول على نتائج أفضل في اختبار Mandelbrot. في الواقع ، نحن نفعل هذا بالفعل ، أي أننا قادرون على إنشاء كود يتنافس مع برنامج التحويل البرمجي C.

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

كيف يعمل PHP



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

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

لكن على الأرجح لا يستخدم أحد PHP في شكله العاري ، يستخدمه الجميع مع OPcache.

PHP + OPcache



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

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

يمكنك بالفعل تضمين JIT في هذه الدائرة ، وهو ما سنفعله. لكن أولاً ، سأريك كيف يعمل المترجم الفوري.



المترجم هو أولاً حلقة تستدعي معالجها الخاص لكل تعليمة.

نستخدم سجلين:

  • execute_data - مؤشر إلى إطار التنشيط الحالي ؛
  • opline - مؤشر إلى التعليمات الافتراضية القابلة للتنفيذ الحالية.

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

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

من المهم ملاحظة أن عنوان المعالج تتم كتابته مباشرة إلى الرمز البريدي. يمكن أن يكون هناك عدة معالجات مختلفة لتعليم واحد. تم اختراع هذا الأصل للتخصص بحيث يمكن للمتخصصين أن يتخصصوا في أنواع المعامل. يتم استخدام نفس التقنية لـ JIT ، لأنه إذا قمت بكتابة العنوان على الكود الجديد الذي تم إنشاؤه كمعالج ، فسيتم تشغيل معالجات JIT دون أي تغييرات في المترجم الفوري.

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

يتم إنشاء وظائف متخصصة من هذا الوصف. نظرًا لوجود ثلاثة معاملات أولية محتملة ، وثلاثة معاملات ثانية محتملة ، نحصل على 9 وظائف مختلفة.



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

الهجين آلة افتراضية


المضاعفات الأخرى التي قطعناها على أنفسنا في الإصدار 7.2 هو ما يسمى الجهاز الظاهري الهجين.

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



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

يسمح الجهاز الظاهري الهجين لزيادة الإنتاجية بنسبة 5-10 ٪ أخرى.

PHP + OPcache + JIT


يتم تنفيذ JIT كجزء من OPcache.



بعد تجميع البايت كود وتحسينه ، يتم إطلاق برنامج التحويل البرمجي JIT ، والذي لم يعد يعمل مع الكود المصدري. من رمز PHP bytecode ، يقوم برنامج التحويل البرمجي JIT بإنشاء تعليمة برمجية أصلية ، وبعد ذلك يتم تغيير عنوان التعليمة الأولى (في الواقع الوظيفة) في الرمز الثانوي.

بعد ذلك ، يبدأ استدعاء الكود الأصلي الذي تم إنشاؤه بالفعل من المترجم الحالي دون أي تغييرات. سأريك مثالا بسيطا.



على اليسار ، تتم كتابة وظيفة معينة في PHP تحسب مجموع الأرقام من 0 إلى 100. على اليمين ، الرمز الثنائي الذي تم إنشاؤه. يعين التعليمة الأولى 0 للمجموع ، والثاني يفعل نفس الشيء بالنسبة i ، ثم قفزة غير مشروطة إلى الملصق. في الملصق L1 ، يتم التحقق من حالة الخروج من الدورة: إذا تم الوفاء بها ، ثم الخروج ، وإذا لم يكن كذلك ، فانتقل إلى الدورة. بعد ذلك ، أضف إلى المبلغ i ، اكتب النتيجة في المبلغ ، قم بزيادة i بمقدار 1.

مباشرة من هنا نقوم بإنشاء رمز المجمّع ، والذي يتضح جيدًا.



QM_ASSIGN تجميع تعليمة QM_ASSIGN الأولى في إرشادات الجهاز اثنين فقط (2-3 خطوط). يحتوي سجل %esi على مؤشر لإطار التنشيط الحالي. في الإزاحة 30 يكمن مقدار متغير. التعليمة الأولى تكتب القيمة 0 ، والثانية تكتب 4 - هذا هو معرف نوع صحيح ( IS_LONG ). بالنسبة للمتغير i أدرك المترجم أنه دائمًا ما يكون طويلًا ، وأنه ليست هناك حاجة لتخزين النوع. علاوة على ذلك ، يمكن تخزينها في سجل الجهاز. لذلك ، هنا ببساطة XOR من التسجيل مع نفسه هو أبسط وأرخص تعليمات لإعادة.

ثم ، بنفس الطريقة ، أي انتقال غير مشروط ، نتحقق مما إذا كان هناك حدث خارجي قد حدث ، ونتحقق من حالة الدورة ، ونذهب إلى الدورة. تقوم الحلقة بالتحقق مما إذا كان المجموع صحيحًا: إذا كانت الإجابة بنعم ، فاقرأ قيمة العدد الصحيح ، وأضف القيمة i إليها ، وتحقق من حدوث تجاوز سعة ، واكتب النتيجة مرة أخرى إلى المجموع ، ثم أضف 1 إلى %edx .

يمكن أن نرى أن الرمز قريب من الأمثل. سيكون من الممكن تحسينه أكثر ، والتخلص من التحقق من مجموع النوع في كل تكرار للحلقة. ولكن هذا هو بالفعل الأمثل معقدة للغاية ، ونحن لا نفعل ذلك حتى الآن. نحن نعمل على تطوير JIT كتقنية بسيطة إلى حد ما ، ونحن لا نحاول القيام بما تحاول Java HotSpot القيام به ، V8 - لدينا طاقة أقل.

ما هو الخطأ في جيت


لماذا ، مع رمز التجميع الجيد ، لا يمكننا تسريع التطبيقات الحقيقية؟

في الواقع ، يجب عليهم؟

  • إذا لم يكن عنق الزجاجة في وحدة المعالجة المركزية ، فلن تساعد JIT.
  • يتم إنشاء الكثير من التعليمات البرمجية (رمز bloat).
  • ثابت نوع الاستدلال لا يعمل دائما.
  • كود صادق (للحالات التي لا يتم تنفيذها مطلقًا).
  • دعم لحالة ثابتة من الجهاز الظاهري (وفجأة استثناء).
  • الطبقات تعيش فقط لطلب واحد.

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

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

خطط متواضعة ل PHP 8


أحد التحسينات التي نريد تحقيقها في PHP 8 هو إنشاء كود أقل . الآن ، كما قلت ، نقوم بإنشاء كود أصلي للنص بأكمله ، والذي نقوم بتحميله في مرحلة التحميل. ولكن بالتأكيد لن يتم استدعاء نصف الوظائف. لذلك ذهبنا أبعد من ذلك قليلاً وقدمنا ​​أداة تسمح لنا بالتهيئة عندما نريد تشغيل JIT. يمكن تشغيله:

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

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

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



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

لكل شيء آخر ، سنضع بذرة. ومع ذلك ، ستكون هناك فحوصات ونواتج محتملة تبدأ عندها عملية إزالة البيانات ، أي أننا سنستعيد حالة الجهاز الظاهري اللازمة للترجمة الشفوية ونمنحها للمترجم الفوري للتنفيذ.

يتم استخدام نظام مماثل في كل من HotSpot Java VM و V8. لكن تكييف التكنولوجيا مع PHP لديه عدد من الصعوبات. بادئ ذي بدء ، هذا هو أننا شاركنا الرمز الثنائي والرمز الأصلي المشترك المستخدم من عمليات مختلفة. لا يمكننا تغييرها مباشرةً في الذاكرة المشتركة ، يجب أولاً نسخ مكان ما ، والتغيير ، ثم الالتزام بالذاكرة المشتركة.

تحميلها مسبقا. مشكلة الطبقة ملزمة


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

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

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

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

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

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

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

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

 <?php function _preload(string $preload, string $pattern = "/\.php$/") { if (is_file($path) && preg_match($pattern, $path)) { opcache_compile_file($path) or die("Preloading failed"); } else if (is_dir($path)) { if ($dh = opendir($path)) { while (($file = readdir($dh)) !== false) { if ($file !== "." && $file !== "..") { _preload($path . "/" . $file, $pattern); } } closedir($dh); } } } _preload("/usr/local/lib/ZendFramework"); 

هذا هو أحد السيناريوهات المحتملة التي تقرأ جميع الملفات بشكل متكرر في بعض الدليل (في هذه الحالة ، ZendFramework). يمكنك تطبيق أي نص برمجي بالكامل في PHP: اقرأ باستخدام قائمة ، أو أضف استثناءات ، أو حتى تقاطع مع الملحن بحيث تكون ملفات podsoval اللازمة للتحميل المسبق. هذه كلها مسألة تقنية ، والأكثر إثارة للاهتمام ليس كيفية الشحن ، ولكن ما يجب شحنها.

ما لتحميل في التحميل المسبق


جربت هذه التكنولوجيا على ووردبريس. إذا قمت فقط بتحميل جميع ملفات * .php ، فإن برنامج WordPress سيتوقف عن العمل بسبب الميزة المذكورة سابقًا: فهو يحتوي على وظيفة تحقق من وجودها ، والتي تصبح دائمًا صحيحة. لذلك ، اضطررت إلى تعديل البرنامج النصي قليلاً من المثال السابق (إضافة استثناءات) ، ومن ثم ، دون أي تغييرات في WordPress نفسها ، نجحت.

السرعة [مسا / ثانية]الذاكرة [MB]عدد النصوصعدد الوظائفعدد الفصول
لا شيء3780000
الكل (تقريبا *)3957.52541770148
البرامج النصية المستخدمة فقط3964.584153251

نتيجة لذلك ، نظرًا للتحميل المسبق ، حصلنا على تسارع بنسبة 5٪ تقريبًا ، وهو أمر ليس سيئًا بالفعل.

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

FFI - واجهة الوظيفة الخارجية


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

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

تتيح لك FFI العمل مع هياكل البيانات المحددة في C ويمكن دمجها مع JIT لإنشاء كود أكثر فعالية. تطبيقه القائم على libffi مدرج بالفعل في PHP 7.4.

ولكن:

  • هذه هي 1000 طريقة جديدة لإطلاق النار على نفسك.
  • يتطلب معرفة C وأحيانا إدارة الذاكرة اليدوية.
  • لا يدعم C-preprocessor (# تتضمن ، #define ، ...) و C ++.
  • الأداء بدون JIT منخفض جدًا.

رغم ذلك ، ربما بالنسبة للبعض سيكون مناسبًا ، لأن المترجم غير مطلوب. حتى في ظل نظام Windows ، سيعمل هذا بدون أي Visual-C من PHP.

سأريك كيفية استخدام FFI لتطبيق تطبيق واجهة المستخدم الرسومية الحقيقي لنظام Linux.

لا تشعر بالانزعاج من رمز C ، فقد كتبت بنفسي واجهة المستخدم الرسومية في C قبل حوالي 20 عامًا ، لكنني وجدت هذا المثال على الإنترنت.

 #include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Hello from C"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); gtk_widget_show_all(window); } int main() { int status; GtkApplication *app; app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), 0, NULL); g_object_unref(app); return status; } 

يقوم البرنامج بإنشاء التطبيق ، وتعليق الحدث تنشيط رد الاتصال ، إطلاق التطبيق. في رد الاتصال ، قم بإنشاء نافذة ، وقم بتعيين حجم العنوان لها وإظهاره.

والآن ، تمت إعادة كتابة نفس الشيء في PHP:

 <?php $ffi = FFI::cdef(" … // #include <gtk/gtk.h> ", "libgtk-3.so.0"); function activate($app, $user_data) { global $ffi; $window = $ffi->gtk_application_window_new($app); $ffi->gtk_window_set_title($window, "Hello from PHP"); $ffi->gtk_window_set_default_size($window, 200, 200); $ffi->gtk_widget_show_all($window); } $app = $ffi->gtk_application_new("org.gtk.example", 0); $ffi->g_signal_connect_data($app, "activate", "activate", NULL, NULL, 0); $ffi->g_application_run($app, 0, NULL); $ffi->g_object_unref($app); 

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

يمكن أن نرى أن كل شيء هو نفسه كما في المثال السابق. الفرق الوحيد هو أننا في C أرسلنا رد اتصال كعنوان ، وفي PHP ، يحدث الاتصال بالاسم المعطى من السلسلة.

الآن لنرى كيف تبدو الواجهة. في الجزء الأول ، نحدد الأنواع والوظائف في C ، وفي السطر الأخير نقوم بتحميل المكتبة المشتركة:

 <?php $ffi = FFI::cdef(" typedef struct _GtkApplication GtkApplication; typedef struct _GtkWidget GtkWidget; typedef void (*GCallback)(void*,void*); int g_application_run (GtkApplication *app, int argc, char **argv); unsigned long * g_signal_connect_data (void *ptr, const char *signal, GCallback handler, void *data, GCallback *destroy, int flags); void g_object_unref (void *ptr); GtkApplication * gtk_application_new (const char *app_id, int flags); GtkWidget * gtk_application_window_new (GtkApplication *app); void gtk_window_set_title (GtkWidget *win, const char *title); void gtk_window_set_default_size (GtkWidget *win, int width, int height); void gtk_widget_show_all (GtkWidget *win); ", "libgtk-3.so.0"); ... 

في هذه الحالة ، يتم نسخ تعريفات C هذه من ملفات h لمكتبة GTK ، دون تغيير تقريبًا.

حتى لا تتداخل مع C و PHP في نفس الملف ، يمكنك وضع C-code بالكامل في ملف منفصل ، على سبيل المثال ، مع الاسم gtk-ffi.h وإضافة زوجين من التعريف الخاص إلى البداية التي تحدد اسم الواجهة والمكتبة للتحميل:

 #define FFI_SCOPE "GTK" #define FFI_LIB "libgtk-3.so.0" 

وبالتالي ، اخترنا الوصف الكامل للواجهة C في ملف واحد. هذا gtk-ffi.h حقيقي تقريبًا ، لكن لسوء الحظ ، لم ننفذ بعد معالجًا C ، مما يعني أن وحدات الماكرو وتشمل لن تعمل.

الآن لنقم بتحميل هذه الواجهة في PHP:

 <?php final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::load(__DIR__ . "/gtk_ffi.h"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

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

لم يتغير باقي الكود كثيرًا ، فقط عندما بدأ معالج الأحداث في استخدام دالة غير مسماة وتمرير العنوان باستخدام الربط المعجمي. وهذا يعني أننا نستخدم كل من C ونقاط القوة في PHP ، والتي لا تتوفر في C.

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

لتجنب ذلك ، وفي الواقع ، السماح لكتابة امتدادات PHP في PHP نفسها ، قررنا عبور FFI مع التحميل المسبق.

FFI + التحميل المسبق


لم يتغير الكود كثيرًا ، والآن فقط نعطي ملفات h للتحميل المسبق ، وننفذ FFI::load مباشرة في وقت التحميل المسبق ، وليس عند إنشاء الكائن. أي ، عند تحميل المكتبة ، تتم جميع عمليات التحليل والربط مرة واحدة (عند بدء تشغيل الخادم) ، وباستخدام FFI::scope("GTK") يمكننا الوصول إلى الواجهة المحملة مسبقًا بالاسم في البرنامج النصي الخاص بنا.

 <?php FFI::load(__DIR__ . "/gtk_ffi.h"); final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::scope("GTK"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

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

يمكن استخدام ملحق تم إنشاؤه بهذه الطريقة مباشرةً من سطر الأوامر:
 $ php -d opcache.preload=gtk.php -r 'GTK::create_window(" !");' 

هناك ميزة أخرى من التهجين والتحميل المسبق لـ FFI وهي القدرة على حظر استخدام FFI لجميع البرامج النصية على مستوى المستخدم. يمكنك تحديد ffi.enable = التحميل المسبق ، مما يعني أننا نثق في الملفات المحمّلة مسبقًا ، لكن استدعاء FFI من البرامج النصية المعتادة لـ PHP محظور.

العمل مع هياكل البيانات C


ميزة أخرى مثيرة للاهتمام من FFI هي أنه يمكن أن تعمل مع هياكل البيانات الأصلية. يمكنك في أي وقت إنشاء بنية بيانات موصوفة في C.

 <?php $points = FFI::new("struct {int x,y;} [100]"); for ($x = 0; $x < count($points); $x++) { $points[$x]->x = $x; $points[$x]->y = $x * $x; } var_dump($points[25]->y); // 625 var_dump(FFI::sizeof($points)); // 800  foreach ($points as &$p) { $p->x += 10; } var_dump($points[25]->x); // 35 

نقوم بإنشاء مجموعة من 100 بنية (لاحظ FFI :: new! = New FFI) تحتوي على رقمين صحيحين. داخل الذاكرة ، سيتم تمثيله تمامًا كما هو مكتوب في C. وبعد ذلك ، يمكننا العمل مع بنية البيانات هذه باستخدام بدائل PHP العادية ، كما لو كانت مجموعة من الكائنات. في هذه الحالة ، يمكنك استخدام عدد ، قراءة / كتابة عناصر الصفيف وحتى التكرار باستخدام foreach حسب المرجع. وتستغرق هذه البنية 800 بايت فقط ، وإذا قمنا ببناء بنية بيانات مماثلة من صفائف وكائنات PHP في PHP ، فسيستغرق ذلك 10 أضعاف.

أمثلة على استخدام FFI:


Python/CFFI : (Cario, JpegTran), (ffmpeg), (LibreOfficeKit), (SDL) (TensorFlow).

, FFI .

- PHP. , , callback' , . FFI. , . FFI c JIT, , LuaJIT, . , , .

 for ($k=0; $k<1000; $k++) { for ($i=$n-1; $i>=0; $i--) { $Y[$i] += $X[$i]; } } 

FFI .
Native ArraysFFI Arrays
PyPy0,0100,081
Python0,2120,343
LuaJIt -joff0,0370,412
LuaJit -jon0,0030,002
PHP0,0400,093
PHP + JIT0,0160,087

: Zeev Surasky (Zend), Andi Gutmans (ex-Zend, Amazon), Xinchen Hui (ex-Weibo, ex-Zend, Lianjia), Nikita Popov (JetBrains), Anatol Belsky (Microsoft), Anthony Ferrara (ex-Google, Lingo Live), Joe Watkins, Mohammad Reza Haghighat (Intel) Intel, Andy Wingo (JS hacker, Igalia), Mike Pall ( LuaJIT).

, , .

PHP Russia 2020 ! telegram- , 2019 youtube- , , — .

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


All Articles