كيف صنعنا PHP 7 ضعف سرعة PHP 5. الجزء 1: تحسين هياكل البيانات

في ديسمبر 2015 ، تم إصدار PHP 7.0. لاحظت الشركات التي تحولت إلى "السبعة" أن الإنتاجية قد زادت ، وانخفض الحمل على الخادم. كان أول من انتقل إلى السبعة هم Vebia و Etsy ، ولدينا Badoo و Avito و OLX. بالنسبة إلى Badoo ، فإن التحويل إلى السبعة يكلف 1 مليون دولار في توفير الخادم. بفضل PHP 7 في OLX ، انخفض متوسط ​​تحميل الخادم بمقدار 3 مرات ، مما أدى إلى زيادة الكفاءة وتوفير الموارد.

تحدث ديمتري ستوغوف من Zend Technologies في HighLoad ++ ، مما زاد من الإنتاجية. في فك التشفير: حول البنية الداخلية لـ PHP ، حول الأفكار الموجودة في قلب الإصدار 7.0 ، حول التغييرات في بنيات البيانات الأساسية والخوارزميات التي حددت النجاح.

إخلاء المسئولية: اعتبارًا من مارس 2019 ، تعمل 80٪ من المواقع على PHP ، و 70٪ منها تعمل على PHP 5 ، على الرغم من أن هذا الإصدار غير مدعوم منذ 1 يناير 2019 . إن تقرير ديمتري لعام 2016 حول المبادئ التي كان هناك قفزة مضاعفة في الإنتاجية بين PHP 5 و 7 مناسب أيضًا في مارس 2019. بالتأكيد ، بالنسبة لنصف المواقع ، بالتأكيد.

نبذة عن المتحدث: بدأ ديمتري ستوغوف البرمجة في الثمانينات: "إلكترونيات B3-34" ، Basic ، مجمّع. في عام 2002 ، تعرف ديمتري على PHP وسرعان ما بدأ العمل على تحسينها: قام بتطوير Turck MMCache for PHP ، وقاد مشروع PHPNG ولعب دورًا مهمًا في العمل على JIT for PHP. آخر 14 عامًا من المهندس الرئيسي في Zend Technologies.

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

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

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

استطرادا صغيرا في تاريخ PHP


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

قدم المبرمج الدنماركي راسموس ليردورف برنامج PHP في يونيو 1995 . في ذلك الوقت ، كانت مجرد مجموعة من نصوص CGI المكتوبة بلغة بيرل . في أبريل 96 ، قدمت Rasmus PHP / FI ، وفي يونيو تم إصدار PHP / FI 2.0. في وقت لاحق ، تمت إعادة صياغة هذا الإصدار بشكل كبير بواسطة Andy Gutmans و Zeev Surasky ، وفي الإصدار 98 من PHP 3.0. بحلول عام 2000 ، أصبحت اللغة من النوع الذي اعتدنا أن نراه اليوم من حيث اللغة والهندسة الداخلية - PHP 4 ، على أساس محرك Zend.

منذ الإصدار 4 ، تطورت PHP. كانت نقطة التحول هي إصدار PHP 5 في عام 2004 ، عندما تم تحديث طراز الكائن بالكامل . هي التي فتحت عصر أطر عمل PHP ورفعت مسألة الأداء إلى مستوى جديد. توقعًا لذلك ، بعد إصدار 5.0 مباشرة ، فكرنا في Zend في تسريع PHP وبدأنا العمل على تحسين الإنتاجية.

الإصدار 7.1 ، الذي تم إصداره في نوفمبر 2016 على الاختبارات الاصطناعية ، هو 25 مرة أسرع من نسخة 2002 . وفقًا للرسم البياني للتغيرات في الأداء في الفروع المختلفة ، تظهر الاختراقات الرئيسية في 5.1 و 7.0.



في الإصدار 5.1 ، بدأنا للتو العمل على الأداء ، وكل شيء تابعناه - انتهى الأمر ، ولكن بعد أن دخلنا إلى الحائط 5.3 ، لم تفلح كل محاولات تحسين المترجم الشفهي.

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



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

يبدأ تاريخ PHP 7 بالركود المستمر منذ ثلاث سنوات والذي بدأ في عام 2012 وانتهى في عام 2015 بإصدار الإصدار السابع. ثم أدركنا أننا لم نعد قادرين على زيادة الإنتاجية من خلال تحسينات بسيطة لمترجمنا الفوري وتحولنا إلى الجانب JIT.

