Оптимизация Perl-скрипта - слишком медленно работает с файлами размером более 40 ГБ

Я сделал следующий Perl-скрипт для выполнения некоторых манипуляций с файлами на работе, но в данный момент он работает слишком медленно, чтобы его можно было запустить в производство.

Я не очень хорошо знаю Perl (не один из моих языков), поэтому может ли кто-нибудь помочь мне определить и заменить части этого скрипта, которые будут медленными, учитывая, что он обрабатывает ~ 40 миллионов строк?

Данные, передаваемые по конвейеру, имеют формат:

col1|^|col2|^|col3|!|
col1|^|col2|^|col3|!|
... 40 million of these.

Массив date_cols вычисляется перед этой частью скрипта и в основном содержит индекс столбцов, содержащих даты в предварительно преобразованном формате.

Вот часть скрипта, которая будет выполняться для каждой входной строки. Я немного подчистил его и добавил комментарии, но дайте мне знать, если что-то еще понадобится:

## Read from STDIN until no more lines are arailable.
while (<STDIN>)
{       
    ## Split by field delimiter
    my @fields = split('\|\^\|', $_, -1);   

    ## Remove the terminating delimiter from the final field so it doesn't
    ## interfere with date processing.
    $fields[-1] = (split('\|!\|', $fields[-1], -1))[0];

    ## Cycle through all column numbres in date_cols and convert date
    ##  to yyyymmdd
    foreach $col (@date_cols)
    {
        if ($fields[$col] ne "")
        {
            $fields[$col] = formatTime($fields[$col]);
        }
    }

    print(join('This is an unprintable ASCII control code', @fields), "\n");
}           

## Format the input time to yyyymmdd from 'Dec 26 2012 12:00AM' like format.
sub formatTime($)
{
    my $col = shift;        

    if (substr($col, 4, 1) eq " ") {
        substr($col, 4, 1) = "0";
    }       
    return substr($col, 7, 4).$months{substr($col, 0, 3)}.substr($col, 4, 2);
}

person John Humphreys    schedule 26.09.2012    source источник
comment
Вы думали о том, чтобы сначала разбить файл на части, используя что-то вроде csplit?   -  person matchew    schedule 26.09.2012
comment
Как это работает, и сможет ли он собрать их заново, если предположить, что я запустил этот скрипт на всех частях?   -  person John Humphreys    schedule 26.09.2012
comment
Явной неэффективности не вижу. Функция print будет самой медленной из показанных, но я предполагаю, что она предназначена только для целей отладки. Если вы запустите именно этот код (за вычетом print), он все еще будет медленным? Я немного подозреваю, потому что сабвуфер trim нигде не используется.   -  person dan1111    schedule 26.09.2012
comment
Если вы можете разделить файл, вы сможете добавить к нему больше оборудования — запустите его на 4 машинах с 10M записями на каждой. Все зависит от того, насколько легко его разобрать и собрать.   -  person Dan Pichelman    schedule 26.09.2012
comment
Разделение @ dan1111 использовалось раньше, но я решил удалить его из цикла, потому что я не думаю, что это необходимо, и я пытался его ускорить. Забыл удалить саб, мой плохой. Функция печати... Я использую ее, именно так я передаю вывод в файл, содержащий преобразование. Это проблема? Он не отображается на экране.   -  person John Humphreys    schedule 26.09.2012
comment
О, и символ в объединении - \022, но он не отображается, поэтому я просто поместил туда текст, чтобы указать, что это такое.   -  person John Humphreys    schedule 26.09.2012
comment
Вместо второго split вы можете установить $/ (разделитель входных записей) на "|!|\n" вне цикла и chomp после каждого readline/<STDIN>. Это перемещает больше функциональности в библиотеку C (скорость!) и позволяет избежать создания анонимного массива. Попробуйте провести бенчмаркинг.   -  person amon    schedule 26.09.2012
comment
Приведет ли это к тому, что каждое чтение из STDIN будет просто давать части строки одну за другой? Итак, в приведенном выше примере col1, col2 и col3|!| будет 3 отдельных чтения из STDIN?   -  person John Humphreys    schedule 26.09.2012
comment
Нет, это все равно даст всю строку как одну строку, но без последнего разделителя. Я пишу ответ с полным примером.   -  person amon    schedule 26.09.2012
comment
Спасибо :) Я попробую, когда вы опубликуете.   -  person John Humphreys    schedule 26.09.2012
comment
Первое (новое) правило Клуба Оптимизации: мы не обсуждаем оптимизацию, пока не воспользуемся Devel::NYTProf, чтобы понять, в чем наша проблема.   -  person DavidO    schedule 26.09.2012


