Август 22, 2010

PHP vs. Python vs. Ruby vs. Groovy

Холивар между поклонниками того или иного языка программирования не закончится никогда. Рискну подбросить дров в огонь, поделившись результатами сравнительного тестирования четырех динамических языков: PHP, Python, Ruby и Groovy. Выбор первых трех очевиден – они наиболее популярны в среде веб-разработчиков. Groovy пока малоизвестен, но в будущем может занять достойную нишу за счет врожденной 100% интеграции с технологиями Java.

Два года назад один хороший человек написал более-менее адекватные скрипты для тестирования производительности всех упомянутых языков и выложил результаты в блог. Я воспроизвел тесты на последних версиях интерпретаторов, результатами и комментариями делюсь ниже.

Тестовая среда

Все тесты проводились на моем домашнем ноутбуке Intel Core2 Duo T5500 (два ядра по 1.66 GHz) с тремя гигабайтами оперативной памяти и быстрым SSD-диском. Управляет парадом свежая инсталляция 32-битной Windows 7 Home Premium.

Каждый скрипт запускался три раза, через некоторое время после прогона всех языков, выполнялся еще один проход по три раза, в качестве итогового времени взяты усредненные значения.

Теоретически, каждый процесс должен был выполняться на одном из ядер процессора, загружая его под завязку (что косвенно подтверждалось постоянным показателем загрузки двухядерного процессора в 50%), но график Диспетчера Задач почему-то показывал одинаковую загрузку в 50% обоих ядер во время теста.

Во время второго прогона также замерялся расход памяти, согласно показаниям Диспетчера Задач.

Производительность

Привожу точные данные по времени выполнения одной итерации скрипта каждым из языков (считается автоматически): табличная форма дает точные цифры, диаграмма позволяет оценить их наглядно.

Точные данные производительности

Язык Время, микросекунд
Groovy 1.7.4 260
PHP 5.2.14 1,200
Python 2.6.5 210
Python 2.6.5 + Psyco 2.0 70
Ruby 1.8.7 400
Ruby 1.9.2 300

Визуальное представление

groovy-php-python-ruby-performance-comparison

Потребление памяти

Здесь необходимо сделать пару замечаний. Большинство интерпретаторов сразу занимали определенный обьем памяти, и в течение работы этот показатель уже не менялся. Однако запросы PHP во время работы линейно росли (особенность тестового кода?), а Ruby 1.8.7 забирал и отпускал память волнообразно (сборка мусора)? В табличку вошли максимальные значения.

Язык Память, мегабайт
Groovy 1.7.4 27
PHP 5.2.14 37
Python 2.6.5 3.8
Python 2.6.5 + Psyco 2.0 4
Ruby 1.8.7 2
Ruby 1.9.2 1.8

groovy-php-python-ruby-memory-consuption

Некоторые замечания

Прежде всего, очень разочаровал PHP. Отвратительное быстродействие и жуткий расход памяти. Я попытался улучшить ситуацию, установив eAccelerator 0.9.5.3, но он фактически никак не повлиял ни на скорость выполнения, ни на потребление памяти. Насколько я понимаю, его фишка состоит в кешировании предварительно скомпилированных скриптов, и устранении фазы интерпретации при повторном обращении к коду. На быстродействие самого кода он не влияет, поэтому в наших условиях толк от него нулевой.

Приятно удивил ускоритель Питона Psyco. Незначительно увеличился расход памяти, при многократно возросшей скорости выполнения. Впечатляющий прирост производительности, в три раза! Связка Python/Psyco позволяет оторваться от всех конкурентов, рассматривая удаляющиеся силуэты в зеркале заднего вида :-)

С другой стороны, Ruby оказался очень экономичным, потребляя в два раза меньше памяти, чем Python, и более чем на порядок обходя PHP. Несомненный плюс. Я ожидал, что версия 1.9.2 будет раза в два-три быстрее предыдущей генерации 1.8.7, но чуда не произошло, разница составляет процентов 30%. Полагаю, что разрыв в большинстве реальных применений будет все-таки значительно больше.

Ну а что же C++ и Java?

Тесты для этих языков я также воспроизвел, но результаты сводить в общую табличку не стал. Например, Java выполняет каждую итерацию всего лишь за полторы микросекунды, то есть в 100-200 раз быстрее, чем указанные динамические языки. С другой стороны, в реальных приложениях разрыв будет гораздо меньше, так как основная часть работы динамических скриптов будет выполняться в подключаемых библиотеках, которые компилируются в двоичный выполнимый код конкретной платформы и работают без потерь производительности.