يتجول حول جيت


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

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

لتجنب المشكلات ذات المستوى المنخفض ، قررنا تجربة LLVM ، وبعد ذلك بعام حصلنا على تسارع 10x لـ bench.php ، لكن لا شيء على التطبيقات الحقيقية. بالإضافة إلى ذلك ، استغرق تجميع التطبيقات الحقيقية الآن دقائق ، على سبيل المثال ، استغرق الطلب الأول إلى Wordpress دقيقتين ولم يعط تسارعًا. بالطبع ، كان هذا غير مناسب تمامًا لممارسة حقيقية.

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

ما يبطئ؟


نعيد التفكير في أسباب الإخفاقات وقررنا مرة أخرى معرفة سبب بطء PHP. تُظهر الصورة نتيجة لمحات عن عدة طلبات إلى صفحة وورد الرئيسية.



يتم إنفاق أقل من 30٪ على تفسير الرمز الفرعي ، 20٪ هو الحمل الإضافي لمدير الذاكرة ، ويعمل 13٪ مع جداول التجزئة ، بينما يعمل 5٪ مع التعبيرات المعتادة.

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

PHPNG. جيل جديد


تم تطوير المشروع بعد محاولات فاشلة لإنشاء JIT لـ PHP. الهدف الرئيسي هو تحقيق مستوى جديد من الإنتاجية ووضع الأساس للتحسينات المستقبلية .

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

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

لم نكن متأكدين من أننا سننهي المشروع. لذلك ، أطلقوا المشروع سرا وفتحوه فقط عندما ظهرت أول نتائج متفائلة. استغرق الأمر أسبوعين لتجميع النواة . بعد أسبوعين ، حصل bench.php. قضينا شهر ونصف لضمان عمل وورد. بعد شهر ، افتتحنا المشروع - كان مايو 2014. في ذلك الوقت ، كان لدينا تسارع بنسبة 30 ٪ على وورد . بدا بالفعل وكأنه حدث كبير.

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

PHP 7.0


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

بحلول هذا الوقت ، كان قد تم بالفعل تجميع الكثير من المواد المخصصة لـ PHP 6: خطب في المؤتمرات ، والكتب المنشورة. من أجل عدم الخلط بين أي شخص ، أطلقنا على المشروع PHP 7 ، تخطي PHP 6. كان هذا الإصدار أكثر حظًا - تم إصدار PHP 7 في ديسمبر 2015 ، وفقًا للخطة.

بالإضافة إلى الأداء ، ظهرت بعض الابتكارات التي طال انتظارها في PHP 7:

  • القدرة على تحديد أنواع العددية من المعلمات وقيم الإرجاع.
  • استثناءات بدلاً من الأخطاء - الآن يمكننا التقاطها ومعالجتها.
  • ظهر Zero-cost assert() ، فصول مجهولة ، عدم تناسق التنظيف ، عوامل تشغيل ووظائف جديدة (<=> ، ؟؟).

الابتكار هو جيد ، ولكن العودة إلى التغييرات الداخلية. دعنا نتحدث عن المسار الذي اتبعه PHP 7 وأين يمكن أن يقودنا هذا المسار.

zval


هذا هو هيكل البيانات PHP الأساسية. يتم استخدامه لتمثيل أي قيمة في PHP . نظرًا لأن لغتنا تتم كتابتها ديناميكيًا ويمكن أن يتغير نوع المتغيرات أثناء تنفيذ البرنامج ، فنحن بحاجة إلى تخزين حقل كتابة (نوع zend_uchar) ، والذي يمكن أن يأخذ القيم IS_NULL و IS_BOOL و IS_LONG و IS_DOUBLE و IS_ARRAY و IS_OBJECT وما إلى ذلك ، وفي الواقع القيمة الممثلة بالاتحاد (القيمة) ، حيث يمكن تخزين عدد صحيح أو رقم حقيقي أو سلسلة أو صفيف أو كائن.

zval في PHP 5


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

تُظهر الصورة في أعلى اليمين هياكل البيانات التي تم إنشاؤها في ذاكرة PHP 5 لبرنامج نصي بسيط.