Ответы (3)


Если бы я писал исключительно для эффективности, я бы написал ваш код так:

sub run_loop {
  local $/ = "|!|\n"; # set the record input terminator
                      # to the record seperator of our problem space
  while (<STDIN>) {       
    # remove the seperator
    chomp;

    # Split by field delimiter
    my @fields = split m/\|\^\|/, $_, -1;

    # Cycle through all column numbres in date_cols and convert date
    #  to yyyymmdd
    foreach $col (@date_cols) {
      if ($fields[$col] ne "") {
        # $fields[$col] = formatTime($fields[$col]);
        my $temp = $fields[$col];
        if (substr($temp, 4, 1) eq " ") {
          substr($temp, 4, 1) = "0";
        }       
        $fields[$col] = substr($temp, 7, 4).$months{substr($temp, 0, 3)}.substr($temp, 4, 2);
      }
    }
    print join("\022", @fields) . "\n";
  }
}

Оптимизации таковы:

  • Использование chomp для удаления строки |!|\n в конце
  • Inlining the formatTime sub.

    Вызовы подпрограмм чрезвычайно дороги в Perl. Если сабвуферы должны использоваться очень эффективно, проверку прототипа можно отключить с помощью синтаксиса &subroutine(@args). Если @args опущены, текущие аргументы @_ видны вызываемой подпрограмме. Это может привести к ошибкам или дополнительной производительности. Используйте с умом. Также можно использовать синтаксис goto &subroutine;, но это мешает return (в основном хвостовой вызов). Не использовать.

Дальнейшая оптимизация может включать удаление поиска хэша %months, так как хэширование дорого.

person amon    schedule 26.09.2012
comment
Я только что запустил этот код с фиктивным вводом (2 поля в строке) на 10 млн итераций. На моем ноутбуке он заканчивается за 11 секунд (с одним столбцом даты) и за 8 секунд при удалении foreach. Это на самом деле вполне нормально и не нуждается в оптимизации. IO является ограничивающим фактором. Решение @pilcros занимает немного больше времени, 18 секунд. [нужна цитата] - person amon; 26.09.2012

Вам нужно будет сравнить свой набор данных для сравнения, но вы можете добавить к нему регулярное выражение. (Еще хуже из-за того, что ваши очень недружественные к регулярным выражениям поля и разделители записей!)

my $i = 0;
our %months = map { $_ => sprintf('%02d', ++$i) } qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);

while (<DATA>) {
  s! \|\^\| !\022!xg;  # convert field separator
  s/ \| !\| $ //xg;        # strip record terminator
  s/\b(\w{3}) ( \d|\d\d) (\d{4}) \d\d:\d\d[AP]M\b/${3} . $months{$1} . sprintf('%02d', $2) /eg;
  print;
}

Не будет делать то, что вы хотите, если одно из полей, отличных от @date_cols, соответствует регулярному выражению даты.

person pilcrow    schedule 26.09.2012
comment
Это удивительно элегантно :) Однако модификатор /x не влияет на пробелы в строке подстановки. Вы должны включить пробелы вокруг разделителя выходного поля, т.е. ·\022·, где точки - это пробелы. Кроме того, не повысит ли эффективность привязка второй замены в конце $? - person amon; 26.09.2012

На моей работе иногда мне нужно собрать журналы ошибок и т. д. из более чем 350 интерфейсов. Я использую шаблон сценария, который я называю «SMP grep»;) Это просто:

  1. stat файл, получить длину файла
  2. Получите «длина чанка» = длина_файла/количество_процессоров
  3. И просто порции начинаются и заканчиваются так, чтобы они начинались/заканчивались на "\n". Просто read() найдите "\n" и рассчитайте смещения.
  4. fork() чтобы сделать num_processor worker'ами, каждый из которых работает над своим чанком

Это может помочь, если вы используете регулярные выражения в своем grep или других операциях ЦП (как я думаю, в вашем случае). Админы жалуются, что этот скрипт потребляет пропускную способность диска, но это единственное узкое место здесь, если сервер имеет 8 процессоров =) Кроме того, очевидно, что если вам нужно проанализировать данные за 1 неделю, вы можете разделить их между серверами.

Завтра могу выложить код, если интересно.

person PSIAlt    schedule 26.09.2012