Как известно в Perl не очень удобная поддержка объектно-ориентированного программирования. Если хочется программировать с классами, то многое приходится делать вручную. Однако у Perl'а есть очень богатые возможности расширения, поэтому со временем появилось много библиотек (пакетов) обеспечивающих поддержку классов, методов и свойств с синтаксисом различной степени удобности. Но как оказалось, эти пакеты проигрывают в производительности по сравнению с ручной реализацией конструкций ООП. Т.е. с одной стороны, их приятно использовать, а с другой, они делают код медленнее. Мне всегда хотелось узнать насколько медленее становится код, и какие из этих пакетов стоит применять, а какие нет. Поэтому я решил провести небольшое исследование.
Для начала проведем небольшой обзор имеющихся пакетов ООП для Perl'а.
Безо всяких пакетов класс на Perl'е можно создать так:
После чего такой класс можно использовать следующим образом:
В основы пакета Moose легли идеи из Perl6. С его помощью создавать классы на порядок проще. Предыдущий пример, переписанный с помощью Moose, будет выглядеть гораздо лучше:
Как видно, уже не нужно явно создавать метод new, а свойства теперь описываются гораздо легче и естественнее. Но еще не все гладко. Нам приходится вручную разпаковывать параметр $self в каждом методе класса. И вообще методы остались просто перловскими процедурами. Хотелось бы, чтобы они выглядели больше похожими на нормальные методы.
И тогда нам на помощь приходит этот замечательный пакет: Method::Signatures. Советую почитать по нему документацию — там много приятных неожиданностей.
С помощью него наш код становится еще лучше:
Пытливые умы адептов Perl'а на этом не остановились, а пошли гораздо дальше и написали MooseX::Declare. Поистине замечательный пакет, если бы не один недостаток (о котором позднее). Теперь мы можем достичь полной нирваны в описании нашего класса Dog:
Perl позволяет достичь почти полной естественности в описании классов. Но не стоит радоваться. Оказывается эта естественность приводит к потере производительности. Чтобы выяснить насколько, сравним такие характеристики как:
И так приступим. Для тестирования будет использоваться пакет Benchmark. В результатах тестов обозначения следующие:
Как видно, создание объектов с помощью всех ООП пакетов почти в 4 раза медленнее, чем на чистом Perl'е. Но есть и одина хорошая новость: операция создания объекта с использованием Moose и MooseX::Declare происходит за одинаковое время. Поэтому если использовать один из этих пакетов, то предпочтение нужно отдать MooseX::Declare.
Перейдем к следующему пункту, вызову метода. Здесь картина не столь замечательна:
MooseX проигрывает Moose (с Signatures или без) в 16 раз! Это конечно большой сюрприз. Если порыться в интернете, то можно обнаружить, что причина такой большой разницы в производительности, пакет MooseX::Method::Signatures. Поэтому, пока ситуация не исправится в лучшую сторону, MooseX, такой какой он есть, использовать нельзя.
Однако, хорошая новость заключается в том, что Moose + Method::Signatures медленнее чистого Perl'а всего на 20%. Поэтому можно смело использовать эти пакеты без какой либо существенной потери производительности.
Перейдем к последнему пункту тестов, доступ к свойству объекта:
Опять, проигрыш всех пакетов по сравнению с чистым Perl'ом только 20%. Как видно, здесь MooseX оказался не хуже остальных пакетов. Если бы только не MooseX::Method::Signatures…
Очень хочется использовать MooseX, но он сильно колется. Попробуем подкорректировать его, чтобы он был хоть немного получше при вызове методов.
Первое, что приходит в голову, использовать Method::Signatures вместе с MooseX::Declare. Перепишем код Dog следующим образом:
Этот подход работает, но приводит к некрасивому предупреждению:
Дело в том, что method является простой функцией Perl'а, которую определяют как MooseX::Method::Signatures, так и Method::Signatures. Но определяют с разными прототипами, откуда и возникает предупреждение. К сожалению, я не нашел, как это предупреждение убрать. Поэтому пришлось придумать другое решение.
Что может быть проще! Method::Signatures::Simple — это упрощенная версия Method::Signatures, которая обладает одним хорошим свойством. В ней можно задать свое имя для функции method, таким образом избежав конфликта с MooseX::Method::Signatures.
С использованием этого пакета Dog будет выглядеть так:
Разница в том, что вместо method мы теперь пишем def (как в Python).
Посмотрим, улучшилась ли производительность нашего кода (moosex_sig):
Наши ожидания оправдались. MooseX::Declare + Method::Signature::Simple дает такую же производительность как и Moose + Method::Signatures.
Несмотря на все богатство выбора, альтернатива вырисовывается всего одна. На сегодняшний день лучшей с моей точки зрения рекомендацией для использования ООП в Perl является комбинация пакетов MooseX::Declare и Method::Signatures::Simple. Она позволяет описывать классы естественным образом и имеет не очень большую, можно даже сказать, вполне приемлимую цену по производительности.
Добавление: исходный код для тестов доступен на github: github.com/alexeiz/oopbench
Обзор пакетов ООП
Для начала проведем небольшой обзор имеющихся пакетов ООП для Perl'а.
Просто Perl
Безо всяких пакетов класс на Perl'е можно создать так:
package Dog;
sub new {
my ($class, %self) = @_;
bless \%self, $class;
return \%self;
}
sub make_noise {
my $self = shift;
say $self->{name}, " says: ruff-ruff!";
}
1;
После чего такой класс можно использовать следующим образом:
use Dog;
my $dog = Dog->new(name => "Snoopy");
$dog->make_noise();
Moose
В основы пакета Moose легли идеи из Perl6. С его помощью создавать классы на порядок проще. Предыдущий пример, переписанный с помощью Moose, будет выглядеть гораздо лучше:
package Dog;
use Moose;
has name => (is => 'ro', isa => 'Str');
sub make_noise {
my $self = shift;
say $self->name(), " says: ruff-ruff!";
}
__PACKAGE__->meta->make_immutable;
Как видно, уже не нужно явно создавать метод new, а свойства теперь описываются гораздо легче и естественнее. Но еще не все гладко. Нам приходится вручную разпаковывать параметр $self в каждом методе класса. И вообще методы остались просто перловскими процедурами. Хотелось бы, чтобы они выглядели больше похожими на нормальные методы.
Method::Signatures
И тогда нам на помощь приходит этот замечательный пакет: Method::Signatures. Советую почитать по нему документацию — там много приятных неожиданностей.
С помощью него наш код становится еще лучше:
package Dog;
use Moose;
use Method::Signatures;
has name => (is => 'ro', isa => 'Str');
method make_noise() {
say $self->name(), " says: ruff-ruff!";
}
__PACKAGE__->meta->make_immutable;
MooseX::Declare
Пытливые умы адептов Perl'а на этом не остановились, а пошли гораздо дальше и написали MooseX::Declare. Поистине замечательный пакет, если бы не один недостаток (о котором позднее). Теперь мы можем достичь полной нирваны в описании нашего класса Dog:
use MooseX::Declare;
class Dog {
has name => (is => 'ro', isa => 'Str');
method make_noise() {
say $self->name(), " says: ruff-ruff!";
}
}
Производительность пакетов ООП
Perl позволяет достичь почти полной естественности в описании классов. Но не стоит радоваться. Оказывается эта естественность приводит к потере производительности. Чтобы выяснить насколько, сравним такие характеристики как:
- Создание объекта класса.
- Вызов метода.
- Доступ к свойству.
И так приступим. Для тестирования будет использоваться пакет Benchmark. В результатах тестов обозначения следующие:
- vanilla — чистый Perl без дополнительных пакетов,
- moose — с пакетом Moose,
- moose_sig — с пакетоми Moose и Method::Signatures,
- moosex — с пакетом MooseX.
Создание объекта
Rate moose_sig moose moosex vanilla moose_sig 104102/s -- -0% -0% -74% moose 104383/s 0% -- -0% -74% moosex 104592/s 0% 0% -- -74% vanilla 401924/s 286% 285% 284% --
Как видно, создание объектов с помощью всех ООП пакетов почти в 4 раза медленнее, чем на чистом Perl'е. Но есть и одина хорошая новость: операция создания объекта с использованием Moose и MooseX::Declare происходит за одинаковое время. Поэтому если использовать один из этих пакетов, то предпочтение нужно отдать MooseX::Declare.
Вызов метода
Перейдем к следующему пункту, вызову метода. Здесь картина не столь замечательна:
Rate moosex moose moose_sig vanilla moosex 8596/s -- -94% -94% -95% moose 148055/s 1622% -- -0% -17% moose_sig 148516/s 1628% 0% -- -17% vanilla 178254/s 1974% 20% 20% --
MooseX проигрывает Moose (с Signatures или без) в 16 раз! Это конечно большой сюрприз. Если порыться в интернете, то можно обнаружить, что причина такой большой разницы в производительности, пакет MooseX::Method::Signatures. Поэтому, пока ситуация не исправится в лучшую сторону, MooseX, такой какой он есть, использовать нельзя.
Однако, хорошая новость заключается в том, что Moose + Method::Signatures медленнее чистого Perl'а всего на 20%. Поэтому можно смело использовать эти пакеты без какой либо существенной потери производительности.
Доступ к свойству
Перейдем к последнему пункту тестов, доступ к свойству объекта:
Rate moosex moose_sig moose vanilla moosex 1503608/s -- -1% -1% -17% moose_sig 1517928/s 1% -- -0% -16% moose 1517928/s 1% 0% -- -16% vanilla 1815063/s 21% 20% 20% --
Опять, проигрыш всех пакетов по сравнению с чистым Perl'ом только 20%. Как видно, здесь MooseX оказался не хуже остальных пакетов. Если бы только не MooseX::Method::Signatures…
Исправляем MooseX
Очень хочется использовать MooseX, но он сильно колется. Попробуем подкорректировать его, чтобы он был хоть немного получше при вызове методов.
Первое, что приходит в голову, использовать Method::Signatures вместе с MooseX::Declare. Перепишем код Dog следующим образом:
use MooseX::Declare;
class Dog {
use Method::Signatures;
has name => (is => 'ro', isa => 'Str');
method make_noise() {
say $self->name(), " says: ruff-ruff!";
}
}
Этот подход работает, но приводит к некрасивому предупреждению:
Subroutine Dog::method redefined at /opt/local/lib/perl5/site_perl/5.14.1/darwin-thread-multi-2level/Devel/Declare/MethodInstaller/Simple.pm line 17. Prototype mismatch: sub Dog::method: none vs (&) at /opt/local/lib/perl5/site_perl/5.14.1/darwin-thread-multi-2level/Devel/Declare/MethodInstaller/Simple.pm line 17.
Дело в том, что method является простой функцией Perl'а, которую определяют как MooseX::Method::Signatures, так и Method::Signatures. Но определяют с разными прототипами, откуда и возникает предупреждение. К сожалению, я не нашел, как это предупреждение убрать. Поэтому пришлось придумать другое решение.
Method::Signatures::Simple
Что может быть проще! Method::Signatures::Simple — это упрощенная версия Method::Signatures, которая обладает одним хорошим свойством. В ней можно задать свое имя для функции method, таким образом избежав конфликта с MooseX::Method::Signatures.
С использованием этого пакета Dog будет выглядеть так:
use MooseX::Declare;
class Dog {
use Method::Signatures::Simple name => 'def';
has name => (is => 'ro', isa => 'Str');
def make_noise() {
say $self->name(), " says: ruff-ruff!";
}
}
Разница в том, что вместо method мы теперь пишем def (как в Python).
Посмотрим, улучшилась ли производительность нашего кода (moosex_sig):
Rate moosex moosex_sig moose_sig moose vanilla moosex 8651/s -- -94% -94% -94% -95% moosex_sig 146152/s 1589% -- -1% -3% -18% moose_sig 148025/s 1611% 1% -- -2% -17% moose 150866/s 1644% 3% 2% -- -16% vanilla 179200/s 1971% 23% 21% 19% --
Наши ожидания оправдались. MooseX::Declare + Method::Signature::Simple дает такую же производительность как и Moose + Method::Signatures.
Вывод
Несмотря на все богатство выбора, альтернатива вырисовывается всего одна. На сегодняшний день лучшей с моей точки зрения рекомендацией для использования ООП в Perl является комбинация пакетов MooseX::Declare и Method::Signatures::Simple. Она позволяет описывать классы естественным образом и имеет не очень большую, можно даже сказать, вполне приемлимую цену по производительности.
Добавление: исходный код для тестов доступен на github: github.com/alexeiz/oopbench