Cinder هو إصدار الإنتاج الداخلي الموجه نحو الأداء من Meta لـ CPython 3.10. يحتوي على عدد من تحسينات الأداء، بما في ذلك التخزين المؤقت المضمن للرمز الثانوي، والتقييم المتلهف لـ coroutines، وطريقة JIT في كل مرة، ومترجم تجريبي للرمز الثانوي الذي يستخدم التعليقات التوضيحية للنوع لإصدار رمز ثانوي متخصص من النوع الذي يعمل بشكل أفضل في JIT.
تعمل Cinder على تشغيل Instagram، حيث بدأت، ويتم استخدامها بشكل متزايد عبر المزيد والمزيد من تطبيقات Python في Meta.
لمزيد من المعلومات حول CPython، راجع README.cpython.rst
.
إجابة مختصرة: لا.
لقد جعلنا Cinder متاحًا للعامة لتسهيل المحادثة حول إمكانية نقل بعض هذا العمل إلى CPython ولتقليل ازدواجية الجهود بين الأشخاص الذين يعملون على أداء CPython.
لم يتم صقل أو توثيق Cinder لاستخدام أي شخص آخر. ليس لدينا الرغبة في أن يصبح بديلاً لـ CPython. هدفنا من إتاحة هذا الرمز هو CPython موحد وأسرع. لذلك بينما نقوم بتشغيل Cinder في الإنتاج، إذا اخترت القيام بذلك فأنت وحدك. لا يمكننا الالتزام بإصلاح تقارير الأخطاء الخارجية أو مراجعة طلبات السحب. نحن نتأكد من أن Cinder مستقر وسريع بما فيه الكفاية لأعباء عمل الإنتاج لدينا، ولكننا لا نقدم أي ضمانات بشأن استقراره أو صحته أو أدائه لأي أعباء عمل أو حالات استخدام خارجية.
ومع ذلك، إذا كانت لديك خبرة في أوقات تشغيل اللغة الديناميكية ولديك أفكار لجعل Cinder أسرع؛ أو إذا كنت تعمل على CPython وترغب في استخدام Cinder كمصدر إلهام للتحسينات في CPython (أو مساعدة الأجزاء الأولية من Cinder إلى CPython)، فيرجى التواصل معنا؛ نحن نحب الدردشة!
يجب أن يتم إنشاء Cinder تمامًا مثل CPython؛ configure
make -j
. ومع ذلك، نظرًا لأن معظم تطوير واستخدام Cinder يحدث في سياق Meta محدد للغاية، فإننا لا نمارسه كثيرًا في بيئات أخرى. على هذا النحو، فإن الطريقة الأكثر موثوقية لإنشاء وتشغيل Cinder هي إعادة استخدام الإعداد المستند إلى Docker من سير عمل GitHub CI الخاص بنا.
إذا كنت ترغب فقط في الحصول على Cinder يعمل دون إنشائه بنفسك، فإن صورة Runtime Docker Image الخاصة بنا ستكون الأسهل (لا حاجة إلى استنساخ الريبو!):
docker run -it --rm ghcr.io/facebookincubator/cinder-runtime:cinder-3.10
إذا كنت ترغب في بنائه بنفسك:
git clone https://github.com/facebookincubator/cinder
docker run -v "$PWD/cinder:/vol" -w /vol -it --rm ghcr.io/facebookincubator/cinder/python-build-env:latest bash
./configure && make
يرجى العلم أن Cinder تم تصميمه أو اختباره فقط على Linux x64؛ ربما لن يعمل أي شيء آخر (بما في ذلك macOS). صورة Docker أعلاه مستندة إلى Fedora Linux ومبنية من ملف مواصفات Docker في Cinder repo: .github/workflows/python-build-env/Dockerfile
.
هناك بعض أهداف الاختبار الجديدة التي قد تكون مثيرة للاهتمام. make testcinder
يشبه إلى حد كبير make test
باستثناء أنه يتخطى بعض الاختبارات التي تمثل مشكلة في بيئة التطوير لدينا. يقوم make testcinder_jit
بتشغيل مجموعة الاختبار مع تمكين JIT بالكامل، لذلك يتم تشغيل جميع الوظائف JIT. يقوم make testruntime
بتشغيل مجموعة من اختبارات وحدة C++ gtest لـ JIT. make test_strict_module
يقوم بتشغيل مجموعة اختبار للوحدات الصارمة (انظر أدناه).
لاحظ أن هذه الخطوات تنتج ثنائي Cinder Python دون تمكين تحسينات PGO/LTO، لذلك لا تتوقع استخدام هذه التعليمات للحصول على أي تسريع لأي حمل عمل لـ Python.
Cinder Explorer عبارة عن ساحة لعب مباشرة، حيث يمكنك أن ترى كيف يقوم Cinder بتجميع كود Python من المصدر إلى التجميع - فنحن نرحب بك لتجربته! لا تتردد في تقديم طلبات الميزات وتقارير الأخطاء. ضع في اعتبارك أن Cinder Explorer، مثل بقية هذا، "مدعوم" على أساس أفضل جهد.
يستخدم Instagram بنية خادم الويب متعددة العمليات؛ تبدأ العملية الأصلية، وتنفذ أعمال التهيئة (على سبيل المثال، تحميل التعليمات البرمجية)، وتفرع عشرات العمليات المنفذة للتعامل مع طلبات العميل. تتم إعادة تشغيل العمليات المنفذة بشكل دوري لعدد من الأسباب (على سبيل المثال، تسرب الذاكرة، وعمليات نشر التعليمات البرمجية) ولها عمر قصير نسبيًا. في هذا النموذج، يجب على نظام التشغيل نسخ الصفحة بأكملها التي تحتوي على كائن تم تخصيصه في العملية الأصلية عند تعديل العدد المرجعي للكائن. من الناحية العملية، فإن الأشياء المخصصة في العملية الأم تبقى أطول من عمر العمال؛ جميع الأعمال المتعلقة بمرجع عدها غير ضرورية.
يحتوي Instagram على قاعدة تعليمات برمجية كبيرة جدًا لـ Python، وتبين أن النفقات العامة الناتجة عن النسخ عند الكتابة من مرجع عد الكائنات طويلة العمر كانت كبيرة. لقد قمنا بتطوير حل يسمى "المثيلات الخالدة" لتوفير طريقة لإلغاء الاشتراك في الكائنات من العد المرجعي. راجع تضمين/object.h للحصول على التفاصيل. يتم التحكم في هذه الميزة عن طريق تعريف Py_IMMORTAL_INSTANCES ويتم تمكينها افتراضيًا في Cinder. لقد كان هذا فوزًا كبيرًا لنا في الإنتاج (حوالي 5%)، ولكنه يجعل كود الخط المستقيم أبطأ. تحدث عمليات عد المرجع بشكل متكرر ويجب التحقق مما إذا كان الكائن يشارك في عد المرجع أم لا عند تمكين هذه الميزة.
"Shadowcode" أو "shadow bytecode" هو تطبيقنا لمترجم متخصص. ويلاحظ حالات معينة قابلة للتحسين في تنفيذ أكواد تشغيل Python العامة و(للوظائف الساخنة) يستبدل أكواد التشغيل هذه ديناميكيًا بإصدارات متخصصة. يوجد جوهر Shadowcode في Shadowcode/shadowcode.c
، على الرغم من أن تطبيقات الرموز الثانوية المتخصصة موجودة في Python/ceval.c
مع بقية حلقة التقييم. توجد الاختبارات الخاصة بـ Shadowcode في Lib/test/test_shadowcode.py
.
إنه مشابه في الروح للمترجم التكيفي المتخصص (PEP-659) الذي سيتم دمجه في CPython 3.11.
يعد Instagram Server بمثابة عبء عمل غير متزامن ثقيل، حيث قد يؤدي كل طلب ويب إلى تشغيل مئات الآلاف من المهام غير المتزامنة، والتي يمكن إكمال العديد منها دون تعليق (على سبيل المثال، بفضل القيم المحفوظة في الذاكرة).
لقد قمنا بتوسيع بروتوكول Vectorcall لتمرير علامة جديدة، Ci_Py_AWAITED_CALL_MARKER
، للإشارة إلى أن المتصل ينتظر هذه المكالمة على الفور.
عند استخدامها مع استدعاءات الوظائف غير المتزامنة التي يتم انتظارها على الفور، يمكننا على الفور (بشغف) تقييم الوظيفة المطلوبة، حتى الاكتمال، أو حتى تعليقها الأول. إذا اكتملت الدالة دون تعليقها، فيمكننا إرجاع القيمة على الفور، دون الحاجة إلى تخصيص كومة إضافية.
عند استخدامها مع التجميع غير المتزامن، يمكننا على الفور (بشغف) تقييم مجموعة المهام المتوقعة التي تم تمريرها، مما قد يؤدي إلى تجنب تكلفة إنشاء وجدولة مهام متعددة لـ coroutines التي يمكن إكمالها بشكل متزامن، والعقود الآجلة المكتملة، والقيم المحفوظة، وما إلى ذلك.
أدت هذه التحسينات إلى تحسين كبير في كفاءة وحدة المعالجة المركزية (~5%).
يتم تنفيذ هذا غالبًا في Python/ceval.c
، عبر علامة اتصال متجه جديدة Ci_Py_AWAITED_CALL_MARKER
، تشير إلى أن المتصل ينتظر هذه المكالمة على الفور. ابحث عن استخدامات الماكرو IS_AWAITED()
وعلامة Vectorcall هذه.
Cinder JIT عبارة عن JIT مخصص يتم تنفيذه في C++ في كل مرة. يتم تمكينه عبر علامة -X jit
أو متغير البيئة PYTHONJIT=1
. وهو يدعم جميع أكواد تشغيل Python تقريبًا، ويمكنه تحقيق تحسينات في السرعة بمعدل 1.5 إلى 4x في العديد من معايير أداء Python.
افتراضيًا، عند تمكينه، سيقوم JIT بتجميع كل وظيفة تم استدعاؤها على الإطلاق، مما قد يجعل برنامجك أبطأ، وليس أسرع، بسبب الحمل الزائد للوظائف التي نادرًا ما يتم استدعاؤها بواسطة JIT. يمكن للخيار -X jit-list-file=/path/to/jitlist.txt
أو PYTHONJITLISTFILE=/path/to/jitlist.txt
توجيهه إلى ملف نصي يحتوي على أسماء الوظائف المؤهلة بالكامل (في النموذج path.to.module:funcname
أو path.to.module:ClassName.method_name
)، واحد في كل سطر، والذي يجب أن يتم تجميعه بواسطة JIT. نحن نستخدم هذا الخيار لتجميع مجموعة فقط من الوظائف الساخنة المستمدة من بيانات ملفات تعريف الإنتاج. (الأسلوب الأكثر شيوعًا لـ JIT هو تجميع الوظائف ديناميكيًا كما يُلاحظ أنه يتم استدعاؤها بشكل متكرر. لم يكن الأمر يستحق ذلك حتى الآن بالنسبة لنا لتنفيذ ذلك، نظرًا لأن بنية الإنتاج لدينا هي خادم ويب ما قبل الانقسام، ومن أجل أسباب مشاركة الذاكرة هي أننا نرغب في القيام بكل عمليات تجميع JIT الخاصة بنا مقدمًا في العملية الأولية قبل أن يتم تفرع العمال، مما يعني أننا لا نستطيع ملاحظة عبء العمل أثناء العملية قبل تحديد الوظائف التي سيتم تجميعها في JIT.)
يوجد JIT في الدليل Jit/
، واختبارات C++ الخاصة به موجودة في RuntimeTests/
(قم بتشغيلها باستخدام make testruntime
). توجد أيضًا بعض اختبارات بايثون لها في Lib/test/test_cinderjit.py
؛ ليس المقصود منها أن تكون شاملة، نظرًا لأننا نقوم بتشغيل مجموعة اختبار CPython بأكملها ضمن JIT عبر make testcinder_jit
؛ إنها تغطي حالات JIT edge التي لم يتم العثور عليها في مجموعة اختبار CPython.
راجع Jit/pyjit.cpp
للتعرف على بعض خيارات -X
ومتغيرات البيئة الأخرى التي تؤثر على سلوك JIT. توجد أيضًا وحدة cinderjit
محددة في هذا الملف والتي تعرض بعض أدوات JIT المساعدة لكود Python (على سبيل المثال، إجبار وظيفة معينة على الترجمة، والتحقق من تجميع الوظيفة، وتعطيل JIT). لاحظ أن cinderjit.disable()
يعطل الترجمة المستقبلية فقط؛ يقوم على الفور بتجميع جميع الوظائف المعروفة ويحتفظ بالوظائف المترجمة من JIT.
يقوم JIT أولاً بخفض كود Python الثانوي إلى تمثيل متوسط عالي المستوى (HIR)؛ يتم تنفيذ هذا في Jit/hir/
. تقوم HIR بتعيين خرائط قريبة بشكل معقول من Python bytecode، على الرغم من أنها آلة تسجيل بدلاً من آلة مكدسة، إلا أنها ذات مستوى أقل قليلاً، ويتم كتابتها، وبعض التفاصيل التي يحجبها Python bytecode ولكنها مهمة للأداء (لا سيما العد المرجعي) هي يتعرض صراحة في HIR. يتم تحويل HIR إلى نموذج SSA، ويتم تنفيذ بعض تمريرات التحسين عليه، ثم يتم إدراج عمليات العد المرجعي فيه تلقائيًا وفقًا للبيانات الوصفية حول إعادة العد وتأثيرات الذاكرة الخاصة بكودات تشغيل HIR.
يتم بعد ذلك تخفيض HIR إلى تمثيل متوسط منخفض المستوى (LIR)، وهو عبارة عن تجريد للتجميع، يتم تنفيذه في Jit/lir/
. في LIR نقوم بتسجيل التخصيص، وتمرير بعض التحسينات الإضافية، ثم أخيرًا يتم تخفيض LIR إلى التجميع (في Jit/codegen/
) باستخدام مكتبة asmjit الممتازة.
فريق JIT في مراحله الأولى. في حين أنه يمكنه بالفعل التخلص من الحمل الزائد لحلقة المترجم الفوري ويقدم تحسينات كبيرة في الأداء للعديد من الوظائف، فقد بدأنا فقط في خدش سطح التحسينات الممكنة. لم يتم بعد تنفيذ العديد من تحسينات المترجم الشائعة. إن تحديد أولوياتنا للتحسينات يعتمد إلى حد كبير على خصائص عبء عمل إنتاج Instagram.
الوحدات الصارمة عبارة عن بضعة أشياء مدمجة في شيء واحد:
1. لن يكون للمحلل الثابت القادر على التحقق من صحة تنفيذ كود المستوى الأعلى للوحدة آثار جانبية مرئية خارج تلك الوحدة.
2. نوع StrictModule
غير قابل للتغيير يمكن استخدامه بدلاً من نوع الوحدة الافتراضية في Python.
3. مُحمل وحدة Python قادر على التعرف على الوحدات التي تم اختيارها في الوضع الصارم (عبر import __strict__
في الجزء العلوي من الوحدة)، وتحليلها للتحقق من عدم وجود آثار جانبية للاستيراد، ونشرها في sys.modules
ككائن StrictModule
.
Static Python هو مترجم كود بايت يستخدم التعليقات التوضيحية للنوع لإصدار رمز بايت بايثون المخصص للنوع والمحدد من النوع. عند استخدامه مع Cinder JIT، يمكنه تقديم أداء مشابه لـ MyPyC أو Cython في كثير من الحالات، مع تقديم تجربة مطور Python خالصة (بناء جملة Python العادي، بدون خطوة تجميع إضافية). يحقق Static Python plus Cinder JIT أداءً يعادل 18 ضعف أداء CPython الخاص بالأوراق المالية على نسخة مكتوبة من معيار Richards. في Instagram، نجحنا في استخدام Static Python في الإنتاج لاستبدال جميع وحدات Cython في قاعدة بيانات خادم الويب الأساسية لدينا، دون أي تراجع في الأداء.
تم إنشاء مترجم Static Python أعلى وحدة compiler
Python التي تمت إزالتها من المكتبة القياسية في Python 3 وتمت صيانتها وتحديثها خارجيًا منذ ذلك الحين؛ تم دمج هذا المترجم في Cinder في Lib/compiler
. يتم تنفيذ مترجم Static Python في Lib/compiler/static/
، واختباراته موجودة في Lib/test/test_compiler/test_static.py
.
تُعطى الفئات المحددة في وحدات Static Python تلقائيًا فتحات مكتوبة (استنادًا إلى فحص سمات الفئة المكتوبة والتخصيصات المشروحة في __init__
)، وتستخدم تحميلات السمات وتخزينها مقابل مثيلات هذه الأنواع أكواد التشغيل STORE_FIELD
و LOAD_FIELD
الجديدة، والتي تصبح مباشرة في JIT يقوم بتحميل/تخزين من/إلى إزاحة ذاكرة ثابتة في الكائن، مع عدم وجود أي اتجاه غير مباشر لـ LOAD_ATTR
أو STORE_ATTR
. تحصل الفئات أيضًا على جداول vtables لأساليبها، لاستخدامها بواسطة أكواد التشغيل INVOKE_*
المذكورة أدناه. يتوفر دعم وقت التشغيل لهذه الميزات في StaticPython/classloader.h
و StaticPython/classloader.c
.
تبدأ دالة Python الثابتة بمقدمة مخفية تتحقق من أن أنواع الوسائط المتوفرة تطابق التعليقات التوضيحية للنوع، وتثير TypeError
إذا لم يكن الأمر كذلك. ستتخطى الاستدعاءات من دالة Python الثابتة إلى دالة Python ثابتة أخرى كود التشغيل هذا (نظرًا لأن المترجم قد تم التحقق من صحة الأنواع بالفعل). يمكن للاستدعاءات الثابتة إلى الثابتة أيضًا تجنب الكثير من الحمل الزائد لاستدعاء دالة Python النموذجية. نقوم بإصدار رمز التشغيل INVOKE_FUNCTION
أو INVOKE_METHOD
الذي يحمل معه بيانات وصفية حول الوظيفة أو الطريقة المطلوبة؛ هذا بالإضافة إلى الوحدات النمطية غير القابلة للتغيير اختياريًا (عبر StrictModule
) والأنواع (عبر cinder.freeze_type()
، والتي نطبقها حاليًا على جميع الأنواع في الوحدات الصارمة والثابتة في محمل الاستيراد الخاص بنا، ولكن في المستقبل قد تصبح جزءًا متأصلًا من Static Python) وتجميعها تتيح لنا معرفة الوقت بتوقيع المستدعى (في JIT) تحويل العديد من استدعاءات وظائف Python إلى استدعاءات مباشرة إلى عنوان ذاكرة ثابت باستخدام اصطلاح الاتصال x64، مع حمل أكبر قليلاً من استدعاء دالة C.
لا تزال لغة Python الثابتة تُكتب تدريجيًا، وتدعم التعليمات البرمجية التي تم شرحها جزئيًا فقط أو تستخدم أنواعًا غير معروفة من خلال الرجوع إلى سلوك Python الديناميكي العادي. في بعض الحالات (على سبيل المثال، عند إرجاع قيمة من نوع غير معروف بشكل ثابت من دالة مع تعليق توضيحي للإرجاع)، يتم إدراج كود تشغيل CAST
لوقت التشغيل والذي سيؤدي إلى ظهور TypeError
إذا كان نوع وقت التشغيل لا يتطابق مع النوع المتوقع.
تدعم Static Python أيضًا أنواعًا جديدة من الأعداد الصحيحة للآلة، والمنطقيات، والمضاعفات، والمتجهات/المصفوفات. في JIT يتم التعامل معها كقيم غير معلبة، وعلى سبيل المثال، يتجنب حساب الأعداد الصحيحة البدائية كل النفقات العامة لـ Python. تم أيضًا تحسين بعض العمليات على الأنواع المضمنة (مثل القائمة أو القاموس المنخفض أو len()
) .
يدعم Cinder الاعتماد التدريجي للوحدات الثابتة عبر مُحمل الوحدة الصارم/الثابت الذي يمكنه اكتشاف الوحدات الثابتة تلقائيًا وتحميلها على أنها ثابتة من خلال التجميع عبر الوحدات. سيبحث المُحمل عن import __static__
والتعليقات التوضيحية import __strict__
في الجزء العلوي من الملف، ويقوم بتجميع الوحدات بشكل مناسب. لتمكين أداة التحميل، لديك أحد الخيارات الثلاثة:
1. قم بتثبيت المُحمل بشكل صريح في المستوى الأعلى لتطبيقك عبر from cinderx.compiler.strict.loader import install; install()
.
PYTHONINSTALLSTRICTLOADER=1
في البيئة الخاصة بك../python -X install-strict-loader application.py
. بدلًا من ذلك، يمكنك تجميع كافة التعليمات البرمجية بشكل ثابت باستخدام ./python -m compiler --static some_module.py
، والذي سيقوم بتجميع الوحدة على أنها Python ثابتة وتنفيذها.
راجع CinderDoc/static_python.rst
لمزيد من الوثائق التفصيلية.