SLoC, Verbosity, Syntactic Sugar

Немалое значение для программистов, работающих с языками, имеют такие показатели, как количество строк кода (SLoC, Source lines of code), а также количество ключевых слов, скобок, спецсимволов (Verbosity) и вообще красота конструкций языка, легкость восприятия кода, и его красота (Syntactic Sugar).

Можно спорить о субьективных показателях, но с цифрами не поспоришь: программа на Python в два раза короче, чем аналогичный код на Groovy, PHP, C++ или Java. Ruby где-то посередине: программа в полтора раза длинее, чем питонный аналог, но все еще значительно короче, чем остальные скрипты.

Насчет красоты конструкций, изящности и легкости восприятия писать не буду – на вкус и цвет все фломастеры разные, смотрите сами, решайте сами :-)

Так кто самый крутой?

Не буду прятаться за размытыми формулировками :-) На мой вкус, очевидно, что круче всех оказался Python (в случае ускорения за счет Psyco), где-то очень рядом находится новый Ruby 1.9.2 – теряя в производительности, получаем выигрыш в расходе памяти.

Конечно, все цифры и выводы относятся только к данному тестовому приложению, в других условиях, на других платформах, более прямыми руками выводы могут быть противоположными, поэтому приветствуется перепроверка моих результатов, критика и комментарии, а также варианты иных тестовых скриптов, которые могут изменить расстановку сил на Олимпе. Ниже вы найдете все исходные коды для разных языков, а значит – сможете самостоятельно повторить мои опыты. Дерзайте и делитесь результатами :-)

Важные обновления

UPD1 Важное замечание для Python: если функцию range заменить на xrange, чтобы не генерировать огромные списки в памяти, а обходить цикл по итератору, производительность скрипта возрастает незначительно, но расход памяти падает в два раза, с 4 до 2 MB (2.5 MB в случае Psyco)! Таким образом, описанное выше преимущество Ruby по памяти становится неактуальным, интерпретаторы идут ноздря в ноздрю по этому показателю.

UPD2 Оптимизированный PHP-скрипт с форума SQL.ru (без сеттеров-геттеров), показывает более чем в два раза лучшее быстродействие. Среднее время выполнения подскочило до 500 микросекунд на итерацию. Потребление памяти, к сожалению, осталось на прежнем уровне.

Исходные тексты скриптов

Groovy

class Chain
{
    def size
    def first

    def init(siz)
    {
        def last
        size = siz
        for(def i = 0 ; i < siz ; i++)
        {
            def current = new Person()
            current.count = i
            if (i == 0) first = current
            if (last != null)
            {
                last.next = current
            }
            current.prev = last
            last = current
        }
        first.prev = last
        last.next = first
    }

    def kill(nth)
    {
        def current = first
        def shout = 1
        while(current.next != current)
        {
            shout = current.shout(shout,nth)
            current = current.next
        }
        first = current
    }
}

class Person
{
    def count
    def prev
    def next

    def shout(shout,deadif)
    {
        if (shout < deadif)
        {
            return (shout + 1)
        }
        prev.next = next

        next.prev = prev
        return 1
    }
}

def main(args)
{
    println "Starting"
    def ITER = 100000
    def start = System.nanoTime()
    for(def i = 0 ; i < ITER ; i++)
    {
        def chain = new Chain()
        chain.init(40)
        chain.kill(3)
    }
    def end = System.nanoTime()
    println "Total time = " + ((end - start)/(ITER * 1000)) + " microseconds"
}

def ITER = 100000
def start = System.nanoTime()
for(def i = 0 ; i < ITER ; i++)
{
    def chain = new Chain()
    chain.init(40)
    chain.kill(3)
}
def end = System.nanoTime()
println "Time per iteration = " + ((end - start)/(ITER * 1000)) + " microseconds"

PHP

class Person
{
    function __construct($c)
    {
        $this->count = $c;
    }       

    function getPrev()
    {
        return $this->prev;
    }           

    function setPrev($pr)
    {
        $this->prev = $pr;
    }   

    function getNext()
    {
        return $this->next;
    }

    function setNext($nxt)
    {
        $this->next = $nxt;
    }

