المسار إلى فحص 4 ملايين سطر من كود Python. الجزء 2

ننشر اليوم الجزء الثاني من ترجمة المواد حول كيفية تنظيم Dropbox للتحكم في الكتابة لعدة ملايين من أسطر كود Python.

المسار إلى فحص 4 ملايين سطر من كود Python. الجزء 2

اقرأ الجزء الأول

دعم النوع الرسمي (PEP 484)

لقد أجرينا تجاربنا الجادة الأولى مع mypy في Dropbox خلال Hack Week 2014. Hack Week هو حدث مدته أسبوع واحد تستضيفه Dropbox. خلال هذا الوقت، يمكن للموظفين العمل على ما يريدون! بدأت بعض مشاريع Dropbox التكنولوجية الأكثر شهرة في مثل هذه الأحداث. ونتيجة لهذه التجربة، خلصنا إلى أن mypy يبدو واعدا، على الرغم من أن المشروع ليس جاهزا بعد للاستخدام على نطاق واسع.

في ذلك الوقت، كانت فكرة توحيد أنظمة التلميحات من نوع بايثون مطروحة في الهواء. كما قلت، منذ إصدار Python 3.0، كان من الممكن استخدام التعليقات التوضيحية للنوع للوظائف، لكن هذه كانت مجرد تعبيرات عشوائية، بدون بناء جملة ودلالات محددة. أثناء تنفيذ البرنامج، تم تجاهل هذه التعليقات التوضيحية في أغلب الأحيان. بعد Hack Week، بدأنا العمل على توحيد الدلالات. أدى هذا العمل إلى ظهور بيب 484 (تعاونت أنا وجويدو فان روسوم ولوكاس لانجا في هذه الوثيقة).

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

كان بناء جملة تلميح النوع الذي تم اعتماده في النهاية مشابهًا جدًا لما كان يدعمه mypy في ذلك الوقت. تم إصدار PEP 484 مع Python 3.5 في عام 2015. لم تعد بايثون لغة مكتوبة ديناميكيًا. أحب أن أفكر في هذا الحدث باعتباره علامة فارقة في تاريخ بايثون.

بداية الهجرة

في نهاية عام 2015، أنشأت Dropbox فريقًا من ثلاثة أشخاص للعمل على mypy. وكان من بينهم جويدو فان روسوم وجريج برايس وديفيد فيشر. ومنذ تلك اللحظة بدأ الوضع يتطور بسرعة كبيرة. كان الأداء هو العائق الأول أمام نمو mypy. كما أشرت أعلاه، في الأيام الأولى للمشروع فكرت في ترجمة تنفيذ mypy إلى لغة C، ولكن تم شطب هذه الفكرة من القائمة في الوقت الحالي. لقد كنا عالقين في تشغيل النظام باستخدام مترجم CPython، وهو ليس بالسرعة الكافية لأدوات مثل mypy. (مشروع PyPy، وهو تطبيق بديل لـ Python مع مترجم JIT، لم يساعدنا أيضًا.)

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

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

كانت هذه فترة اعتماد سريع وطبيعي للتحقق من النوع في Dropbox. بحلول نهاية عام 2016، كان لدينا بالفعل ما يقرب من 420000 سطر من كود Python مع التعليقات التوضيحية للنوع. كان العديد من المستخدمين متحمسين للتحقق من النوع. المزيد والمزيد من فرق التطوير كانت تستخدم Dropbox mypy.

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

المزيد من الإنتاجية!

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

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

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

المزيد من الإنتاجية!

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

قررنا العودة إلى إحدى الأفكار السابقة المتعلقة بـ mypy. وهي تحويل كود بايثون إلى كود C. تجربة Cython (النظام الذي يسمح لك بترجمة التعليمات البرمجية المكتوبة بلغة Python إلى كود C) لم تمنحنا أي تسريع واضح، لذلك قررنا إحياء فكرة كتابة مترجمنا الخاص. نظرًا لأن قاعدة بيانات mypy (المكتوبة بلغة Python) تحتوي بالفعل على جميع التعليقات التوضيحية الضرورية للنوع، فقد اعتقدنا أنه سيكون من المفيد محاولة استخدام هذه التعليقات التوضيحية لتسريع النظام. لقد قمت بسرعة بإنشاء نموذج أولي لاختبار هذه الفكرة. لقد أظهر زيادة في الأداء بأكثر من 10 أضعاف على مختلف المعايير الدقيقة. كانت فكرتنا هي تجميع وحدات Python إلى وحدات C باستخدام Cython، وتحويل التعليقات التوضيحية للكتابة إلى اختبارات للنوع أثناء التشغيل (عادةً ما يتم تجاهل التعليقات التوضيحية للكتابة في وقت التشغيل وتستخدم فقط بواسطة أنظمة التحقق من النوع). لقد خططنا بالفعل لترجمة تطبيق mypy من لغة Python إلى لغة تم تصميمها لتتم كتابتها بشكل ثابت، والتي ستبدو (وفي معظمها، تعمل) تمامًا مثل لغة Python. (أصبح هذا النوع من الترحيل عبر اللغات شيئًا من تقليد مشروع mypy. تمت كتابة تطبيق mypy الأصلي بلغة Alore، ثم كان هناك هجين نحوي بين Java وPython).

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

تبين أن المترجم، الذي أطلقنا عليه اسم mypyc (نظرًا لأنه يستخدم mypy كواجهة أمامية لتحليل الأنواع)، كان مشروعًا ناجحًا للغاية. بشكل عام، حققنا تسريعًا يصل إلى 4x تقريبًا لعمليات تشغيل mypy المتكررة دون تخزين مؤقت. استغرق تطوير جوهر مشروع mypyc فريقًا صغيرًا من مايكل سوليفان وإيفان ليفكيفسكي وهيو هان وأنا حوالي 4 أشهر تقويمية. كان هذا القدر من العمل أصغر بكثير مما هو مطلوب لإعادة كتابة mypy، على سبيل المثال، في C++ أو Go. وكان علينا إجراء تغييرات أقل بكثير على المشروع مما كان علينا إجراؤه عند إعادة كتابته بلغة أخرى. كنا نأمل أيضًا أن نتمكن من رفع mypyc إلى مستوى يمكن لمبرمجي Dropbox الآخرين استخدامه لتجميع التعليمات البرمجية الخاصة بهم وتسريعها.

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

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

يتبع ...

القراء الأعزاء! ما هي انطباعاتك عن مشروع mypy عندما علمت بوجوده؟

المسار إلى فحص 4 ملايين سطر من كود Python. الجزء 2
المسار إلى فحص 4 ملايين سطر من كود Python. الجزء 2

المصدر: www.habr.com

إضافة تعليق