Perl6: Каков наилучший способ работы с очень большими файлами? - программирование
Подтвердить что ты не робот

Perl6: Каков наилучший способ работы с очень большими файлами?

На прошлой неделе я решил попробовать Perl6 и начал переориентировать одну из моих программ. Я должен сказать, что Perl6 - это простое программирование объектов, что очень мучительно для меня в Perl5.

Моя программа должна читать и хранить большие файлы, такие как целые геномы (до 3 ГБ и более, см. Пример 1 ниже) или данные таблицы.

Первая версия кода была сделана в Perl5 путем повторения строки за строкой ("genome.fa".IO.lines). Это было очень медленно и небезопасно для правильного времени выполнения.

my class fasta {
  has Str $.file is required;
  has %!seq;

  submethod TWEAK() {
    my $id;
    my $s;

    for $!file.IO.lines -> $line {
      if $line ~~ /^\>/ {
        say $id;
        if $id.defined {
          %!seq{$id} = sequence.new(id => $id, seq => $s);
        }
        my $l = $line;
        $l ~~ s:g/^\>//;
        $id = $l;
        $s = "";
      }
      else {
        $s ~= $line;
      }
    }
    %!seq{$id} = sequence.new(id => $id, seq => $s);
  }
}


sub MAIN()
{
    my $f = fasta.new(file => "genome.fa");
}

Поэтому после небольшого количества RTFM я изменил для slurp в файле, раскол на \n, который я проанализировал с помощью цикла for. Таким образом мне удалось загрузить данные за 2 мин. Гораздо лучше, но недостаточно. Обманывая, я имею в виду, удалив максимум \n (пример 2), я уменьшил время выполнения до 30 секунд. Довольно хорошо, но не полностью удовлетворенный, этот формат fasta не самый используемый.

my class fasta {
  has Str $.file is required;
  has %!seq;

  submethod TWEAK() {
    my $id;
    my $s;

    say "Slurping ...";
    my $f = $!file.IO.slurp;

    say "Spliting file ...";
    my @lines = $f.split(/\n/);

    say "Parsing lines ...";
    for @lines -> $line {
      if $line !~~ /^\>/ {
          $s ~= $line;
      }
      else {
        say $id;
        if $id.defined {
          %!seq{$id} = seq.new(id => $id, seq => $s);
        }
        $id = $line;
        $id ~~ s:g/^\>//;
        $s = "";
      }
    }
    %!seq{$id} = seq.new(id => $id, seq => $s);
  }
}

sub MAIN()
{
    my $f = fasta.new(file => "genome.fa");
}

Так снова RTFM, и я обнаружил магию грамматики. Так что новая версия и время выполнения 45 секунд, независимо от формата fasta. Не самый быстрый способ, но более элегантный и стабильный.

my grammar fastaGrammar {
  token TOP { <fasta>+ }

  token fasta   {<.ws><header><seq> }
  token header  { <sup><id>\n }
  token sup     { '>' }
  token id      { <[\d\w]>+ }
  token seq     { [<[ACGTNacgtn]>+\n]+ }

}

my class fastaActions {
  method TOP ($/){
    my @seqArray;

    for $<fasta> -> $f {
      @seqArray.push: seq.new(id => $f.<header><id>.made, seq => $f<seq>.made);
    }
    make @seqArray;
  }

  method fasta ($/) { make ~$/; }
  method id    ($/) { make ~$/; }
  method seq   ($/) { make $/.subst("\n", "", :g); }

}

my class fasta {
  has Str $.file is required;
  has %seq;

  submethod TWEAK() {

    say "=> Slurping ...";
    my $f = $!file.IO.slurp;

    say "=> Grammaring ...";
    my @seqArray = fastaGrammar.parse($f, actions => fastaActions).made;

    say "=> Storing data ...";
    for @seqArray -> $s {
      %!seq{$s.id} = $s;
    }
  }
}

sub MAIN()
{
    my $f = fasta.new(file => "genome.fa");
}

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

Будучи новичком в Perl6, мне было бы интересно узнать, есть ли лучшие способы борьбы с большими данными или если есть какое-то ограничение из-за реализации Perl6?

Будучи новичком в Perl6, я задал бы два вопроса:

  • Существуют ли другие механизмы Perl6, которые я еще не знаю или еще не задокументирован, для хранения огромных данных из файла (например, моих геномов)?
  • Достиг ли максимальная производительность для текущей версии Perl6?

Спасибо за прочтение !


Фаста Пример 1:

>2L
CGACAATGCACGACAGAGGAAGCAGAACAGATATTTAGATTGCCTCTCATTTTCTCTCCCATATTATAGGGAGAAATATG
ATCGCGTATGCGAGAGTAGTGCCAACATATTGTGCTCTTTGATTTTTTGGCAACCCAAAATGGTGGCGGATGAACGAGAT
...
>3R
CGACAATGCACGACAGAGGAAGCAGAACAGATATTTAGATTGCCTCTCATTTTCTCTCCCATATTATAGGGAGAAATATG
ATCGCGTATGCGAGAGTAGTGCCAACATATTGTGCTCTTTGATTTTTTGGCAACCCAAAATGGTGGCGGATGAACGAGAT
...

