عندما يسألني أحدهم عن كيفية تعاملي مع بطء بايثون، يكون ردي المعتاد هو أن بايثون هي أسرع لغة برمجة أعرفها كتابةً، وأنظفها، وأسهلها صيانةً، وأن أي انخفاض طفيف في الأداء وقت التشغيل يُعدّ ثمنًا زهيدًا مقابل مكاسب إنتاجية ملحوظة. نادرًا ما أشعر أن بايثون تُبطئني، ومن ناحية أخرى، أُعجب دائمًا بسرعة كتابتي للبرامج بها مقارنةً باللغات الأخرى.
معايير أداء بايثون
مع كل إصدار جديد من بايثون، نسمع عن تحسينات جديدة في الأداء، مما يوحي بأن اللغة تزداد سرعةً شيئًا فشيئًا. ولقياس ذلك، كتبتُ بعض الاختبارات التي تستهلك الكثير من وحدة المعالجة المركزية (CPU) وأجريتُها على العديد من اللغات والإصدارات.
- CPython 2.7 وجميع الإصدارات بين 3.8 و3.13 الحالي
- أحدث إصدار من PyPy، والذي يتوافق مع CPython 3.10 في الوقت الذي أقوم فيه بهذا (بالنسبة لأولئك الذين لم يسمعوا أبدًا عن PyPy، فهذا تنفيذ بديل للغة Python المُحسّنة للسرعة)
- أحدث إصدار من Node.js
- أحدث إصدار من Rust
قبل أن تسأل، لا، لم أُضمِّن روبي أو PHP أو أي لغة برمجة أخرى بطيئة في هذا الاختبار، ببساطة لأنني لا أستخدم أيًا منها حاليًا، ولن أبدأ باستخدامها حتى لو أظهرت نتائج ممتازة في الاختبار. كما أنني لم أُعرِ اهتمامًا لمحركات جافا سكريبت الأخرى سوى Node.js.
بالنسبة لأولئك المهتمين بمعرفة كيفية تشغيل هذا المعيار على لغات أخرى لا أهتم بها، في نهاية هذه المقالة سأشارك جميع التعليمات البرمجية والبرامج النصية لتمكينك من تشغيل كل شيء.
أرقام فيبوناتشي
مثالي الأول هو نص برمجي صغير يحسب جميع أرقام فيبوناتشي المُعطاة كوسيطات سطر أوامر. إليك نسخة بايثون:
import sys
def fibo(n):
if n <= 1:
return n
else:
return fibo(n-1) + fibo(n-2)
if __name__ == '__main__':
for n in sys.argv[1:]:
print(n, fibo(int(n)))
يوضح المثال التالي كيفية تشغيل البرنامج النصي أعلاه لحساب الأرقام 10 و20 و30 و40 في متتالية فيبوناتشي:
$ python fibo.py 10 20 30 40
10 55
20 6765
30 832040
40 102334155
لكل لغة متنافسة، شغّلتُ البرنامج النصي بالوسيطات الموضحة أعلاه. كرّرتُ كل اختبار ثلاث مرات، وأخذتُ متوسط الوقت بالثواني. أجريتُ جميع الاختبارات على نفس النظام، وهو حاسوب محمول بمعالج Intel Core i5 يعمل بنظام Ubuntu. إليكم نتائج هذه التجربة:

اللغة والإصدار | الوقت (بالثواني) | Speedup مقابل CPython 3.8 | Speedup مقابل CPython 3.13 |
---|---|---|---|
CPython 2.7 | 16.52 | 1.22x | 0.6x |
CPython 3.8 | 20.10 | 1.0x | 0.5x |
CPython 3.9 | 19.87 | 1.0x | 0.5x |
CPython 3.10 | 22.07 | 0.9x | 0.4x |
CPython 3.11 | 10.57 | 1.9x | 0.9x |
CPython 3.12 | 9.41 | 2.1x | 1.0x |
CPython 3.13 | 9.72 | 2.1x | 1.0x |
PyPy-3.10 | 1.65 | 12.5x | 6.1x |
Node.js 22 | 1.76 | 11.4x | 5.5x |
Rust-1.81 | 0.25 | 80.4x | 38.9x |
الآن نعلم أن Rust يمكن أن يكون أسرع بـ 80 مرة من Python 3.8!
لاحظ أنني أقول “يمكن” لأن هذا هو فرق السرعة الذي لاحظته في هذا الاختبار. هناك العديد من العوامل التي تؤثر على الأداء، لذا من غير المرجح أن يكون هذا الفرق ثابتًا في جميع أمثلة التعليمات البرمجية. سأتطرق إلى هذا لاحقًا.
قبل إجراء هذا الاختبار، لم أكن أعلم مدى سرعة Rust مقارنةً بـ Python، لكنني كنت أعلم أنه لا بد من وجود فرق كبير، لذا لا أستطيع القول إنني مندهش من هذا الفرق البالغ 80 ضعفًا. لكن بعض النتائج الأخرى في منتصف الجدول ساعدتني في استخلاص بعض الاستنتاجات المثيرة للاهتمام التي لم تكن واضحة لي من قبل:
- بالنظر إلى CPython، حقق الإصدار 3.11 قفزة نوعية في الأداء. لاحظ كيف كان الأداء ثابتًا، بل ومتناقصًا، بين الإصدارين 3.8 و3.10، ثم تضاعف في الإصدار 3.11. هذا هو أول إصدار يتفوق على الإصدار 2.7 العريق في الأداء.
- أحدث إصدار من PyPy أسرع من الإصدار 3.8 بأكثر من 12 مرة، أو أسرع بست مرات من الإصدار 3.13. بل إنه أسرع قليلاً من إصدار Node.js لهذا الاختبار!
الآن حان الوقت لتجربة مثال آخر، لمعرفة ما إذا كانت مجموعة جديدة من الأرقام تحكي نفس القصة أو قصة مختلفة.
فرز الفقاعات
لاختباري الثاني، كتبتُ خوارزمية فرز الفقاعات. إليكم نسخة بايثون:
import random
import sys
def bubble_sort(arr):
n = len(arr)
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
if __name__ == '__main__':
n = int(sys.argv[1])
arr = [random.randint(0, n - 1) for _ in range(n)]
bubble_sort(arr)

اللغة والإصدار | الوقت (بالثواني) | Speedup vs. CPython 3.8 | Speedup vs. CPython 3.13 |
---|---|---|---|
CPython 2.7 | 3.26 | 1.7x | 1.1x |
CPython 3.8 | 5.53 | 1.0x | 0.7x |
CPython 3.9 | 5.65 | 0.9x | 0.7x |
CPython 3.10 | 5.16 | 1.1x | 0.7x |
CPython 3.11 | 2.94 | 1.9x | 1.3x |
CPython 3.12 | 3.72 | 1.5x | 1.0x |
CPython 3.13 | 3.70 | 1.5x | 1.0x |
PyPy-3.10 | 0.21 | 26.3x | 17.6x |
Node.js 22 | 0.11 | 50.3x | 33.7x |
Rust-1.81 | 0.06 | 92.2x | 61.7x |
بشكل عام، لا يوجد فرق كبير في نتائج هذا الاختبار. كان الإصدار 3.10 أبطأ قليلاً من الإصدارين 3.8 و3.9 في الاختبار الأول، بينما كان أسرع قليلاً في هذا الاختبار. ونظرًا لضآلة هذه الفروقات في كلا الاختبارين، أعتقد أنه من المنطقي اعتبار إصدارات بايثون 3.8 و3.9 و3.10 ضمن فئة أداء متشابهة.
في الاختبار الأول، كان الإصداران 3.12 و3.13 أسرع بقليل من 3.11، ولكن في هذا الاختبار الجديد، انعكست النتائج، حيث كان 3.11 هو الأسرع. هذا ليس غريبًا بالنسبة لي، فالاختلافات بين الاختبارات متوقعة لأن الأداء ليس ثابتًا ويعتمد بشكل كبير على الكود والتحسينات الموجودة في كل إصدار من اللغة. لهذا السبب، أرى أيضًا أن الإصدارات 3.11 و3.12 و3.13 مجموعة ذات خصائص أداء متشابهة.
في هذا الاختبار يبدو أن أداء 3.11 و3.12 و3.13 مشابه لأداء 2.7، وليس أسرع كما في الاختبار السابق، لذا باستخدام هاتين النقطتين من البيانات يمكننا القول أن 3.11 هو أول إصدار يطابق أو يتجاوز سرعة الإصدار 2.7.
ماذا عن PyPy؟ في هذا الاختبار، كان أبطأ من إصدار Node.js، وإذا كنت تتذكر، كان أسرع قليلاً قبل ذلك. لكن بشكل عام، لا يزال PyPy يعمل بسرعات أقرب بكثير إلى Node منه إلى CPython. أسرع بحوالي 18 مرة من Python 3.13، وهو أمر مذهل.
أخيرًا، لا يزال Rust رائدًا كما هو متوقع، على الرغم من أنني لم أبذل أي جهد لتحسين كود Rust وجعله فعالًا قدر الإمكان، وهو ما أظن أنه سيجعله يعمل بشكل أسرع.
هناك أمرٌ أود توضيحه لمن يطلع على شيفرة المثال، وهو أنني أقيس دائمًا مدة تشغيل النصوص البرمجية من البداية إلى النهاية. يشمل ذلك الوقت الذي يستغرقه بايثون لبدء التشغيل، وخاصةً في مثال فرز الفقاعات، وكذلك تحضير مصفوفة الأرقام العشوائية التي تُفرز بعد ذلك. كل شيء مُضمن، لأنني أتبع نهجًا بسيطًا في توقيت تشغيل العملية بأكملها.
هل هذا يكفي من المعايرة؟ الأمر يعتمد على الحالة. إذا أردتُ إجراء تحليل شامل للأداء، فسأحتاج إلى تشغيل أمثلة إضافية كثيرة. ولكن كما ذكرتُ سابقًا، كنتُ أحاول فقط تكوين فكرة عن فروق الأداء بين إصدارات بايثون، وهذا كافٍ لإشباع فضولي.
باستثناء السؤال الأخير.
معايير ماك
كان جهدي الأخير هو نقل جميع الملفات إلى حاسوبي المحمول ماك لإعادة تشغيل جميع الاختبارات والتأكد من تطابق النتائج. تكمن أهمية هذا الأمر في أن أجهزة ماك تعمل حاليًا بمعالجات ARM، لذا قد تختلف خصائص تحسينات الأداء تمامًا عن تلك المصممة لشرائح Intel.
فيما يلي مخطط لنتائج مؤشر فيبوناتشي القياسي بما في ذلك نتائج Intel (الأشرطة الزرقاء) وM2 (الأشرطة الحمراء).

