قضيت الفترة الأخيرة في إختبار أداء بايثون خصوصا أداء الحلاقات التسلسلية
for و مقارنته بكل من روبي (
Ruby)، بيئتشبي (
PHP)، جنوا سي بلاس بلاس (
GNU C++)، سي شارب (
C#)، جافا (
Java) و سكالا (
Scala)
عند تعلمي لبايثون لاحظت أن مرونته و أدائه جد قويين في مجموعة من المهام، من بينها إنشاء قاموس يحتوي على 1 مليون مفتاح و قيمة في ثانية واحدة و أشياء أخرى. و أثناء حلي لمشاكل المشروع Euler Project لاحظت بعض البطء في الحلاقات التسلسلية for لكن لم أعر الأمر أي اهتمام لأن حاسوبي يعتبر من الديناصورات (P4 2.4Ghz) و عند تجربتي لنفس الكود على حاسوب آخر (Core 2 Duo 2.4Ghz) كان ينجز نفس المهمة 3 مرات أسرع.
و بعد وضعي
للمشكلة رقم 5 تفضل الصديق
محمد الجوهري بوضع الحل بلغة C# و عند تجربتي لحله تفاجأت من السرعة التي يتم العثور بها على الحل. قرأت الكود و جربته أكثر من مرة محاولا العثور على شيء يميزه، لكنه عادي. من هنا انطلقت رحلتي لفهم ما يجري في الحلاقات التسلسلية
for في بايثون 2.6.2
الكود الاول:
في البداية كان الكود كالتالي و يستغرق 55 ثانية ليجد الحل:
n = 20
divs = [x for x in range(n, 0, -1)]
i = 0
while True:
i = i+1
nop = False
for x in divs:
if (i*n) % x != 0:
nop = True
break
if nop == False:
print "\nThe answer is ", i*n
break
هذا الكود رديء، و غيرته ليصبح كالتالي و هو يستغرق 40 ثانية:
i = 20
done = False
seq = range(2, 20+1)
while not done:
for j in seq:
if i % j != 0:
break
elif j == 20:
done = True
i += 20
print i-20
ثم قرأت حول طرق تحسين كود بايثون و وجدت من ينصح باﻹستخدام الدوال التالية
lambda,
map و
filter بدلا من الحلقة التسلسلية
for و كانت النتيجة كالتالي:
الكود الثالث يستغرق 5 دقائق و 34 ثانية:
i = 20
done = False
seq = range(2, 20+1)
while not done:
if len(filter(None, map(lambda y: i % y == 0, seq))) == len(seq):
done = True
break
i += 20
print i
الكود الرابع يستغرق هو أيضا 5 دقائق و 34 ثانية:
i = 20
done = False
seq = range(2, 20+1)
while not done:
if False in map(lambda x: i % x == 0, seq):
pass
else:
done = True
break
i = i + 20
print i
الكود الخامس: ثم بعد ذلك قمت بإدراج الكود الثاني داخل دالة و أصبح على الشكل التالي و يستغرق 24 ثانية:
def euler5():
i = 20
seq = range(2, 20+1)
while True:
for j in seq:
if i % j != 0:
break
elif j == 20:
return i
i += 20
print euler5()
المهم ماذا يجري هنا؟ و لما كل هذا؟
بايثون يعتبر من لغات البرمجة التي تعتمد على التحديد الدينامكي أو المرن للمتغيرات (
Dynamically Typed)، بمعنى أن المتغيرة
a يمكن أن تحتوي على قيمة رقمية ثم تتحول لتعبر عن قيمة نصية بكل سهولة أثناء عمل البرنامج. و هذا ما يتشابه فيه كل من بايثون python، روبي ruby و بيئيتشبي php. بينما تعتمد كل من لغة سي بلاس بلاس c++ و جافا java و سي شارب c# و سكالا Scala على التحديد الثابت للمتغيرات (
Statically Typed). طبعا هنا أقوم بشرح مبسط كما لم أتطرق للأفضلية لأن لكل منها (الغات) نقاط قوتها و ضعفها.
المهم هو أن هذه المرونة تتم على حساب جزء بسيط في ضعف الاداء في بعض العمليات. لكن المثير هو أن ضعف أداء الحلاقات التسلسلية
for أكبر بكثير من المتوقع، فما هو السبب؟
اتضح لي فيما بعد أن الفلسفة التي يتبعها مطورو بايثون هي:
1. سهولة قراءة و وضوح الكود أهم من تعقيده لأجل تحسينه (
optimization) للإستفادة من أخر جزء من الثانية.
2. هنالك قانون يسمى قانون مور (
Moore's law) يقول أن كفائة العتاد تتضاعف كل 18 شهر تقريا. بمعنى أنه مع تطور العتاد سيسبح الفرق بسيطا بين الكود المُحسن و العادي.
3. و إذا كانت السرعة أولوية لا غنى عنها حينها يجب على المبرمج أن يستخدم قاعدة 90/10 بحيث يُفضل أن يكتب الجزء المسرع/المحسن بلغة C أو لغة أخرى.
النقطة المهمة التي تعجبني في
مشاكل المشروع يولر Euler Project هي أن كل مشكلة تقريبا يمكن حلها بطريقة تقليدية و أخرى بإستخدام بعض الذكاء و التفكير مع مراعات أن يبقى الحل بسيطا :)
و كما قال
لِيُوناردو دا فينشي (
Leonardo da Vinci) "
البساطة هي منتهى التعقيد"
و هذا هو ما قمت به في
الحل الثاني للمشكلة رقم 5 بحيث
تظهر النتيجة في جزء من الثانية.
لكن كيف هو أداء باقي لغات البرمجة الأخرى في إيجاد الحل للمشكلة رقم 5؟
1. روبي Ruby 1.8.7 أوجد الحل في دقيقتين و ثلاثين ثانية (2min 30s):
def GetResult()
n = 20
i = n
while true do
for j in 2..n do
break if i % j != 0
if j == n
return i
end
end
i += n
end
end
puts GetResult()
ثم بعد تعديل الحلقة التسلسلية for أوجد الحل في دقيقتين و ثانية (2min 1s)
def GetResult()
n = 20
i = n
seq = 2
..n
while true do
for j in seq
do
break if i % j != 0
if j == n
return i
end
end
i = i+n
end
end
بيئيتشبي PHP 5.2.9 أوجد الحل في 21 ثانية:
$i = 20;
$n = $i;
$Done = false;
while (! $Done) {
for ($j = 2; $j <= $n; ++$j) {
if ($i % $j != 0) { break; }
else {
if ($j == $n) { $Done = true; }
}
}
$i = $i + $n;
}
print $i - $n . "\n";
من خلال ما سبق يتضح أن php أسرع من python و ruby. و كلها تصنف كلغات ديناميكية النوعية.
لكن كيف هو أداء اللغات الثابتة النوعية ك C#, Scala و GNU C++ ؟ الجواب هو إنها فائقة السرعة مقارنة مع اللغات الديناميكية المجربة أعلاه.
بالنسبة لجنوا سي++ (GNU C++) أوجد الحل في ثاتية و نصف!:
#include <cstdlib>
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int i = 20;
bool Done = false;
while (!Done) {
for (int j = 2; j <= 20; ++j)
{
if (i % j != 0) break;
else
{
if (j == 20) Done = true;
}
}
i += 20;
}
cout << endl << i-20 << endl;
return EXIT_SUCCESS;
}
بالنسبة لسي شارب C# أوجد الحل في ثانيتين. (بإستخدام كود الصديق
محمد الجوهري)
بالنسبة لجافا (java) أوجدت الحل في ثانية و ثمانية أعشار الثانية (1.8s)
public class Main {
public static void main(String[] args) {
int i = 20;
boolean Done = false;
while (!Done) {
for (int j = 2; j <= 20; ++j) {
if (i % j != 0) {
break;
} else {
if ( j == 20) {
Done = true;
}
}
}
i=i+1;
}
System.out.println(i-1);
}
}
و أخيرا لغة سكالا (Scala) التي أوجدت الحل في ثانيتين و أربعة أعشار الثانية (2.4s)
def euler5(): Int = {
var n = 20
var i = n
var done = true
while (done)
{
var j = 2
var iter = true
while (iter && j < n+1)
{
if (i % j != 0) iter = false
else j += 1
}
if (iter == true) done = false
i += n
}
i-n
}
println(euler5())
كما نلاحظ فإن اللغات دات التحديد الثابت تعرض الحل في حدود ثانيتين، بينما اللغات دات التحديد الديناميكي تسغرق بعض الوقت لتصل إلى النتيجة. هذه الفرق عند إنجاز بعض المهام أو في بعض الحالات قد لا يعجب البعض، لكن في أغلب الأحيان لن تواجهوا هذا النوع من المتطلبات (السرعة أولا و أخيرا) و ستجدون في اللغات دات التحديد الديناميكي المرونة التي ستسمح لكم بتطوير برمجياتكم بسرعة أكبر و مدة أقصر. لكن إن صممت يمكن إعتبار
Scala بديل أفضل لأنها تجمع بين مجموعة من المزايا و أنماط البرمجة.
الخلاصة:
1. أكتب دائما كود بسيط و تسهل قرائته.
2. الحلاقات التسلسلية for تستهلك الكثير من وقت المعالج، و يمكن
تحسينها بإتباع بعض النصائح.
3. إدا كانت السرعة جد ضرورية في منطقة معينة من برنامجك حينها قم ببرمجة ذلك الجزء بلغة سي أو سي ++
4. سيحصل بايثون على
آلة إفتراضية محسنة (
LLVM) ستجعل أداءه مشابه للغة سي و سي++
تحديث 1: قمت بتجربة الكود الخامس بإستخدام الإصدار الجديد لبايثون 3.1 الذي توصل إلى الحل في 32 ثانية. (مازل أبطأ في هذا الإختبار لكن أفضل من الإصدار 3.0)