    function shout($shout, $nth)
    {
        if ($shout < $nth)
        {
            return $shout + 1;
        }
        $this->getPrev()->setNext($this->getNext());
        $this->getNext()->setPrev($this->getPrev());
        return 1;
    }
}

class Chain
{
    var $first;

    function __construct($size)
    {
        $last = null;

        for($i = 0; $i < $size ; $i++)
        {
            $current = new Person($i);
            if ($this->first == null) $this->first = $current;
            if ($last != null)
            {
                $last->setNext($current);
                $current->setPrev($last);
            }
            $last = $current;
        }
        $this->first->setPrev($last);
        $last->setNext($this->first);
    }

    function kill($nth)
    {
        $current = $this->first;
        $shout = 1;
        while($current->getNext() !== $current)
        {
            $shout =  $current->shout($shout,$nth);
            $current = $current->getNext();
        }
        $this->first = $current;
    }
}

$start = microtime(true);
$ITER = 100000;
for($i = 0 ; $i < $ITER ; $i++)
{
    $chain = new Chain(40);
    $chain->kill(3);
}
$end = microtime(true);
printf("Time per iteration = %3.2f microseconds ",(($end -  $start) * 1000000 / $ITER));

Python

import psyco
psyco.full()

class Person(object):
    def __init__(self,count):
        self.count = count;
        self.prev = None 

        self.next = None
    def shout(self,shout,deadif):
        if (shout < deadif): return (shout + 1)
        self.prev.next = self.next
        self.next.prev = self.prev
        return 1

class Chain(object):
    def __init__(self,size):
        self.first = None
        last = None
        for i in range(size):
            current = Person(i)
            if self.first == None : self.first = current
            if last != None :
                last.next = current
                current.prev = last
            last = current
        self.first.prev = last
        last.next = self.first
    def kill(self,nth):
        current = self.first
        shout = 1
        while current.next != current:
            shout = current.shout(shout,nth)
            current = current.next
        self.first = current
        return current

import time
ITER = 100000
start = time.time()
for i in range(ITER):
    chain = Chain(40)
    chain.kill(3)
end = time.time()
print 'Time per iteration = %s microseconds ' % ((end - start) * 1000000 / ITER)

Ruby

class Person
    attr_reader :count, :prev, :next
    attr_writer :count, :prev, :next

    def initialize(count)
        #puts 'Initializing person : ' + count.to_s()
        @count = count
        @prev = nil
        @next = nil
    end

    def shout(shout, deadif)
        if shout < deadif
            return shout + 1
        end
        @prev.next = @next
        @next.prev = @prev
        return 1
    end
end      

class Chain
    attr_reader :first
    attr_writer :first

    def initialize(size)
        @first = nil
        last = nil
        for i in (1..size)
            current = Person.new(i)
            if @first == nil
                @first = current
            end
            if last != nil
                last.next = current
                current.prev = last
            end
            last = current
        end
        @first.prev = last
        last.next = @first
    end

    def kill(nth)
        current = @first
        shout = 1
        while current.next != current
            shout = current.shout(shout,nth)
            current = current.next
        end
        @first = current
        return current
    end
end

ITER=100000
start = Time.now
ITER.times { |i|
chain = Chain.new(40)
chain.kill(3)
}
ends = Time.now
puts 'Time per iteration = ' + ((ends - start) * 1000000 / ITER).to_s() + " microseconds"

C++

[/cpp]
#include <stdio .h>
#include <stdlib .h>
#include <time .h>
#include <sys /time.h>

class Person
{

    public:

        Person(int count) : _next(NULL), _prev(NULL) { _count = count; }
        int shout(int shout, int nth)
        {
            if (shout < nth) return (shout + 1);
            _prev->_next = _next;

            _next->_prev = _prev;
            return 1;
        }
        int count() { return _count; }
        Person* next() { return _next; }
        void next(Person* person) { this->_next = person ; }
        Person* prev() { return _prev; }
        void prev(Person* person) { this->_prev = person; }
    private:
        int _count;
        Person* _next;
        Person* _prev;
};

