يونيو 21، 2009

مقارنة بين أداء بايثون و لغات البرمجة الأخرى

قضيت الفترة الأخيرة في إختبار أداء بايثون خصوصا أداء الحلاقات التسلسلية 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)

هناك 9 تعليقات:

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

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

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


    بالنسبة لإستخدام بايثون في البرامج التي تتطلب سرعة فهنالك من يستخدمه في تلك البرمجيات، و أنا غير مؤهل حاليا لأحكم عليه أو أعطي رأيا يبنى عليه. (ستجد هنا بعض الشركات التي تستخدمه في بعض الألعاب و برامج المحاكات http://wiki.python.org/moin/OrganizationsUsingPython)

    التدوينة كانت لمجرد التنبيه إلى شيئ لاحظته. و شكرا جزيلا لك لأنك دفعتني إلى تعلم المزيد من خلال حلك :)

    ردحذف
  3. هل جربت نفص الكود مع البيثون 3.1 ؟

    ردحذف
  4. سأجربه بعد أن أتوصل بالتحديث 3.1 بشكل تلقائي.
    بالنسبة للإصدار 3.0 كانت نتائجه أبطأ من 2.6.2

    ردحذف
  5. قمت اليوم بإختبار الإصدار 3.1 و كانت النتيجة أنه مازال أبطأ (32 ثانية) مقارنة مع 2.6.2 (24 ثانية)

    ردحذف
  6. غير معرف14/6/10 11:57 ص

    كيف تمة حسابة الوقت ؟
    هل كان هذا بعد إقلاع بارد في كل قياس ؟

    ردحذف
  7. غير معرف14/6/10 12:01 م

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

    مقارنة اللغات (benchmarking) تحتاج أولا اختيار برامج أو تطبيقات واقعية تمثل الإستعمالات الأكثر شيوعا و هذا بحد ذاته صعب.

    ردحذف
  8. إلى غير معروف (أفترض أنك صاحب التعليقين):
    * حسبت الوقت باستخدام الأمر time على نظام جنو/لينوكس.
    * المدة للإقلاع الساخن.
    * الأمثلة بسيطة فعلا، لكن الحساب و الحلقات منشرة في أغلب أنواع البرامج.
    * نعم لا يمكن الحكم على أي لغة بتلك السهولة لكن أعتقد أن الفكرة هنا هي المقارنة -- و لو بأمثلة جد بسيط -- بين اللغات (الشبه) التفسيرية و (الشبه) المُجَمعة. كما أعتقد أن المبرمجين المتمرسين يعرفون ذلك الفرق و لا يمكن إنكاره فقط التحايل عليه كما حاولت من خلال أمثلة بايثون.

    * و أهم شيء أشكرك على تلك الأسئلة القيمة، سعدت بذلك :)

    ردحذف
  9. جرب اضافة
    from psyco import full
    full()

    قبل كود البايثون
    لتستدعي مكتبة التسريع
    ستصبح بايثون اسرع من C++
    وجرب استخدام مفسر pypy لتجد السرعة تضاعفت

    ردحذف