【问题标题】:Perl6 : What is the best way for dealing with very big files?Perl6:处理非常大的文件的最佳方法是什么?
【发布时间】:2019-01-30 23:24:12
【问题描述】:

上周我决定尝试 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 之后,我改变了文件的一个 slurp,一个我用 for 循环解析的 \n 上的拆分。这样我设法在 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
...

快速示例 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.7秒完成,太棒了! 我还在小麦基因组(10 Gb)上尝试了这个代码。它在 35.2 秒内完成。 Perl6 终于没那么慢了!

非常感谢您的帮助!

【问题讨论】:

  • 您已经在尝试几种不同的机制。如果没有更多关于您正在尝试做的事情的详细信息,将很难回答这个问题。
  • 限制 Larry on P6 speed in general and grammars in particular 从 11 分 30 秒到 17 分 20 秒。 (我很确定他没有将 Fates 从 STD 移植到 NQP。)请描述参见P6 doc & beta of new profiler 使用 P6+P5? 1) P6在 P5 metacpan.org/pod/Inline::Perl6,2) P5 在 P6 github.com/niner/Inline-Perl5改进您的 P5 OO? Stevan 设计了 ​​P6 OO,然后是 Moose 的 P5,现在 Moxie
  • 您的第一个示例可以通过将正则表达式的使用降至零来变得更快;在我的机器上,它从 40 秒下降到 7.6 秒,发生以下变化:$file.IO.slurp.lines 而不是 $file.IO.lines$line.starts-with("&gt;") 而不是 line ~~ /^&gt;/$l = $l.substr(1) 而不是 $l ~~ s:/^&gt;//。另外,从循环中删除“say $id”。如果你真的需要输出,可以在 TWEAK 的末尾或在单独的方法中使用 put %!seq.keys.join("\n")
  • 对于任何想要尝试该脚本的人,这里有一个生成一些示例 fasta 数据的单行代码:my $f = "genome.fa".IO.open(:w); my @ids = ("A".."Z").combinations(20).pick(*); while $f.tell &lt; 300_000_000 { $f.put("&gt;" ~ @ids.shift.join()); $f.put(&lt;A C G T&gt;.roll(80).join()) for ^(2..15).pick }
  • 我不确定它是属于评论还是属于它自己的答案,但这是我写的一篇关于这个问题的博客文章:“Fast FASTA, please”:wakelift.de/2018/08/31/faster-fasta-please

标签: performance parsing grammar fasta raku


【解决方案1】:

一个简单的改进是使用诸如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,并将对例程的调用(例如.index)替换为NQP builtins,例如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 倍,但考虑到潜在的可移植性问题,通常不鼓励编写此类代码,但如果您确实需要该级别的性能,现在可能有必要:

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

【讨论】:

  • 在循环体内使用.skip(1) 代替[1..*].head.skip(1) 可以大大加快您的第一个答案;此外,它需要将.split("\n")“增强”为.split("\n").cache,以便head 和skip 方法对其进行处理。在我的机器上,它从 47 秒缩短到了 12 秒。我还有一些想法,在以后的 cmets 中会有更多的想法,或者可能有自己的答案
  • 对第二个代码的快速分析显示,花费的大部分时间是在..^ Range 构造函数运算符中。使用 $pos, $end - $pos 而不是 $pos ..^ $end 可以让我从 16.2 秒缩短到 8.75 秒,因此时间几乎减半。
  • moarvm 不会自动执行类似于假设 Latin1 编码的事情,直到输入打破该假设?换句话说,对于实际上是 Latin1 的文件,从性能的角度来看,:enc&lt;latin1&gt; 不是在很大程度上或完全是多余的吗?
  • @raiph 它的作用是尝试以每个字素 8 位存储从 utf8 源读取的数据,直到遇到不适合的数据,此时它将变为每个字素 32 位。我确实相信,utf8 解码器比 latin1 解码器投入了更多的优化工作,当我尝试切换编码时,它几乎没有任何区别。
  • 使用原生整数转换版本以使用 nqp 操作(顺便说一句,官方不支持这些操作,使用这些操作的代码会在 rakudo 更改时自发中断)使程序在 2.9 秒内完成,其中 0.34 s 是按时间计算的系统时间,分析器估计大约 18% 的总时间花费在“slurp”本身中。听起来还不错。
猜你喜欢
  • 2014-02-11
  • 1970-01-01
  • 2011-07-28
  • 1970-01-01
  • 2018-12-03
  • 1970-01-01
  • 2020-07-28
  • 2012-04-04
  • 2011-04-29
相关资源
最近更新 更多