• בלוג
  • השוואת זמני ריצה בין השפות פרל, פייתון ורובי

השוואת זמני ריצה בין השפות פרל, פייתון ורובי

18/06/2015
C++

פוסט זה כולל טיפ קצר לעבודה יעילה עם Python. אם אתם רוצים ללמוד פייתון יותר לעומק אני ממליץ על קורס Python כאן באתר.
הקורס כולל עשרות שיעורי וידאו והמון תרגול מעשי וילמד אתכם Python בצורה מקצועית מההתחלה ועד הנושאים המתקדמים.

בעולם שפות התכנות יש הפרדה (לפחות תפיסתית) בין שפות ״אמיתיות״ לשפות ״סקריפטים״. בשפה אמיתית תשתמשו לכתוב Kernel או לפתח קוד תקשורת זמן-אמת ואילו בשפת הסקריפטים נשתמש בשביל לכתוב דברים שזמן הביצוע שלהם פחות חשוב.
בשנים האחרונות חל מהפך תפיסתי מסוים כשמערכות ווב עוברות להכתב בשפות הקלילות. הסיבה היא בעיקר שיפור בחומרה שאיפשר לשפות הסקריפטים לרוץ מהר מספיק.

בפוסט זה אני לא הולך לבחור את האבחנה בין שני הסוגים, או לנסות להבין איזה שפה שייכת לאיזה קטגוריה. במקום, לקחתי 3 שפות השייכות בבירור לקטגוריית השפות הקלילות ובחרתי לראות מי משלושתן מבצעת את אותה המשימה מהר יותר. 

1. המשימה

בשביל לעשות חיים קשים למחשב ולא יותר מדי קשים עבורנו, נכתוב תוכנית המבצעת חישוב מקדים לערכי MD5 של מחרוזות בשפה. ישנן מעל 1.5 מיליון מחרוזות באורך 4 המורכבות מאותיות קטנות וספרות מה שאומר שהחישוב ייקח זמן אבל לא יותר מדי.
בשביל לסבך עוד יותר את המחשב, שמרתי את כל התוצאות בזכרון במבנה של Hash ובסוף שלפתי ממנו את אחד הערכים.

התוכנית מומשה באופן מכוון בצורה מאוד דומה בין השפות. 

2. נתחיל עם פרל

פרל היתה ההפתעה הגדולה שלי במבחן, מאחר ולפחות בחוגים בהם אני מסתובב היא ידועה בתור המהירה בחבורה. ייתכן והבעייה במודול המחשב MD5 או במקום אחר. גירסת פרל ששימשה לניסוי היא 5.20. בכל מקרה הנה התוכנית וזמן העבודה על הנייד שלי:

use strict;
use warnings;
use v5.18;
use Digest::MD5 qw/md5_hex/;
use File::Slurp;

my $len = 4;
my @ab = (0..9, 'a'..'z');
my $total = @ab ** $len;

my %hash;

for ( my $i= 0; $i < $total; $i++ ) {
	my $next = join('', map { char_at(\@ab, $i, $_) } (0..$len - 1));
	my $word = $next;
	my $md5 = md5_hex($next);
	if ( $i % 100_000 == 0 ) {
		say int($i/100_000), " / ", int($total/100_000);
	}
	$hash{$md5} = $word;
}
say $hash{"657f8b8da628ef83cf69101b6817150a"};


sub char_at {
	my ($ab, $i, $d) = @_;
	$ab->[($i / (@$ab ** $d )) % @$ab ];
}

וזמן הריצה:

real 0m14.610s
user 0m13.874s
sys 0m0.445s

3. נמשיך עם פייתון

פייתון הציגה פער ניכר מפרל בזמן החישוב. הגירסא היא 2.7.6 והנה הקוד והתוצאה:

import string
import hashlib

def main():
    _len = 4
    _trace = 100000

    ab = string.ascii_lowercase + string.digits
    total = len(ab) ** _len

    print "total = %d" % total

    res = {}

    for i in xrange(total):
        m = hashlib.md5()
        _next = ''.join([char_at(ab, i, d) for d in range(_len)])
        m.update(_next)
        md5 = m.hexdigest()
        res[md5] = _next

        if i % _trace == 0:
            print "%d / %d" % ( int(i/_trace), int(total / _trace))

    print res["657f8b8da628ef83cf69101b6817150a"]

def char_at(ab, i, d):
    return ab[int(i / (len(ab) ** d)) % len(ab)]



if __name__ == '__main__':
    main()

זמן ריצה:

real 0m12.449s
user 0m11.578s
sys 0m0.498s

 

4. והנצחון הולך ל… רובי

רובי הצליחה להציג פער קטן מאוד אך עקבי מול פייתון. אני משתמש בגירסת 2.1.2 והנה הקוד והתוצאה:

require 'openssl'

def char_at(ab, i, d)
	ab[(i / (ab.length ** d)).to_i % ab.length]
end

len = 4
ab = [*'0'..'9', *'a'..'z']

total = ab.length ** len
trace = 100_000
res = {}

hasher = OpenSSL::Digest::MD5.new

total.times do |idx|
	word = (0..len - 1).map { |d| char_at(ab, idx, d) }.join
	md5 = hasher.hexdigest(word)
	res[md5] = word

	puts "#{(idx/trace).to_i} / #{(total / trace).to_i}" if idx % trace == 0
end


puts res['657f8b8da628ef83cf69101b6817150a']

וזמן הריצה

real 0m12.098s
user 0m11.292s
sys 0m0.494s

 

5. ורק לצורך ההשוואה: כך זה נראה ב C++

לא סתם מספרים ש C++ היא בחירה טובה כשביצועים הם שיקול. אותו הקוד רץ כמעט במחצית הזמן:

int main(int argc, char *argv[])
{    
    QCoreApplication a(argc, argv);

    QCryptographicHash hasher(QCryptographicHash::Md5);
    StringHash results;

    for ( size_t i=0; i < qPow(sz, len); i++ ) {
        QString next;

        for ( size_t d=0; d < len; d++ ) {
            next += ab[int(i / qPow(sz, d)) % sz];
        }
        if ( i % trace == 0 ) {
            qDebug() << int(i / trace) << " / " << int(total / trace);
        }

        hasher.addData(next.toLocal8Bit());
        QString md5 = QString(hasher.result().toHex());
        QString word = next;

        results[md5] = word;
        hasher.reset();
    }
    qDebug() << results["657f8b8da628ef83cf69101b6817150a"];

    return 0;
}

וזמן הריצה:

real	0m7.298s
user	0m6.816s
sys	0m0.368s

 

6. השורה התחתונה

מעבר לטריוויה אני כלל לא בטוח שיש חשיבות לפערים אלו בין פרל, פייתון ורובי (וזה כמובן לא קשור לעובדה שפרל הפסידה כאן). מה שכן מעניין לראות זה הפער בין השפות הקלילות ל C++, שעוזר להבין את המשיכה של שפה זו. צריך גם לזכור שאת שיפור הביצועים המשמעותי אנו נקבל באמצעות מיקבול החישוב, אבל זה כבר נושא לפוסט אחר.