Python بطيء. لماذا؟

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



أحكام عامة


كيف ترتبط Java من حيث الأداء بـ C أو C ++؟ كيف تقارن C # و Python؟ تعتمد إجابات هذه الأسئلة بشكل كبير على نوع التطبيقات التي حللها الباحث. لا يوجد معيار مثالي ، ولكن دراسة أداء البرامج المكتوبة بلغات مختلفة ، يمكن أن تكون لعبة معايير لغة الكمبيوتر نقطة انطلاق جيدة.

لقد كنت أشير إلى لعبة معايير لغة الكمبيوتر لأكثر من عشر سنوات. Python ، بالمقارنة مع اللغات الأخرى ، مثل Java و C # و Go و JavaScript و C ++ ، هي واحدة من أبطأ . يتضمن هذا اللغات التي تستخدم تجميع JIT (C # ، Java) وتجميع AOT (C # ، C ++) ، بالإضافة إلى اللغات المفسرة مثل JavaScript.

هنا أود أن أشير إلى أنه عندما أقول "Python" ، أعني التنفيذ المرجعي لمترجم Python - CPython. في هذه المادة ، سنتطرق إلى تطبيقاتها الأخرى. في الواقع ، أريد هنا العثور على إجابة لسؤال لماذا تستغرق Python من 2 إلى 10 مرات وقتًا أطول من اللغات الأخرى لحل المشكلات المماثلة ، وما إذا كان يمكن القيام بذلك بشكل أسرع.

إليك بعض النظريات الأساسية التي تحاول تفسير سبب بطء بايثون:

  • والسبب في ذلك هو GIL (قفل المترجم العالمي ، قفل المترجم العالمي).
  • السبب هو أن Python هي لغة مترجمة وليست ترجمة.
  • السبب هو الكتابة الديناميكية.

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

جيل


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

على سبيل المثال ، يحتوي متصفح Chrome الخاص بي ، في الوقت الذي أكتب فيه ، على 44 سلسلة رسائل مفتوحة. يجب أن يوضع في الاعتبار أن بنية و API للنظام للعمل مع التدفقات تختلف في أنظمة التشغيل المستندة إلى Posix (Mac OS ، Linux) ، وفي عائلة Windows لنظام التشغيل. يخطط نظام التشغيل أيضًا لمؤشرات الترابط.

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

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

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

يمكن للمترجم إجراء عملية واحدة فقط في كل مرة ، بغض النظر عن عدد سلاسل الرسائل الموجودة في البرنامج.

affect كيف يؤثر GIL على أداء تطبيقات Python؟


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

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


تصور GIL (مأخوذ من هنا )

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

▍ كيف يتصرف مترجمو Python الآخرون؟


يحتوي PyPy على GIL ، وعادة ما يكون أسرع بأكثر من 3 مرات من CPython.

لا يوجد GIL في Jython ، لأن خيوط Python في Jython يتم تمثيلها كخيوط Java. تستخدم هذه السلاسل إمكانات إدارة الذاكرة في JVM.

ow كيف يتم تنظيم التحكم في التدفق في JavaScript؟


إذا تحدثنا عن جافا سكريبت ، في البداية ، تجدر الإشارة إلى أن جميع محركات JS تستخدم خوارزمية جمع القمامة. كما سبق ذكره ، فإن السبب الرئيسي لاستخدام GIL هو خوارزمية إدارة الذاكرة المستخدمة في CPython.

لا تحتوي جافا سكريبت على GIL ، ومع ذلك ، فإن JS هي لغة مفردة الترابط ، وبالتالي ، فهي لا تحتاج إلى مثل هذه الآلية. بدلاً من تنفيذ التعليمات البرمجية المتوازية ، تستخدم JavaScript تقنيات برمجة غير متزامنة تستند إلى حلقة حدث ووعود واسترجاعات. يحتوي Python على شيء مشابه توفره وحدة asyncio .

Python - لغة مفسرة


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

بالنسبة لنا ، عند التفكير في هذه العملية ، من المهم بشكل خاص هنا ، في مرحلة التجميع ، إنشاء ملف __pycache__/ ، __pycache__/ من __pycache__/ إلى الملف في دليل __pycache__/ ، المستخدم في كل من Python 3 و Python 2.

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

ونتيجة لذلك ، في معظم الأوقات (ما لم تكتب رمزًا يتم تشغيله مرة واحدة فقط) ، ستقوم Python بتنفيذ الرمز البايت النهائي. بمقارنة هذا مع ما يحدث في Java و C # ، اتضح أن شفرة Java يتم تجميعها في "اللغة المتوسطة" ، ويقرأ جهاز Java الظاهري الرمز الفرعي وينفذ تجميع JIT في رمز الجهاز. يستخدم NET CIL "لغة وسيطة" (وهو نفس .NET Runtime، CL-Language-Runtime) ترجمة JIT للانتقال إلى رمز الجهاز.

ونتيجة لذلك ، يتم استخدام بعض "اللغة المتوسطة" في Java و C # وهناك آليات مماثلة موجودة. لماذا ، إذن ، تُظهر Python معايير أسوأ بكثير من Java و C # إذا كانت جميع هذه اللغات تستخدم أجهزة افتراضية ونوعًا من البايت كود؟ بادئ ذي بدء ، يرجع ذلك إلى حقيقة أن تجميع JIT يستخدم في .NET و Java.

يتطلب JIT compilation (Just In Time compilation أو on-the-fly أو just-in-time compilation) لغة متوسطة للسماح بتقسيم الكود إلى أجزاء (إطارات). تم تصميم أنظمة AOT-compilation (Ahead Of Time ، التجميع قبل التنفيذ) بطريقة تضمن الوظائف الكاملة للكود قبل بدء تفاعل هذا الكود مع النظام.

