قضيت الفترة الأخيرة في إختبار أداء بايثون خصوصا أداء الحلاقات التسلسلية 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 ثانية ليجد الحل:
هذا الكود رديء، و غيرته ليصبح كالتالي و هو يستغرق 40 ثانية:
ثم قرأت حول طرق تحسين كود بايثون و وجدت من ينصح باﻹستخدام الدوال التالية lambda, map و filter بدلا من الحلقة التسلسلية for و كانت النتيجة كالتالي:
الكود الثالث يستغرق 5 دقائق و 34 ثانية:
الكود الرابع يستغرق هو أيضا 5 دقائق و 34 ثانية:
الكود الخامس: ثم بعد ذلك قمت بإدراج الكود الثاني داخل دالة و أصبح على الشكل التالي و يستغرق 24 ثانية:
المهم ماذا يجري هنا؟ و لما كل هذا؟
بايثون يعتبر من لغات البرمجة التي تعتمد على التحديد الدينامكي أو المرن للمتغيرات (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):
ثم بعد تعديل الحلقة التسلسلية for أوجد الحل في دقيقتين و ثانية (2min 1s)
بيئيتشبي PHP 5.2.9 أوجد الحل في 21 ثانية:
من خلال ما سبق يتضح أن php أسرع من python و ruby. و كلها تصنف كلغات ديناميكية النوعية.
لكن كيف هو أداء اللغات الثابتة النوعية ك C#, Scala و GNU C++ ؟ الجواب هو إنها فائقة السرعة مقارنة مع اللغات الديناميكية المجربة أعلاه.
بالنسبة لجنوا سي++ (GNU C++) أوجد الحل في ثاتية و نصف!:
بالنسبة لسي شارب C# أوجد الحل في ثانيتين. (بإستخدام كود الصديق محمد الجوهري)
بالنسبة لجافا (java) أوجدت الحل في ثانية و ثمانية أعشار الثانية (1.8s)
و أخيرا لغة سكالا (Scala) التي أوجدت الحل في ثانيتين و أربعة أعشار الثانية (2.4s)
كما نلاحظ فإن اللغات دات التحديد الثابت تعرض الحل في حدود ثانيتين، بينما اللغات دات التحديد الديناميكي تسغرق بعض الوقت لتصل إلى النتيجة. هذه الفرق عند إنجاز بعض المهام أو في بعض الحالات قد لا يعجب البعض، لكن في أغلب الأحيان لن تواجهوا هذا النوع من المتطلبات (السرعة أولا و أخيرا) و ستجدون في اللغات دات التحديد الديناميكي المرونة التي ستسمح لكم بتطوير برمجياتكم بسرعة أكبر و مدة أقصر. لكن إن صممت يمكن إعتبار Scala بديل أفضل لأنها تجمع بين مجموعة من المزايا و أنماط البرمجة.
الخلاصة:
1. أكتب دائما كود بسيط و تسهل قرائته.
2. الحلاقات التسلسلية for تستهلك الكثير من وقت المعالج، و يمكن تحسينها بإتباع بعض النصائح.
3. إدا كانت السرعة جد ضرورية في منطقة معينة من برنامجك حينها قم ببرمجة ذلك الجزء بلغة سي أو سي ++
4. سيحصل بايثون على آلة إفتراضية محسنة (LLVM) ستجعل أداءه مشابه للغة سي و سي++
تحديث 1: قمت بتجربة الكود الخامس بإستخدام الإصدار الجديد لبايثون 3.1 الذي توصل إلى الحل في 32 ثانية. (مازل أبطأ في هذا الإختبار لكن أفضل من الإصدار 3.0)
عند تعلمي لبايثون لاحظت أن مرونته و أدائه جد قويين في مجموعة من المهام، من بينها إنشاء قاموس يحتوي على 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
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
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
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
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()
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()
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
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";
$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;
}
#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);
}
}
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())
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)
معنى ذلك أنه لا يفضل استخدام بايثون في الوقت الحالي لتصميم البرامج التي تتطلب سرعة عالية كالألعاب أو برامج المحاكاه.
ردحذفننتظر توافر الآلات الإضافة المحسنة للغة بايثون وكذلك كومبايلر لأنظمة التشغيل المختلفة خاصة أنظمة تشغيل الجوال.
لا، ليس هذا هو المستخلص بل على مبرمجي بايثون الجدد أن يتعلموا طبيعة هذه اللغة و يفهموا كيف يتعامل بايثون مع بعض الجزئيات حتى يستفيدوا من مزايه بدلا من كتابة كود يعتمد على السرعة الخامة للمعالج.
ردحذفإستخدام الألات و الإضافات المحسنة لا يحقق سرعة كبيرة (في حدود 20% على الأكثر و قد جربت بعض منها رغم أني لم أذكر ذلك في التدوينة) بل قد يزيد في تعقيد المهمة.
بالنسبة لإستخدام بايثون في البرامج التي تتطلب سرعة فهنالك من يستخدمه في تلك البرمجيات، و أنا غير مؤهل حاليا لأحكم عليه أو أعطي رأيا يبنى عليه. (ستجد هنا بعض الشركات التي تستخدمه في بعض الألعاب و برامج المحاكات http://wiki.python.org/moin/OrganizationsUsingPython)
التدوينة كانت لمجرد التنبيه إلى شيئ لاحظته. و شكرا جزيلا لك لأنك دفعتني إلى تعلم المزيد من خلال حلك :)
هل جربت نفص الكود مع البيثون 3.1 ؟
ردحذفسأجربه بعد أن أتوصل بالتحديث 3.1 بشكل تلقائي.
ردحذفبالنسبة للإصدار 3.0 كانت نتائجه أبطأ من 2.6.2
قمت اليوم بإختبار الإصدار 3.1 و كانت النتيجة أنه مازال أبطأ (32 ثانية) مقارنة مع 2.6.2 (24 ثانية)
ردحذفكيف تمة حسابة الوقت ؟
ردحذفهل كان هذا بعد إقلاع بارد في كل قياس ؟
لا يمكن الحكم علي سرعة لغة باسرها من خلال أمثلة بسيطة تقوم بعمل بسيط واحد.
ردحذفمقارنة اللغات (benchmarking) تحتاج أولا اختيار برامج أو تطبيقات واقعية تمثل الإستعمالات الأكثر شيوعا و هذا بحد ذاته صعب.
إلى غير معروف (أفترض أنك صاحب التعليقين):
ردحذف* حسبت الوقت باستخدام الأمر time على نظام جنو/لينوكس.
* المدة للإقلاع الساخن.
* الأمثلة بسيطة فعلا، لكن الحساب و الحلقات منشرة في أغلب أنواع البرامج.
* نعم لا يمكن الحكم على أي لغة بتلك السهولة لكن أعتقد أن الفكرة هنا هي المقارنة -- و لو بأمثلة جد بسيط -- بين اللغات (الشبه) التفسيرية و (الشبه) المُجَمعة. كما أعتقد أن المبرمجين المتمرسين يعرفون ذلك الفرق و لا يمكن إنكاره فقط التحايل عليه كما حاولت من خلال أمثلة بايثون.
* و أهم شيء أشكرك على تلك الأسئلة القيمة، سعدت بذلك :)
جرب اضافة
ردحذفfrom psyco import full
full()
قبل كود البايثون
لتستدعي مكتبة التسريع
ستصبح بايثون اسرع من C++
وجرب استخدام مفسر pypy لتجد السرعة تضاعفت