تطوير برامج بيثون سريعة للغاية

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



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

قياس الوقت والتنميط


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

يوجد أدناه رمز البرنامج ، والذي سأستخدمه لأغراض العرض التوضيحي. مأخوذة من وثائق بيثون. هذا الكود يثير e إلى قوة x :

 # slow_program.py from decimal import * def exp(x):    getcontext().prec += 2    i, lasts, s, fact, num = 0, 0, 1, 1, 1    while s != lasts:        lasts = s        i += 1        fact *= i        num *= x        s += num / fact    getcontext().prec -= 2    return +s exp(Decimal(150)) exp(Decimal(400)) exp(Decimal(3000)) 

أسهل طريقة "لتعريف" الكود


للبدء ، فكر في أبسط طريقة لتعريف الرمز الخاص بك. لذلك أقول ، "التنميط للكسول". يتكون في استخدام أمر time يونكس:

 ~ $ time python3.8 slow_program.py real 0m11,058s user 0m11,050s sys 0m0,008s 

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

الطريقة الأكثر دقة التنميط


في الطرف الآخر من طيف أساليب وضع التعليمات البرمجية ، تكمن أداة cProfile ، والتي توفر للمبرمج ، باعتراف الجميع ، الكثير من المعلومات:

 ~ $ python3.8 -m cProfile -s time slow_program.py         1297 function calls (1272 primitive calls) in 11.081 seconds   Ordered by: internal time   ncalls tottime percall cumtime percall filename:lineno(function)        3  11.079  3.693 11.079  3.693 slow_program.py:4(exp)        1  0.000  0.000 0.002  0.002 {built-in method _imp.create_dynamic}      4/1  0.000  0.000 11.081  11.081 {built-in method builtins.exec}        6  0.000  0.000 0.000  0.000 {built-in method __new__ of type object at 0x9d12c0}        6  0.000  0.000 0.000  0.000 abc.py:132(__new__)       23  0.000  0.000 0.000  0.000 _weakrefset.py:36(__init__)      245  0.000  0.000 0.000  0.000 {built-in method builtins.getattr}        2  0.000  0.000 0.000  0.000 {built-in method marshal.loads}       10  0.000  0.000 0.000  0.000 <frozen importlib._bootstrap_external>:1233(find_spec)      8/4  0.000  0.000 0.000  0.000 abc.py:196(__subclasscheck__)       15  0.000  0.000 0.000  0.000 {built-in method posix.stat}        6  0.000  0.000 0.000  0.000 {built-in method builtins.__build_class__}        1  0.000  0.000 0.000  0.000 __init__.py:357(namedtuple)       48  0.000  0.000 0.000  0.000 <frozen importlib._bootstrap_external>:57(_path_join)       48  0.000  0.000 0.000  0.000 <frozen importlib._bootstrap_external>:59(<listcomp>)        1  0.000  0.000 11.081  11.081 slow_program.py:1(<module>) 

هنا نقوم بتشغيل البرنامج النصي الذي تم cProfile باستخدام وحدة cProfile ونستخدم وسيطة time . نتيجة لذلك ، يتم فرز خطوط الإخراج حسب الوقت الداخلي (وقت cumtime ). إنه يعطينا الكثير من المعلومات. في الواقع ، ما هو موضح أعلاه هو فقط حوالي 10 ٪ من الناتج من cProfile .

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

دراسة مؤشرات الأداء المؤقتة لوظيفة محددة


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

 def timeit_wrapper(func):    @wraps(func)    def wrapper(*args, **kwargs):        start = time.perf_counter() #       time.process_time()        func_return_val = func(*args, **kwargs)        end = time.perf_counter()        print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__, end - start))        return func_return_val    return wrapper 

يمكن تطبيق هذا الديكور على الوظيفة المراد استكشافها:

 @timeit_wrapper def exp(x):    ... print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time')) exp(Decimal(150)) exp(Decimal(400)) exp(Decimal(3000)) 

الآن بعد بدء البرنامج ، سنتلقى المعلومات التالية:

 ~ $ python3.8 slow_program.py module   function time __main__ .exp : 0.003267502994276583 __main__ .exp : 0.038535295985639095 __main__ .exp : 11.728486061969306 

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

رمز التسارع


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

▍ استخدم أنواع البيانات المدمجة


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

ly تطبيق التخزين المؤقت (المذكرة) مع lru_cache


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

 import functools import time #   12   @functools.lru_cache(maxsize=12) def slow_func(x):    time.sleep(2) #       return x slow_func(1) # ...  2     slow_func(1) #    -   ! slow_func(3) # ...   2     

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

▍ استخدام المتغيرات المحلية


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

يمكنك تحسين الأداء باستخدام الطرق التالية لتعيين القيم التي قد تبدو غير ضرورية تمامًا وغير مجدية لشخص غير مطلع:

 #  #1 class FastClass:    def do_stuff(self):        temp = self.value #           for i in range(10000):            ... #      `temp` #  #2 import random def fast_function():    r = random.random    for i in range(10000):        print(r()) #   `r()` ,     random.random() 

code التفاف رمز في وظيفة


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

 def main():    ... #  ,     main() 

not لا تقم بالوصول إلى السمات


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

 # : import re def slow_func():    for i in range(10000):        re.findall(regex, line) # ! # : from re import findall def fast_func():    for i in range(10000):        findall(regex, line) # ! 

ew احذر من الأوتار


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

 f'{s} {t}' # ! s + ' ' + t ' '.join((s, t)) '%s %s' % (s, t) '{} {}'.format(s, t) Template('$s $t').substitute(s=s, t=t) # ! 

that تعرف أن المولدات تعمل بسرعة كبيرة


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

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

النتائج


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

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

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


All Articles