Почему замена /// в 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 предложений и его четырехкратного копирования.

Другие вопросы по тегам