على المكدس ، تم تخصيص الذاكرة لـ 4 متغيرات تمثلها المؤشرات. القيم نفسها (zval) موجودة على كومة الذاكرة المؤقتة. في حالتنا ، هذه عبارة عن اثنين فقط من zval ، تتم الإشارة إلى كل منهما بمتغيرين ، وبالتالي يتم تعيين عدادات المرجع الخاصة بهما على 2.

للوصول إلى نوع أو قيمة عددية ، تحتاج إلى قراءتين على الأقل: أولاً قم بقراءة قيمة المؤشر ، ثم قيمة البنية. إذا كنت لا تحتاج إلى قراءة قيمة عددية ، ولكن ، على سبيل المثال ، جزء من سلسلة أو صفيف ، فستحتاج إلى قراءة واحدة على الأقل.

zval في PHP 7


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



على اليسار هو ما بدا عليه في PHP 5 ، وعلى اليمين ، في PHP 7.



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

نسخة السجل


في السطر العلوي من البرنامج النصي ، تمت إضافة مهمة أخرى.



في PHP5 ، قمنا بتخصيص ذاكرة من كومة الذاكرة المؤقتة zval الجديدة ، وقمنا بتهيئة int (2) ، وقمنا بتغيير قيمة المؤشر إلى المتغير b ، وقللنا العداد المرجعي للقيمة التي أشار إليها b سابقًا.

في PHP 7 ، قمنا ببساطة بتهيئة المتغير b مباشرة في المكان مع بعض الإرشادات ، بينما في PHP 5 تطلب مئات التعليمات. لذلك يبدو zval الآن في الذاكرة.



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

العلامات هي مجموعة من البتات حيث يشير كل بت إلى ما إذا كان zval يدعم البروتوكول. على سبيل المثال ، إذا كان IS_TYPE_REFCOUNTED ، فعند العمل مع هذا zval ، يجب على المحرك الاهتمام بقيمة العداد المرجعي. عند التعيين ، الزيادة ؛ عند الخروج من النطاق ، إنقاص ؛ إذا كان العداد المرجعي يصل إلى الصفر ، فقم بتدمير البنية التابعة.

من بين الأنواع ، مقارنة بـ PHP 5 ، ظهرت عدة أنواع جديدة.

  • IS_UNDEF - علامة متغير غير مهيأ.
  • IS_BOOL استبدال IS_FALSE المنفصل بـ IS_FALSE و IS_TRUE .
  • إضافة نوع منفصل للروابط وبعض الأنواع السحرية.

أنواع من IS_UNDEF إلى IS_DOUBLE هي عددية ، ولا تتطلب ذاكرة إضافية. لنسخها ، يكفي نسخ أول كلمة 64 بت من الآلة بقيمة ونصف الثانية بنوع وإشارات.

Refcounted


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



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

خطوط


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



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

كما هو الحال في PHP 5 ، لا يزال لدينا مفهوم السلاسل الثابتة أو الداخلية . عادة ما تكون موجودة في مثيل واحد ، وتعيش حتى نهاية الاستعلام ، ويمكن أن تتصرف مثل القيم العددية. لا نحتاج لرعاية عداد المراجع إليهم ، ولنسخه يكفي نسخ zval نفسه فقط بمساعدة أربعة إرشادات الجهاز.

المصفوفات


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



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

هكذا يبدو جدول التجزئة في PHP 5.



هذا هو تطبيق جدول التجزئة الكلاسيكي مع دقة التصادم باستخدام القوائم الخطية (كما هو موضح في الزاوية اليمنى العليا). يتم تمثيل كل عنصر بواسطة دلو. يتم ربط جميع المجموعات بواسطة قوائم مرتبطة مزدوجة لحل التصادمات ، ويتم ربطها من خلال قائمة أخرى مرتبطة مضاعفة بالتكرار بالترتيب. يتم تخصيص القيم لكل zval بشكل منفصل - في Bucket نقوم فقط بتخزين رابط لها. أيضا ، يمكن تخصيص مفاتيح السلسلة بشكل منفصل.

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

هذا ما حدث في PHP 7.



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

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

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

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

صفائف غير قابلة للتغيير


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



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

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

الكائنات


تكمن الروابط لجميع الكائنات في PHP 5 في مستودع منفصل ، وفي zval كان هناك مقبض فقط - معرف كائن فريد.