class Chain
{
    public:
        Chain(int size) : _first(NULL)
        {
            Person* current = NULL;
            Person* last = NULL;
            for(int i = 0 ; i < size ; i++)
            {
                current = new Person(i);
                if (_first == NULL) _first = current;
                if (last != NULL)
                {
                    last->next(current);
                    current->prev(last);
                }
                last = current;
            }
            _first->prev(last);
            last->next(_first);
        }
        Person* kill(int nth)
        {
            Person* current = _first;
            int shout = 1;
            while(current->next() != current)

            {
                Person* tmp = current;
                shout = current->shout(shout,nth);
                current = current->next();
                if (shout == 1)
                {
                    delete tmp;
                }
            }
            _first = current;
            return current;
        }
        Person* first() { return _first; }
    private:
        Person* _first;
};

int main(int argc, char** argv)
{
    int ITER = 1000000;
    Chain* chain;
    struct timeval start, end;
    gettimeofday(&start,NULL);
    for(int i = 0 ; i < ITER ; i++)
    {
        chain = new Chain(40);
        chain->kill(3);

        delete chain;
    }
    gettimeofday(&end,NULL);
    fprintf(stdout,"Time per iteration = %d microseconds ", (((end.tv_sec - start.tv_sec) * 1000000) + (end.tv_usec - start.tv_usec)) / ITER);
    return 0;
}
[cpp]

Java

public class Chain
{
	private Person first = null;

	public Chain(int size)
	{
		Person last = null;
		Person current = null;
		for (int i = 0 ; i < size ; i++)
		{
			current = new Person(i);
			if (first == null) first = current;
			if (last != null)
			{
				last.setNext(current);
				current.setPrev(last);
			}
			last = current;
		}
		first.setPrev(last);
		last.setNext(first);
	}

	public Person kill(int nth)
	{
		Person current = first;
		int shout = 1;
		while(current.getNext() != current)
		{
			shout = current.shout(shout, nth);
			current = current.getNext();
		}
		first = current;
		return current;
	}

	public Person getFirst()
	{
		return first;
	}
	public static void main(String[] args)
	{
		int ITER = 100000;
		long start = System.nanoTime();
		for (int i = 0 ; i < ITER ; i++)
		{
			Chain chain = new Chain(40);
			chain.kill(3);
		}
		long end = System.nanoTime();
		System.out.println("Time per iteration = " + ((end - start) / (ITER )) + " nanoseconds.");
	}
}

class Person
{
	int count;
	private Person prev = null;
	private Person next = null;

	public Person(int count)
	{
		this.count = count;
	}

	public int shout(int shout, int deadif)
	{
		if (shout < deadif) return (shout + 1);
		this.getPrev().setNext(this.getNext());
		this.getNext().setPrev(this.getPrev());
		return 1;
	}

	public int getCount()
	{
		return this.count;
	}

	public Person getPrev()
	{
		return prev;
	}

	public void setPrev(Person prev)
	{
		this.prev = prev;
	}

	public Person getNext()
	{
		return next;
	}

	public void setNext(Person next)

	{
		this.next = next;
	}
}