Пример Fasta 2:

>2L
GACAATGCACGACAGAGGAAGCAGAACAGATATTTAGATTGCCTCTCAT...            
>3R
TAGGGAGAAATATGATCGCGTATGCGAGAGTAGTGCCAACATATTGTGCT...

EDIT Я применил советы @Christoph и @timotimo и проверил код:

my class fasta {
  has Str $.file is required;
  has %!seq;

  submethod TWEAK() {
    say "=> Slurping / Parsing / Storing ...";
    %!seq = slurp($!file, :enc<latin1>).split('>').skip(1).map: {
  .head => seq.new(id => .head, seq => .skip(1).join) given .split("\n").cache;
    }
  }
}


sub MAIN()
{
    my $f = fasta.new(file => "genome.fa");
}

Программа закончилась в 2,7 секунды, что так здорово! Я также пробовал этот код на геноме пшеницы (10 Гб). Это закончилось в 35.2s. Perl6 не так медленно!

Большое спасибо за помощь!

4b9b3361

Ответ 1

Одним из простых улучшений является использование кодирования с фиксированной шириной, такого как latin1 для ускорения декодирования символов, хотя я не уверен, насколько это поможет.

Что касается Rakudo regex/grammar engine, я нашел, что он довольно медленный, поэтому действительно может потребоваться использовать более низкий уровень.

Я не проводил бенчмаркинга, но сначала попытался бы попробовать что-то вроде этого:

my %seqs = slurp('genome.fa', :enc<latin1>).split('>')[1..*].map: {
    .[0] => .[1..*].join given .split("\n");
}

Поскольку стандартная библиотека Perl6 реализована в самом Perl6, иногда можно повысить производительность, просто избегая ее, написав код в императивном стиле, например:

my %seqs;
my $data = slurp('genome.fa', :enc<latin1>);
my $pos = 0;
loop {
    $pos = $data.index('>', $pos) // last;

    my $ks = $pos + 1;
    my $ke = $data.index("\n", $ks);

    my $ss = $ke + 1;
    my $se = $data.index('>', $ss) // $data.chars;

    my @lines;

    $pos = $ss;
    while $pos < $se {
        my $end = $data.index("\n", $pos);
        @lines.push($data.substr($pos..^$end));
        $pos = $end + 1
    }

    %seqs{$data.substr($ks..^$ke)} = @lines.join;
}

Однако, если части стандартной используемой библиотеки видели некоторую производительность, это может на самом деле ухудшить ситуацию. В этом случае следующим шагом будет добавление аннотаций типа низкого уровня, таких как str и int и замена вызовов подпрограмм, таких как .index с встроенными функциями NQP, такими как nqp::index.

Если это еще слишком медленно, вам не повезло и вам нужно будет переключать языки, например, звонить в Perl5, используя Inline::Perl5 или C, используя NativeCall.


Обратите внимание, что @timotimo выполнил некоторые измерения производительности и написал статью об этом.

Если моя короткая версия является базовым, настоятельная версия улучшает производительность на 2,4 раза.

На самом деле ему удалось выжать 3-кратное улучшение из короткой версии, переписав его

my %seqs = slurp('genome.fa', :enc<latin-1>).split('>').skip(1).map: {
    .head => .skip(1).join given .split("\n").cache;
}

Наконец, переписывание императивной версии с использованием встроенных контроллеров NQP ускорило ситуацию в 17 раз, но, учитывая потенциальные проблемы с переносимостью, запись такого кода обычно не рекомендуется, но может потребоваться сейчас, если вам действительно нужен такой уровень производительности:

use nqp;

my Mu $seqs := nqp::hash();
my str $data = slurp('genome.fa', :enc<latin1>);
my int $pos = 0;

my str @lines;

loop {
    $pos = nqp::index($data, '>', $pos);

    last if $pos < 0;

    my int $ks = $pos + 1;
    my int $ke = nqp::index($data, "\n", $ks);

    my int $ss = $ke + 1;
    my int $se = nqp::index($data ,'>', $ss);

    if $se < 0 {
        $se = nqp::chars($data);
    }

    $pos = $ss;
    my int $end;

    while $pos < $se {
        $end = nqp::index($data, "\n", $pos);
        nqp::push_s(@lines, nqp::substr($data, $pos, $end - $pos));
        $pos = $end + 1
    }

    nqp::bindkey($seqs, nqp::substr($data, $ks, $ke - $ks), nqp::join("", @lines));
    nqp::setelems(@lines, 0);
}