للوصول إلى الكائن ، أجرينا ما لا يقل عن 3 قراءات. بالإضافة إلى ذلك ، تم تخصيص ذاكرة قيمة كل خاصية للكائن بشكل منفصل ، ونحن بحاجة إلى قراءتين على الأقل لقراءتها.

في PHP 7 ، تمكنا من الانتقال إلى عنونة مباشرة.



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

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

إشارة


أخيرًا ، كان علينا تقديم نوع منفصل لتمثيل روابط PHP.



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

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

IS_FALSE و IS_TRUE


لقد قلت بالفعل أن النوع الوحيد IS_BOOL تم تقسيمه إلى IS_FALSE و IS_TRUE منفصلين. تم التجسس على هذه الفكرة في تطبيق LuaJIT ، وتم صنعها لتسريع واحدة من أكثر العمليات شيوعًا - الانتقال الشرطي.



إذا كان مطلوبًا في PHP 5 قراءة النوع ، والتحقق من المنطقية ، وقراءة القيمة ، ومعرفة ما إذا كانت صحيحة أم خاطئة وإجراء عملية نقل بناءً على ذلك ، يكفي الآن التحقق من النوع ومقارنته بالصحيح:

  • إذا كان هذا صحيحًا ، فنحن نمضي في فرع واحد ؛
  • إذا كان أقل من صحيح ، انتقل إلى فرع آخر ؛
  • إذا كان أكثر من صحيح ، فانتقل إلى ما يسمى المسار البطيء (المسار البطيء) وهناك نتحقق من النوع الذي جاء منه وماذا نفعل به: إذا كان عددًا صحيحًا ، فيجب علينا مقارنة قيمته بالرقم 0 ، إذا كان الطفو - مرة أخرى بالرقم 0 ( لكن حقيقي) ، الخ

دعوة الاتفاقية


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



أولاً ، سوف أخبرك كيف عملت في PHP 5.

استدعاء الاتفاقية في PHP 5


أول عبارة SEND_VAL كانت لإرسال القيمة "3" إلى وظيفة foo. للقيام بذلك ، اضطرت إلى تخصيص zval جديد على الكومة ، وانسخ القيمة (3) هناك واكتب قيمة المؤشر إلى هذه البنية على المكدس.



وبالمثل مع التعليمات الثانية. قام DO_FCALL بتهيئة CALL FRAME ، وحجز مكانًا للمتغيرات المحلية والمؤقتة ، ونقل التحكم إلى الوظيفة المطلوبة.



فحص RECV الأول الوسيطة الأولى وتهيئة الفتحة على الرصة باستخدام المتغير المحلي المقابل ($ a). لقد فعلنا هنا بدون نسخ وقمنا ببساطة بزيادة العداد المرجعي للمعلمة المقابلة (zval بقيمة 3). وبالمثل ، RECV الثاني اتصالًا بين المتغير $ b والمعلمة 5.



المزيد من وظائف الجسم. حدث إضافة 3 + 5 - اتضح 8. هذا متغير مؤقت وتم تخزين قيمته مباشرة على المكدس.



العودة ونعود من وظيفة.



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

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

استدعاء الاتفاقية في PHP 7


في PHP 7 ، تم حل هذه المشكلات - الآن في المجموعة ، لا نقوم بتخزين مؤشرات zval ، ولكن تلك zval هي نفسها.



قدمنا ​​أيضًا تعليمة جديدة ، INIT_FCALL ، وهي الآن مسؤولة عن تهيئة الذاكرة وتخصيصها ضمن CALL FRAME ، وحجز مساحة INIT_FCALL والمتغيرات المؤقتة.



SEND_VAL 3 الآن مجرد نسخ الوسيطة إلى الفتحة الأولى بعد CALL FRAME . التالي SEND_VAL 5 إلى الفتحة الثانية.



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



لذلك نذهب مباشرة إلى الجسم من وظيفة ، وجعل الجمع والعودة.



عند العودة ، نقوم بمسح جميع المتغيرات المحلية ، ولكن الآن فقط لشريحتين ، وبما أن لدينا عددًا قياسيًا ، فلن نحتاج مرة أخرى إلى فعل أي شيء.



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

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



بالإضافة إلى ذلك ، لم يعد من الممكن أن تحتوي الدالة على معلمات متعددة بنفس الاسم. لم يكن هناك أي نقطة في هذا من قبل ، لكن قابلت كود PHP هذا foo($_, $_) . كيف تبدو؟ (لقد أدركت Prolog)

