Почему замена /// в Perl фиктивной функцией с использованием Inline::C вызывает значительное замедление?
У меня есть массив строк около 100000 элементов. Мне нужно перебрать каждый элемент и заменить некоторые слова другими словами. Это займет несколько секунд в чистом Perl. Мне нужно как можно быстрее ускорить это. Я тестирую, используя следующий фрагмент:
use strict;
my $string = "This is some string. Its only purpose is for testing.";
for( my $i = 1; $i < 100000; $i++ ) {
$string =~ s/old1/new1/ig;
$string =~ s/old2/new2/ig;
$string =~ s/old3/new3/ig;
$string =~ s/old4/new4/ig;
$string =~ s/old5/new5/ig;
}
Я знаю, что это на самом деле ничего не заменяет в тестовой строке, но только для скоростного тестирования.
Я надеялся на Inline:: C. Я никогда не работал с Inline::C
до, но после прочтения немного, я подумал, что это довольно просто реализовать. Но, очевидно, даже вызов функции-заглушки, которая ничего не делает, происходит намного медленнее. Вот фрагмент кода, с которым я тестировал:
use strict;
use Benchmark qw ( timethese );
use Inline 'C';
timethese(
5,
{
"Pure Perl" => \&pure_perl,
"Inline C" => \&inline_c
}
);
sub pure_perl {
my $string = "This is some string. Its only purpose is for testing.";
for( my $i = 1; $i < 1000000; $i++ ) {
$string =~ s/old1/new1/ig;
$string =~ s/old2/new2/ig;
$string =~ s/old3/new3/ig;
$string =~ s/old4/new4/ig;
$string =~ s/old5/new5/ig;
}
}
sub inline_c {
my $string = "This is some string. Its only purpose is for testing.";
for( my $i = 1; $i < 1000000; $i++ ) {
$string = findreplace( $string, "old1", "new1" );
$string = findreplace( $string, "old2", "new2" );
$string = findreplace( $string, "old3", "new3" );
$string = findreplace( $string, "old4", "new4" );
$string = findreplace( $string, "old5", "new5" );
}
}
__DATA__
__C__
char *
findreplace( char *text, char *what, char *with ) {
return text;
}
на моем Linux-компьютере результат:
Benchmark: timing 5 iterations of Inline C, Pure Perl...
Inline C: 6 wallclock secs ( 5.51 usr + 0.02 sys = 5.53 CPU) @ 0.90/s (n=5)
Pure Perl: 2 wallclock secs ( 2.51 usr + 0.00 sys = 2.51 CPU) @ 1.99/s (n=5)
Чистый Perl в два раза быстрее вызова пустой C-функции. Совсем не то, что я ожидал! Опять же, я никогда раньше не работал с Inline:: C, так что, может быть, я что-то здесь упускаю?
1 ответ
В версии, использующей Inline::C
вы сохранили все, что было в оригинальном чистом скрипте Perl, и изменили только одно: кроме того, вы заменили высоко оптимизированный Perl s///
с худшей реализацией. Вызов вашей фиктивной функции на самом деле включает в себя работу, в то время как ни один из s///
вызовы делают много в этом случае. Это априори невозможно для Inline::C
Версия для запуска быстрее.
На стороне С, функция
char *
findreplace( char *text, char *what, char *with ) {
return text;
}
не является функцией "ничего не делать". Вызов этого включает распаковку аргументов. Строка, на которую указывает text
должен быть скопирован в возвращаемое значение. Есть некоторые накладные расходы, которые вы платите за каждый вызов.
При условии s///
не заменяет, в этом нет никакого копирования. Кроме того, Perl's s///
высоко оптимизирован. Вы уверены, что можете написать лучшую функцию поиска и замены, которая быстрее компенсирует накладные расходы на вызов внешней функции?
Если вы используете следующую реализацию, вы должны получить сопоставимые скорости:
sub inline_c {
my $string = "This is some string. It's only purpose is for testing.";
for( my $i = 1; $i < 1000000; $i++ ) {
findreplace( $string );
findreplace( $string );
findreplace( $string );
findreplace( $string );
findreplace( $string );
}
}
__END__
__C__
void findreplace( char *text ) {
return;
}
Тест: время 5 итераций Inline C, Pure Perl... Inline C: 6 сек. С тайм-аутом ( 5,69 usr + 0,00 сис = 5,69 CPU) @ 0,88/ с (n=5) Чистый Perl: 6 сек настенных часов ( 5,70 usr + 0,00 sys = 5,70 CPU) @ 0,88/ с (n=5)
Единственная возможность набрать скорость - использовать любую специальную структуру, связанную с шаблоном поиска и заменами, и написать что-то для реализации этого.
На стороне Perl вы должны хотя бы предварительно скомпилировать шаблоны.
Кроме того, поскольку ваша проблема смущающе параллельна, вам лучше разбить работу на столько кусков, сколько у вас есть ядер для работы.
Например, взгляните на записи Perl в задаче regex-redux в Benchmarks Game:
Perl # 4 (только форк): 14,13 секунды
а также
Perl #3 (ветка и темы): 14,47 секунды
против
Perl # 1: 34,01 секунды
То есть некоторое примитивное использование возможностей распараллеливания приводит к ускорению на 60%. Эта проблема не совсем сопоставима, потому что замены должны выполняться последовательно, но все же дает вам представление.
Если у вас восемь ядер, распределите работу до восьми ядер.
Также рассмотрим следующий скрипт:
#!/usr/bin/env perl
use warnings;
use strict;
use Data::Fake::Text;
use List::Util qw( sum );
use Time::HiRes qw( time );
use constant INPUT_SIZE => $ARGV[0] // 1_000_000;
run();
sub run {
my @substitutions = (
sub { s/dolor/new1/ig },
sub { s/fuga/new2/ig },
sub { s/facilis/new3/ig },
sub { s/tempo/new4/ig },
sub { s/magni/new5/ig },
);
my @times;
for (1 .. 5) {
my $data = read_input();
my $t0 = time;
find_and_replace($data, \@substitutions);
push @times, time - $t0;
}
printf "%.4f\n", sum(@times)/@times;
return;
}
sub find_and_replace {
my $data = shift;
my $substitutions = shift;
for ( @$data ) {
for my $s ( @$substitutions ) {
$s->();
}
}
return;
}
{
my @input;
sub read_input {
@input
or @input = map fake_sentences(1)->(), 1 .. INPUT_SIZE;
return [ @input ];
}
}
В этом случае каждый вызов find_and_replace
занимает около 2,3 секунд моего ноутбука. Пять повторов выполняются примерно за 30 секунд. Накладные расходы представляют собой совокупную стоимость генерации набора данных из 1 000 000 предложений и его четырехкратного копирования.