مقارنة أداء الأسس مع سي وبايثون وجافاسكريبت

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

سيتم تنفيذ الأمثلة على نظام بهذه المواصفات:

معالج Intel Core i7-8565U رباعي الأنوية، 1.8 جيجا هيرتز
ذاكرة DDR4 2400MHz
ذاكرة تخزين SSD بسرعة 560 ميجابايت\ثانية
نظام لينكس بتوزيعة PopOS 19.10 محدَّث

وستتم المقارنة بين هذه الإصدارات:

الأسس 0.6.1
لغة سي باستخدام مترجم GCC 9.2.1
بايثون 3.7.5
جافاسكريبت باستخدام نود 12.14

المقارنة ستتم بحساب مدة التنفيذ والذاكرة المستهلكة لكل من اللغات الأربع باستخدام الأمر time من سطر الأوامر. الأداء الأفضل هو الذي يتطلب وقتًا وذاكرة أقل لإتمام المهمة.

لغرض الحفاظ على تلوين الشفرة المصدرية قمنا بتضمينها كصورة بدل نص، لكن كل الأمثلة موجودة للتنزيل من هنا.

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

حساب ما إذا كان العدد أوليًا

في هذا المثال سنختار عددًا كبيرًا ونحسب ما إذا كان العدد أوليًا بمحاولة قسمته على كل الاحتمالات الممكنة.

بلغة الأسس

بلغة C

بلغة جافاسكريبت

بلغة بايثون

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-19%2009-47-54

النتائج

المدة المستغرقة لتنفيذ البرنامج لكل من اللغات الأربع:

الأسس: 5.445 ثانية
لغة سي: 5.140 ثانية
جافاسكريبت: 2.630 ثانية
بايثون: 55.910 ثانية

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

شفرة بايثون باستخدام numpy

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-19%2012-50-16

النتيجة لبايثون تغيرت إلى 7.735 ثانية. ما زالت الأبطأ لكن التحسن كبير جدا.

ماذا لو جربنا رقما أصغر؟ لنجرب إزالة مرتبة من الرقم وجعله 673214311 بدلًا من 1673214311 ونجرب اللغات الأربع مجددا واستخدام بايثون بدون numpy.

الأسس: 0.042 ثانية
السي: 0.029 ثانية
جافاسكريبت: 0.020 ثانية
بايثون: 0.011 ثانية

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

حساب النسبة الثابتة

سنجرب الآن برنامجًا أعقد من السابق وهو حساب 800 مرتبة من النسبة الثابتة باستخدام الخوارزمية المذكورة هنا. العملية سريعة إلى حد كبير لذلك سنكرر هذه العملية 1000 مرة.

بلغة الأسس

بلغة C

بلغة جافاسكريبت

بلغة بايثون

النتائج

الأسس: 2.311 ثانية
لغة سي: 1.936 ثانية
جافاسكريبت: 28.668 ثانية
بايثون: 67.250 ثانية

ماذا لو أزلنا التكرار وقمنا بحساب النسبة الثابتة مرة واحدة بدل إعادة العملية 1000 مرة؟ النتائج كانت كما يلي:

الأسس: 0.046 ثانية
لغة سي: 0.024 ثانية
جافاسكريبت: 0.040 ثانية
بايثون: 0.080 ثانية

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

قراءة وتحليل ملف CSV كبير

الاختباران السابقان كانا للتعامل مع العمليات الحسابية والآن جاء دور التعامل مع كمية كبيرة من البيانات. عند التعامل مع كمية كبيرة من البيانات فإن سرعة التنفيذ وحدها لا تكفي لتقييم الأداء وإنما نحتاج أيضًا لحساب استهلاك الذاكرة. في هذا الاختبار سنستخدم الخيار -v للأمر time للحصول على معلومات مفصلة ويهمنا منها قيمة Maximum resident set size، وهو أقصى استخدام للذاكرة وصل إليه البرنامج أثناء التنفيذ.

سنقوم في هذا الاختبار بقراءة ملف CSV يحتوي على مليون قيد، ثم حساب عدد القيود التي تحتوي قيمة معينة. ملف CSV المستخدم مأخوذ من هنا وقد أخذنا من هذا الموقع قائمة المبيعات ذات المليون قيد وقمنا بحساب مبيعات الملابس.

سنبدأ باستخدام عمليات محارف مرتفعة المستوى مثل عملية التقطيع (split). لغة السي كما هو معروف منخفضة المستوى وليس فيها مقابل لهذه العمليات ولذلك سنستثنيها من هذا الاختبار بدل أن نستخدم فيها خوارزمية مختلفة.

رغم حجم البيانات إلا أن العملية ما زالت نسبيًا سريعة، لذلك سنكرر العملية كلها 3 مرات.

بلغة الأسس

بلغة جافاسكريبت

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-19%2023-46-19

بلغة بايثون

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-19%2023-46-57

النتائج

الأسس: 3.75 ثانية، 311.032 ميجابايت
جافاسكريبت: 2.61 ثانية، 481.128 ميجابايت
بايثون: 1.68 ثانية، 381.372 ميجابايت

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

بلغة الأسس

بلغة جافاسكريبت

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-20%2000-03-34

بلغة بايثون

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-20%2000-04-19

النتائج

الأسس: 0.78 ثانية، 311.076 ميجابايت
جافاسكريبت: 1.03 ثانية، 494.632 ميجابايت
بايثون: 1.44 ثانية، 381.372 ميجابايت

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

بلغة الأسس

بلغة جافاسكريبت

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-20%2000-17-17

بلغة بايثون

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-20%2000-18-14

النتائج

الأسس: 0.28 ثانية، 153.844 ميجابايت
جافاسكريبت:0.27 ثانية، 526.856 ميجابايت
بايثون: 1.55 ثانية، 373.488 ميجابايت

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

ماذا لو حسنّا الأداء أكثر وقرأنا ملف CSV سطرًا فسطر بدل قراءته دفعة واحدة؟ في الجافاسكريبت تحميل ملف سطرًا فسطر معقد بعض الشيء وقد يؤثر على المقارنة، لكننا سنعتمدها في كل الأحوال. إليكم الشفرات:

لغة الأسس

لغة جافاسكريبت

لغة بايثون

%D9%84%D9%82%D8%B7%D8%A9%20%D8%B4%D8%A7%D8%B4%D8%A9%20%D9%85%D9%86%202020-02-20%2000-31-48

النتائج

الأسس: 0.25 ثانية، 32.252 ميجابايت
جافاسكريبت: 1.05 ثانية، 71.192 ميجابايت
بايثون: 1.12 ثانية، 9.028 ميجابايت

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

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

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

بلغة الأسس

بلغة C

النتائج

الأسس: 0.12 ثانية، 1.596 ميجابايت
السي: 0.17 ثانية، 1.608 ميجابايت

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

شاركونا آراءكم ومقترحاتكم في التعليقات أدناه.

2 Likes