Последнее время я стал исследовать вопросы производительности и эффективности perl-программ, и появилась идея опубликовать цикл простых, но наглядных тестов-сравнений. Начну с самого простого и типичного — с присваиваний. Если тема интересна — буду продолжать.
Ни одна большая программа не обходится без присваиваний. В программах на perl (да и не только на нем) данные часто хранят в структурах хэшей, и обновление полей этих хэшей — очень типичная задача. Разумеется, в perl существует множество способов написать код для этого — и все они различаются как по читабельности и красоте, так и по скорости. Мне стало интересно, каковы эти различия — и я провел небольшое исследование.
Пусть у нас есть
Можно воспользоваться hash slice — и вместо трех операторов скалярного присваивания достичь того же эффекта одним оператором присваивания списков:
Можно просто присвоить хэши (указав внутри
Наконец, можно воспользоваться циклом (часто это бывает удобно):
Или записать то же самое в виде конструкции map:
Как вы думаете, какой способ самый быстрый? Запустим тест, который сравнит скорость выполнения разных способов обновить поля хэша:
Вот его результаты:
Как видим, самый быстрый способ — прямое присваивание. Hash slice работает в два раза медленнее, конструкции с циклом — еще в 2-3 раза медленнее. Цикл map работает несколько быстрее, чем аналогичный for.
Однако данные можно хранить не только в хэшах, но и в массивах. Аналогичный тест для массива дает аналогичные результаты:
Сравнение проводилось для perl 5.8.8 на трех машинах (десктоп под Windows XP и два сервера под FreeBSD). Конкретные цифры различаются, но соотношение везде примерно такое.
P.S. Как-то некрасиво тут смотрится код, обернутый в <pre>. Есть ли способ сделать его покрасивее?
Ни одна большая программа не обходится без присваиваний. В программах на perl (да и не только на нем) данные часто хранят в структурах хэшей, и обновление полей этих хэшей — очень типичная задача. Разумеется, в perl существует множество способов написать код для этого — и все они различаются как по читабельности и красоте, так и по скорости. Мне стало интересно, каковы эти различия — и я провел небольшое исследование.
Пусть у нас есть
$hash — ссылка на хэш с несколькими полями, и мы хотим обновить три из них. Банальный способ записать это:<font color="gray">$hash->{foo} = 456;
$hash->{bar} = $bar;
$hash->{baz} = 'baz';
</font>Можно воспользоваться hash slice — и вместо трех операторов скалярного присваивания достичь того же эффекта одним оператором присваивания списков:
<font color="gray">@$hash{qw(foo bar baz)} = ( 456, $bar, 'baz' );
</font>Можно просто присвоить хэши (указав внутри
%$hash для того, чтобы сохранились прочие поля):<font color="gray">%$hash = ( %$hash, foo => 456, bar => $bar, baz => 'baz' ); </font>
Наконец, можно воспользоваться циклом (часто это бывает удобно):
<font color="gray">my %h = ( foo => 456, bar => $bar, baz => 'baz' );
$hash->{$_} = $h{$_} for qw(foo bar baz);
</font>Или записать то же самое в виде конструкции map:
<font color="gray">map { $hash->{$_} = $h{$_} } qw(foo bar baz);
</font>Как вы думаете, какой способ самый быстрый? Запустим тест, который сравнит скорость выполнения разных способов обновить поля хэша:
<font color="gray">#!/usr/bin/perl
use strict;
use Benchmark qw(cmpthese);
my $hash = {
foo => 'foo',
bar => 123,
baz => undef,
};
my $bar = 'bar';
cmpthese(
-10,
{
h_direct => sub {
$hash->{foo} = 456;
$hash->{bar} = $bar;
$hash->{baz} = 'baz';
},
h_list => sub {
@$hash{qw(foo bar baz)} = ( 456, $bar, 'baz' );
},
h_hash => sub {
%$hash = ( %$hash, foo => 456, bar => $bar, baz => 'baz' );
},
h_for => sub {
my %h = ( foo => 456, bar => $bar, baz => 'baz' );
$hash->{$_} = $h{$_} for qw(foo bar baz);
},
h_forref => sub {
my $h = { foo => 456, bar => $bar, baz => 'baz' };
$hash->{$_} = $h->{$_} for qw(foo bar baz);
},
h_map => sub {
my %h = ( foo => 456, bar => $bar, baz => 'baz' );
map { $hash->{$_} = $h{$_} } qw(foo bar baz);
},
h_mapref => sub {
my $h = { foo => 456, bar => $bar, baz => 'baz' };
map { $hash->{$_} = $h->{$_} } qw(foo bar baz);
},
} );
</font>Вот его результаты:
Rate h_hash h_forref h_mapref h_for h_map h_list h_direct
h_hash 100913/s -- -30% -42% -43% -53% -80% -91%
h_forref 144297/s 43% -- -17% -19% -32% -71% -88%
h_mapref 174524/s 73% 21% -- -2% -18% -65% -85%
h_for 177449/s 76% 23% 2% -- -17% -65% -85%
h_map 213368/s 111% 48% 22% 20% -- -58% -82%
h_list 505768/s 401% 251% 190% 185% 137% -- -57%
h_direct 1169409/s 1059% 710% 570% 559% 448% 131% --
Как видим, самый быстрый способ — прямое присваивание. Hash slice работает в два раза медленнее, конструкции с циклом — еще в 2-3 раза медленнее. Цикл map работает несколько быстрее, чем аналогичный for.
Однако данные можно хранить не только в хэшах, но и в массивах. Аналогичный тест для массива дает аналогичные результаты:
<font color="gray">#!/usr/bin/perl
use strict;
use Benchmark qw(cmpthese);
my $list = [ 'foo', 123, undef ];
my $bar = 'bar';
cmpthese(
-10,
{
l_direct => sub {
$list->[0] = 456;
$list->[1] = $bar;
$list->[2] = 'baz';
},
l_list => sub {
@$list[ 0 .. 2 ] = ( 456, $bar, 'baz' );
},
l_for => sub {
my @l = ( 456, $bar, 'baz' );
$list->[$_] = $l[$_] for 0 .. 2;
},
l_forref => sub {
my $l = [ 456, $bar, 'baz' ];
$list->[$_] = $l->[$_] for 0 .. 2;
},
l_map => sub {
my @l = ( 456, $bar, 'baz' );
map { $list->[$_] = $l[$_] } 0 .. 2;
},
l_mapref => sub {
my $l = [ 456, $bar, 'baz' ];
map { $list->[$_] = $l->[$_] } 0 .. 2;
},
} );
</font>
Rate l_forref l_for l_mapref l_map l_list l_direct
l_forref 197498/s -- -11% -17% -38% -65% -86%
l_for 222737/s 13% -- -6% -30% -60% -84%
l_mapref 237429/s 20% 7% -- -25% -58% -83%
l_map 318127/s 61% 43% 34% -- -43% -77%
l_list 559192/s 183% 151% 136% 76% -- -60%
l_direct 1395278/s 606% 526% 488% 339% 150% --
Сравнение проводилось для perl 5.8.8 на трех машинах (десктоп под Windows XP и два сервера под FreeBSD). Конкретные цифры различаются, но соотношение везде примерно такое.
P.S. Как-то некрасиво тут смотрится код, обернутый в <pre>. Есть ли способ сделать его покрасивее?
