Perl6:处理超大文件的最佳方法是什么?

时间:2018-08-24 13:03:35

标签: performance parsing grammar perl6 fasta

上周,我决定尝试一下Perl6,并开始重新实现我的程序之一。 我不得不说,Perl6是如此容易进行对象编程,这在Perl5中让我非常痛苦。

我的程序必须读取和存储大文件,例如整个基因组(最大3 Gb和更多,请参见下面的示例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秒。这种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,我发现了语法的魔力。因此,无论使用哪种fasta格式,新版本的执行时间均为45秒。不是最快的方法,而是更优雅,更稳定。

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...

编辑 我应用了@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.7s内完成,真是太好了! 我也在小麦基因组(10 Gb)上尝试了此代码。它在35.2秒内完成。 终于Perl6没那么慢!

非常感谢您的帮助!

1 个答案:

答案 0 :(得分:5)

一个简单的改进是使用诸如latin1之类的固定宽度编码来加快字符解码的速度,尽管我不确定这样做有多大帮助。

就Rakudo的正则表达式/语法引擎而言,我发现它的运行速度很慢,因此确实有必要采用更底层的方法。

我没有进行任何基准测试,但是我首先尝试的是这样的事情:

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

但是,如果所使用的标准库的某些部分表现出一定的性能,那么实际上可能会使情况变得更糟。在这种情况下,下一步是添加低级类型注释,例如strint,并用NQP builtins替换对诸如.index之类的例程的调用,例如{ nqp::index

如果这仍然太慢,则说明您不走运,需要切换语言,例如使用Inline::Perl5调用Perl5或使用NativeCall的C语言。


请注意,@ timotimo已进行了一些性能评估,并为此写了an article

如果我的简短版本是基准版本,则命令版本可以将性能提高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倍,但是鉴于潜在的可移植性问题,通常不鼓励编写此类代码,但是如果您确实需要这种性能水平,则现在可能有必要: / p>

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