في حد ذاته ، لا يؤدي استخدام JIT إلى تسريع تنفيذ التعليمات البرمجية ، حيث يتم تنفيذ بعض أجزاء رمز البايت ، كما هو الحال في Python. ومع ذلك ، يتيح لك JIT إجراء تحسينات التعليمات البرمجية أثناء التنفيذ. يستطيع مُحسِّن JIT الجيد تحديد الأجزاء الأكثر تحميلًا من التطبيق (يُسمى هذا الجزء من التطبيق "النقطة الفعالة") وتحسين أجزاء الكود المقابلة ، واستبدالها بخيارات محسنة وأكثر إنتاجية من تلك التي تم استخدامها سابقًا.

هذا يعني أنه عندما يقوم تطبيق معين بتنفيذ إجراءات معينة مرارًا وتكرارًا ، يمكن أن يسرع هذا التحسين بشكل كبير من تنفيذ هذه الإجراءات. ضع في اعتبارك أيضًا أن Java و C # هما لغتان مكتوبتان بقوة ، لذا يمكن للمحسن وضع المزيد من الافتراضات حول التعليمات البرمجية التي يمكن أن تساعد في تحسين أداء البرنامج.

يوجد مترجم JIT في PyPy ، وكما سبق ذكره ، فإن تنفيذ مترجم Python هذا أسرع بكثير من CPython. يمكن العثور على معلومات حول مقارنة مترجمين لبايثون مختلفين في هذه المقالة.

▍ لماذا لا يستخدم CPython مترجم JIT؟


لدى جامعي JIT أيضًا عيوب. واحد منهم هو وقت الإطلاق. يبدأ CPython بالفعل ببطء نسبيًا ، و PyPy أبطأ 2-3 مرات من CPython. كما أن وقت تشغيل JVM الطويل حقيقة معروفة. يتغلب CLR .NET على هذه المشكلة من خلال البدء أثناء تمهيد النظام ، ولكن تجدر الإشارة إلى أن كل من CLR ونظام التشغيل الذي يقوم بتشغيل CLR تم تطويرهما من قبل نفس الشركة.

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

ومع ذلك ، CPython هو تنفيذ مترجم Python للأغراض العامة. لذلك ، إذا كنت تقوم بتطوير ، باستخدام Python ، تطبيق سطر الأوامر ، فإن الحاجة إلى انتظار طويل حتى يبدأ مترجم JIT في كل مرة يتم فيها تشغيل هذا التطبيق ستبطئ العمل بشكل كبير.

تحاول CPython تقديم الدعم لأكبر عدد ممكن من حالات استخدام Python. على سبيل المثال ، هناك إمكانية لربط مترجم JIT بـ Python ، ومع ذلك ، فإن المشروع الذي ينفذ هذه الفكرة لا يتطور بنشاط كبير.

نتيجة لذلك ، يمكننا القول أنه إذا كنت تستخدم Python لكتابة برنامج يمكن أن يتحسن أداءه عند استخدام مترجم JIT ، استخدم مترجم PyPy.

Python هي لغة مكتوبة ديناميكيًا


في اللغات المكتوبة بشكل ثابت ، عند تعريف المتغيرات ، يجب عليك تحديد أنواعها. من بين هذه اللغات يمكن ملاحظة C ، C ++ ، Java ، C # ، Go.

في اللغات المكتوبة ديناميكيًا ، يكون لمفهوم نوع البيانات نفس المعنى ، ولكن نوع المتغير ديناميكي.

 a = 1 a = "foo" 

في هذا المثال الأبسط ، تقوم Python أولاً بإنشاء المتغير الأول a ، ثم الثاني الذي يحمل نفس الاسم من النوع str ، ويحرر الذاكرة التي تم تخصيصها للمتغير الأول a .

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

يقوم Python بإجراء مثل هذه التحويلات تلقائيًا ، ولا يرى المبرمج هذه العمليات ، ولا يحتاج إلى الاهتمام بهذه التحويلات.

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

إنها بنية Python التي تجعل التحسين صعبًا للغاية.

لتوضيح هذه الفكرة ، سأستخدم أداة لتتبع مكالمات النظام على نظام MacOS تسمى DTrace.

لا توجد آليات دعم DTrace في توزيع CPython النهائي ، لذا يجب إعادة تجميع CPython بالإعدادات المناسبة. هنا يتم استخدام الإصدار 3.6.6. لذلك ، نستخدم التسلسل التالي من الإجراءات:

 wget https://github.com/python/cpython/archive/v3.6.6.zip unzip v3.6.6.zip cd v3.6.6 ./configure --with-dtrace make 

الآن ، باستخدام python.exe ، يمكنك استخدام DTRace لتتبع التعليمات البرمجية. اقرأ حول استخدام DTrace مع Python هنا . وهنا يمكنك العثور على نصوص برمجية لقياس مؤشرات الأداء المختلفة لبرامج Python باستخدام DTrace. من بينها معلمات وظائف الاتصال ، ووقت تشغيل البرامج ، ووقت استخدام المعالج ، ومعلومات حول مكالمات النظام ، وما إلى ذلك. إليك كيفية استخدام الأمر dtrace :

 sudo dtrace -s toolkit/<tracer>.d -c '../cpython/python.exe script.py' 

وإليك كيف تُظهر py_callflow تتبع py_callflow استدعاءات الوظائف في التطبيق.


تتبع باستخدام DTrace

الآن دعنا نجيب على سؤال ما إذا كانت الكتابة الديناميكية تؤثر على أداء Python. إليك بعض الأفكار حول هذا:

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

الملخص


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

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

أعزائي القراء! كيف تحل مشاكل الأداء السيئة لبايثون؟

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


All Articles