Комментарии [12] - на пост “PHP vs. Python vs. Ruby vs. Groovy”

  1. Зверёк

    Касательно сравнения LoC, то для языка, который я знаю хорошо (Ruby) — такое ощущение, что писавший код либо не очень хорошо его знает, либо злонамеренно :) удлиннял код, чтобы Python смотрелся выигрышно.

    Т.е. в той части, которая про syntactic sugar и verbosity я бы этим результатам не стал доверять :)

  2. Сергей Гоцуляк

    Бесспорно, что специалист по тому или иному языку перепишет соответствующий скрипт, сделав его и более коротким, и более производительным :-)

    Здесь интерес скорее в том, чтобы оценить языки с высоты птичьего полета, посмотрев на более-менее аналогичный код, который можно понять, не погружаясь в хитрые специфические идиоматические конструкции, которые присутствуют в каждом языке.

    Хотя следующим шагом теста логично было бы сравнить как раз оптимизированные специалистами программы, в этом случае наверняка размеры сильно уменьшатся, а скорость всех скриптов возрастет раза в два.

  3. Зверёк

    Тогда раздел «SLoC, Verbosity, Syntactic Sugar» не имеет смысла вообще.

  4. Дмитрий

    В приведенном оригинале статьи (с исходниками тестов) один из пользователей привел код на Perl, который значительно выиграл по скорости у Python и Ruby. Почему вы не включили в статью результат на Perl, если он явно быстрее чем Python?

  5. Vasay

    Если Вы приводите результаты тестирования питона с Psyco, то было бы честно привести ркзультаты тестирования:

    jRuby (по некоторым данным всреднем в 10 раз быстрее Ruby 1.8.7, хотя и жрет много памяти)
    Groovy++ (по некоторым данным уступает Java 2-3 раза)
    Да ускорителей PHP хватает.

  6. Andrey A.

    Из всего исследования можно сделать только один вывод: когда пишешь велосипед, его КПД (производительность, LoC и прочее) зависит только от случая (в данном случае – от интерпретатора/транслятора). Не берусь говорить про все языки, скажу только про C++: то что Вы написали, это не C++. Это первая лабораторная работа студента-первокурсника. Причём, студент-первокурсник, очевидно, изучает Паскаль, а не C++.

    В C++ есть такая практика как применение Boost (в Boost есть готовый интрузивный список – улучшает метрику LoC). Есть такая практика как аллокаторы (если заранее известно с каким количеством каких объектов мы будем работать, с памятью можно работать более вменяемо – повышает производительность). Ваше дёрганье new по поводу и без – это вообще полнейший ахтунг.

    Вы бы ещё, для пущей C++ности std::cout использовали вместо printf(), чтобы показать как вывод на консоль медленно работает.

    Не стоит Вам говорить о языках программирования, а тем более сравнивать их, если Вы не удосужились познакомиться с ними глубже if-then.

  7. Kanaris

    Интересно, очень инетересно. Особенно интересен отрыв php – 1200 :) Теперь понятно, почему люди требуют знание ruby для высоко нагруженных проектов

  8. Владимир

    Для меня, как знающего языки C# и Java, Groovy гораздо выразительнее, понятнее и лаконичнее чем Python. Я с питоном мало знаком. В синтаксисе питона не понравились подчеркивания, особенно стрелочки >>> и много слов вместо символов. http://groovy.codehaus.org/Differences+from+Python
    У меня правильно отформатированный код груви (убрал лишее) занял 50 строк. Так что питон в этом плане ни сколько не обгоняет груви. Я не критикую питон, скорее оправдываю груви. :)
    После Java на груви писать раза в два короче, если не больше. Нравятся свойства, коллекции, замыкания. Про DSL вообще молчу. Минус груви как и плюс – это Java – потребляет много памяти.

  9. melco

    Вообще, если рассматривать по чесноку, то код для groovy с минимальными модификациями и использованием groovy++ исполняется в ~40 раз быстрее :)
    Код смотрите https://github.com/alextkachman/groovypp/blob/master/Examples/src/Iterations.groovy
    У меня с закоментированным @Typed “Total time = 162.4111341 microseconds”, а если раскоментировать – получим “Total time = 4.3760887 microseconds”
    Ощущаем разницу. Из всех приведенных в тесте Groovy самый быстрый

  10. Йн

    Perl и не надо включать.
    Достаточно сложные программы, а не тесты уровня “Hello, world” на Perl программеру писать сложнее. А упомянутые языки тем и хороши, что позволяют создавать сложные проекты не шибко квалифицированными программистами.

  11. toto

    Рекомендую посмотреть в сторону Rubinius. Еще одна имплементация руби.
    Я прогнал тест на последнем development preview бранче, 2.0pre.
    Результаты в среднем 53 микросекунд за итерацию. Думаю весьмы переспективная имплементация.

  12. Ян Крик

    Злые какие-то тут все программисты. Спасибо Сергей за статью и проделанный труд, я думаю в штатах где вы находитесь люди более приятные и ценят друг друга. У нас так и осталось как в СССР. Все делают друг другу гадости, вместо того, чтобы улыбнуться и похвалить. Читал на одном американском форуме наших эмигрантов. В штатах, радуется когда сосед прибавку получил, у нас же, когда у соседа машина сломалась. Ну лан. А теперь по теме.

    Ваша статья, мне помогла немного разобраться в языках, посмотреть на них глазами обычного пользователя. Я начинающий программист. Раньше изучал С++, потом Java, сейчас учу С# и смотрю в область веб языков программирования. Заинтересовался Руби и Питоном больше всего. Есть также в приоритетах уехать в дальнейшем в штаты. Будьте добры, посоветуйте, какой язык из этих наиболее востребован, наиболее престижный и на каком из них написан какой-то крупный проект вроде фэйсбук, слышал что твитер написан на рубе, а ютуб на питоне, может только слухи.

    Еще раз спасибо за статью, поставил ссылку в своем блоге. Удачи.

Ваше мнение?