Perl6: Как лучше всего работать с очень большими файлами? - PullRequest
0 голосов
/ 24 августа 2018

На прошлой неделе я решил попробовать 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 я изменил на ошибку в файле, разделение на \ n, которое я проанализировал с помощью цикла for.Таким образом мне удалось загрузить данные за 2 мин.Намного лучше, но недостаточно.Обманывая, я имею в виду, удалив максимум \ n (Пример 2), я сократил время выполнения до 30 секунд.Неплохо, но не полностью удовлетворено, этот формат не является наиболее используемым.

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 секунд, независимо от используемого формата fastta.Не самый быстрый способ, но более элегантный и стабильный.

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?

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


Fasta Пример 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.2 с.Perl6 не такой медленный, наконец!

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

1 Ответ

0 голосов
/ 25 августа 2018

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

Что касается механизма регулярных выражений / грамматики Ракудо, я обнаружил, что он довольно медленный, поэтому, возможно, действительно необходимо использовать более низкоуровневый подход.

Я не делал никаких тестов, но сначала попробую что-то вроде этого:

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);
}
...