التالي هو اختبار فرز الفقاعات:

عند النظر إلى هذه النتائج، تذكّر أن سرعة الاختبارات التي تُجرى على كلا الجهازين تُقاس بالثواني وليست مُوحدة، لذا فإن مقارنة الفترات الزمنية المُطلقة بينهما لا تُقدم أي معلومات مهمة سوى معرفة أيهما يتمتع بمعالج أقوى. المهم هو رؤية أشكال الأشرطة الزرقاء والحمراء.
وماذا تشير هذه النتائج؟ قفزة الأداء في الإصدار 3.11 موجودة أيضًا على معالجات ARM، لكنها تبدو أقل حدة من إصدارات Intel من بايثون. وبعيدًا عن هذه الملاحظة، لا أرى أي اختلافات جوهرية بين المنصتين، وهو أمر جيد.
“أنا أحصل على نتائج مختلفة عن نتائجك”
إذا شغّلتَ معاييرَ أداءٍ خاصة بك، فمن المُحتمل تمامًا أن تختلف نتائجك عن نتائجي، خاصةً إذا كنتَ تستخدم أمثلةً برمجيةً مختلفة. كما ذكرتُ سابقًا، سرعةُ تحليل شيفرة بايثون ليست ثابتة، بل تتغير باختلاف مُفسّر شيفرة بايثون المُجمّعة. من الطبيعي أن تختلف خصائص الأداء في الأمثلة المُختلفة، ولهذا السبب يصعب تحديد مدى سرعة أو بطء بايثون أو كيفية مُقارنة إصدارين مُختلفين.
تجدر الإشارة أيضًا إلى أن المثالين النصيين اللذين كتبتهما مصممان لاستخدام منطق بايثون الصرف، لأنني أردتُ تقييم أداء المُفسّر. في العديد من أنواع التطبيقات، ستجد أن بايثون أو التبعيات التي تستخدمها توفر دوالًا مُحسّنة مكتوبة بلغة C أو C++، لذا فإن هذه الدوال مُحسّنة بالفعل ولن تصبح أسرع بفضل تحسينات مُفسّر بايثون. هذه بعض الأمثلة على الدوال المُنفّذة في الكود الأصلي، لذا فهي بالفعل سريعة قدر الإمكان:
- جميع خوارزميات إنشاء التجزئة في وحدة hashlib.
- وحدة re لمطابقة التعبيرات العادية.
- معظم مكتبات علوم البيانات والذكاء الاصطناعي، بما في ذلك numpy، وpandas، وscipy، وscikit-learn، وpytorch، وما إلى ذلك.
- الكثير من الأشياء الأخرى!
إذا كنت تقوم بمقارنة أداء كود الإنتاج الحقيقي، فمن المحتمل جدًا أن يكون لديك مزيج من Python الخالص وال المحسّنة بشكل كبير والتي تنتهي بتشغيل الكود الأصلي، لذا فإن توقعي هو أن الاختلافات التي وجدتها في اختباراتي ستكون أقل أهمية عند استخدام الكود الذي لا يتجنب الدوال المحسّنة عن قصد كما فعلت.
ومع ذلك، إذا كنت تستخلص استنتاجات من معاييرك التي تبدو مثيرة للاهتمام بطرق مختلفة عن معاييري، فمن الأفضل أن تشارك نتائجك علنًا وتخبرني بذلك!
ما يقوله مطورو بايثون عن الأداء
لأن أداء بايثون موضوعٌ شائعٌ هذه الأيام، تسمع الكثير من الآراء، ومنها ما هو خاطئ أو مضلل أو مبالغ فيه. سأعلق على بعضها هنا.
Asyncio هو لاعب يغير قواعد اللعبة في الأداء
هذا مثير للاهتمام، لأن asyncio تم إصداره منذ فترة طويلة في أيام بايثون 3.4 ومع ذلك لا يزال هناك الكثير من الأشخاص الذين يعتقدون أن Python غير المتزامن أسرع من Python العادي.
أعتذر عن خيبة أملك إن كنت تعتقد هذا، ولكن هذا نتيجة سوء فهم لماهية asyncio وكيفية عمله. تعمل لغة بايثون غير المتزامنة بنفس سرعة بايثون العادية، بل هي في الواقع نفس المُفسّر الذي يُشغّل الأكواد القياسية وغير المتزامنة.
الفائدة التي يجلبها asyncio إلى Python هي أنه في بعض الحالات يسمح لتطبيقك بالتعامل مع عدد أكبر من المهام المتزامنة مقارنة بالتعدد في الخيوط والمعالجة المتعددة.
مُجمِّع Python 3.13 JIT يُغيِّر قواعد اللعبة في الأداء
يشير هذا إلى ميزة تجريبية جديدة في Python 3.13.0 التي تقدم مُجمِّع Just-In-Time (JIT).
مُجمِّع JIT عبارة عن وحدة تقوم بترجمة الكود الثنائي لـ Python إلى كود آلي أثناء التشغيل، مما قد يؤدي إلى تحسينات كبيرة في الأداء.
مع أن هذا يبدو رائعًا، إلا أن إصدار مُجمِّع JIT المُضمَّن في الإصدار 3.13.0 مُعطَّل افتراضيًا، لأن فريق مُطوِّري Python الأساسيين يعتبرون أن تحسينات الأداء ليست كبيرة بما يكفي حتى الآن، خاصةً وأن تجميع JIT يستهلك ذاكرة أكبر ويُقدِّم تبعيات جديدة أثناء البناء. هذه ميزة يجب مُتابعتها مع نضجها في الإصدارات المستقبلية.
على أي حال، كنتُ متشوقًا لمعرفة أداء مثاليّتي معه، لذا بنيتُ نسخة JIT من مُفسّر 3.13.0 وأجريتُ الاختبارات عليه. إليك النتائج:
مثال | الوقت (بالثواني) | Speedup vs. CPython 3.13 |
---|---|---|
Fibonacci numbers | 9.89 | 0.98x |
Bubble sort | 3.06 | 1.21x |
عند النظر إلى هذه الأرقام، أوافق على أن هذا لديه القدرة على أن يصبح ميزة مهمة في الإصدارات المستقبلية من Python، لكنه في الحقيقة ليس شيئًا أرغب في استخدامه حتى الآن.
إذا كنت تريد أن يكون لديك فكرة عن إمكانيات التسريع باستخدام مُجمِّع JIT أكثر تطورًا، ففكر في نتائج المعايير الخاصة بـ Node.js وPyPy كأمثلة جيدة، حيث يستخدم كلاهما هذه التقنية.
قفل المترجم العالمي (GIL) يقتل أداء Python
لقد تلقت GIL قدرًا كبيرًا من الصحافة السيئة في الآونة الأخيرة، وهو أمر أشعر أنه غير عادل إلى حد ما.
ما هو بايثون GIL؟ GIL هي آلية تحمي هياكل البيانات الداخلية التي يحتفظ بها بايثون.
حماية المُفسّر من عدم الاتساق نتيجةً للوصول المتزامن من خيوط متعددة. بدون GIL، تحتاج البرامج متعددة الخيوط إلى آلية مختلفة، وربما أكثر تطورًا، لحماية هياكل البيانات هذه، وهو أمرٌ ثبتت صعوبته. يُعدّ التعقيد المُرتبط به السبب الرئيسي وراء استمرار الحديث عن إزالة GIL لسنوات، ولكن لم يُحرز تقدم ملموس في هذه المهمة إلا مؤخرًا مع الإصدار 3.13.0.
في حين أنه من الصحيح أن GIL يمكن أن يؤثر سلبًا على أداء البرامج متعددة الخيوط، فمن المهم أن نأخذ في الاعتبار أن هناك الكثير من التطبيقات التي لا تتأثر بهذا، أي تلك التي لا تستخدم الخيوط، أو تلك التي تستخدم الخيوط، ولكنها مرتبطة في الغالب بالإدخال/الإخراج.
تكمن المشكلة في أن أي استبدال لـ GIL من المرجح أن يزيد من عبء وقت التشغيل، لذا فإن التطبيقات متعددة الخيوط التي تباطأت بشكل ملحوظ بسبب GIL فقط هي التي ستشهد تحسنًا في الأداء على الرغم من هذا العبء. أما بالنسبة لأي تطبيق آخر، فإن إزالة GIL ستؤدي إلى انخفاض في الأداء.
لاختبار صحة تحليلي، قمتُ ببناء الإصدار 3.13.0 من بايثون مع خيار التشغيل الحر التجريبي (أو “بدون GIL”)، وشغّلتُ عليه اختباري الأداء. هذه الأمثلة ليست متعددة الخيوط، لذا لا يُتوقع أن تستفيد من عدم وجود GIL، ولكن السؤال هو: ما مدى تأثير ذلك على الأداء؟ إليك النتائج:
مثال | الوقت (بالثواني) | Speedup vs. CPython 3.8 | Speedup vs. CPython 3.13 |
---|---|---|---|
Fibonacci numbers | 20.0 | 1.01x | 0.49x |
Bubble sort | 7.1 | 0.78x | 0.52x |
بشكل أساسي، تُشغّل نسخة بايثون 3.13.0 ذات الترابط الحرّ الكود بسرعة تُقارب نصف سرعة بايثون 3.13، وفي بعض الحالات قد تكون أبطأ من بايثون 3.8. أنا متأكد من أن تكلفة تعطيل GIL ستنخفض في الإصدارات المستقبلية من بايثون مع تطور هذه الميزة، لكنني لا أرى حاليًا استخدامات كثيرة لبايثون بدون GIL، على الأقل بالنسبة لنوع التطبيقات التي أكتبها، والتي عادةً ما تكون مُقيدة بالإدخال والإخراج.
أكرر أنه حتى مع هذه التكلفة الكبيرة، ستلاحظ بعض تطبيقات بايثون التي تعتمد على وحدة المعالجة المركزية بشكل كبير وتستخدم تعدد الخيوط تحسنًا في الأداء، لذا لا أريد أن يُنظر إليّ بنظرة سلبية. أعتبر هذه الميزة إضافة مرحب بها إلى بايثون، طالما يُمكن تفعيلها أو تعطيلها حسب الحاجة!
إذا كنت تعتقد أن GIL يبطئ جميع تطبيقات Python، فلديك الآن بعض البيانات التي تتعارض مع هذه الفكرة.
في عنوان هذه المقالة، سألتُ إن كانت بايثون بطيئةً حقًا. لا توجد إجابة موضوعية على هذا السؤال، لذا ستحتاج إلى استخلاص استنتاجاتك الخاصة بناءً على البيانات التي عرضتها هنا، أو إذا كنت مهتمًا، يمكنك إجراء اختباراتك الخاصة وتكملة نتائجي بنتائجك الخاصة.
بناءً على تحليلي، أنا مندهشٌ للغاية من تحسينات الأداء التي طرأت على بايثون خلال العامين الماضيين، بدءًا من بايثون 3.11، كما أنني منبهرٌ جدًا بـ PyPy، الذي توقعتُ أنه أسرع بقليل من CPython، ولكنه يعمل بسرعة Node.js أو قريبة منها. سأستخدم PyPy أكثر في المستقبل بالتأكيد!
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.