Последнее время я стал исследовать вопросы производительности и эффективности 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>. Есть ли способ сделать его покрасивее?