مدير ذاكرة جديد


بعد الانتهاء من تحسين هياكل البيانات والخوارزميات الأساسية ، لفتنا الانتباه مرة أخرى إلى جميع أنظمة الفرامل الفرعية. استغرق مدير الذاكرة في PHP 5 ما يقرب من 20 ٪ من وقت المعالج على وورد.

بعد أن تخلصنا من الكثير من المخصصات ، أصبحت تكاليفه العامة أقل ، لكنها لا تزال كبيرة - وليس لأنه كان يقوم بأي عمل مهم ، ولكن لأنه عثر عليه في ذاكرة التخزين المؤقت. حدث هذا نظرًا لحقيقة أننا استخدمنا خوارزمية malloc الكلاسيكية الخاصة بـ Doug Lea ، والتي تضمنت إيجاد مواقع ذاكرة مناسبة مناسبة من خلال السفر عبر الروابط والأشجار ، وكل هذه الرحلات تسببت حتماً في فقد ذاكرة التخزين المؤقت.

اليوم ، هناك خوارزميات جديدة لإدارة الذاكرة تأخذ في الاعتبار ميزات المعالجات الحديثة. على سبيل المثال: jemalloc و ptmalloc من Google . في البداية ، حاولنا استخدامها دون تغيير ، لكننا لم نحقق أي فوز ، لأن نقص الوظائف الخاصة بـ PHP جعل الأمر أكثر تكلفة لتحرير الذاكرة بالكامل في نهاية الطلب. نتيجة لذلك ، تخلينا عن dlmalloc وكتبنا شيئًا خاصًا بنا ، ونجمع بين الأفكار من مدير الذاكرة القديم و jemalloc.

لقد قمنا بتخفيض مقدار الحمل الإضافي لـ Memory Manager إلى 5٪ ، وقمنا بتقليل الحمل الإضافي للذاكرة للحصول على معلومات الخدمة وتحسين استخدام ذاكرة التخزين المؤقت CPU. يتم الآن البحث عن كتل الذاكرة المناسبة بواسطة الصور النقطية ، ويتم تخصيص ذاكرة الكتل الصغيرة من الصفحات الفردية وتخزينها مؤقتًا عند إصدارها ، وتتم إضافة وظائف متخصصة لأحجام الكتل المستخدمة بشكل متكرر.

العديد من التحسينات الصغيرة


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

  • واجهة برمجة تطبيقات سريعة لتحليل معلمات الوظائف الداخلية وواجهة برمجة تطبيقات جديدة للتكرار عبر HashTable.
  • تعليمات VM جديدة: تسلسل السلسلة والتخصص والتعليمات الفائقة.
  • تم تحويل بعض الوظائف الداخلية إلى تعليمات VM: strlen، is_int.
  • باستخدام سجلات وحدة المعالجة المركزية لسجلات VM: IP و FP.
  • تعظيم الازدواجية وحذف المصفوفات.
  • استخدام عدد مرات حساب الارتباط بدلاً من النسخ أينما يمكنك.
  • PCRE JIT.
  • تحسين الوظائف الداخلية وتسلسلها ().
  • انخفاض حجم الرمز والبيانات المعالجة.

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

ماذا أتيت إلى


هذه هي مساهمة الأنظمة الفرعية المختلفة في WordPress / PHP 7.0.



زادت مساهمة الجهاز الظاهري إلى 50 ٪. Memory Manager 5% — Memory Manager, . 130 . , 10 . , Memory Manager , .


:

  • 2 .
  • MM 17 .
  • - 4 .
  • WordPress 3,5 .

2,5- , . ? , , CPU time, — , . PHP , .

PHP 7


WordPress 3.6 — . - , PHP 7 mysql, , .



, PHPNG. 2/3 . , .

, WordPress, , — 1,5 2- .

PHP 7 HHVM


HHVM.



— . . Facebook . HHVM . , , , , .



PHP 7 — . Vebia, Etsy Badoo. Highload- , .

PHP 7.0 Etsy Badoo -. Badoo .



, 2 , — 7 .

PHP 7.0. , PHP 7.1, .

PHP Russia PHP 8 . PHP, , , — 1 . , , — , , , .

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


All Articles