当前位置: 首页 > 知识库问答 >
问题:

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

贺佑运
2023-03-14

上周,我决定尝试Perl6,并开始重新实现我的一个程序。我不得不说,Perl6对于对象编程来说非常容易,这对我来说是Perl5非常痛苦的一个方面。

我的程序必须读取和存储大文件,如整个基因组(高达3 Gb或更多,见下面的例子1)或制表数据。

代码的第一个版本是通过逐行迭代以Perl5的方式制作的(“基因组.fa”。对于正确的执行时间,它非常慢且不可行。

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 split。这样,我设法在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秒的执行时间,无论使用哪种快速格式。不是最快的方法,但更优雅和稳定。

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的最高性能

感谢阅读!

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

非常感谢您的帮助!

共有1个答案

轩辕实
2023-03-14

一个简单的改进是使用固定宽度的编码(如 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;
}

但是,如果所使用的标准库的某些部分在性能上有所改善,这实际上可能会使情况变得更糟。在这种情况下,下一步是添加低级类型注释,如<code>str与NQP内置,如NQP::index

如果这仍然太慢,你就倒霉了,需要切换语言,例如使用Inline::Perl5调用Perl5或使用NativeCall调用C。

请注意,@timotimo已经做了一些性能测量,并写了一篇关于它的文章。

如果我的短版本是基线,命令式版本将性能提高了2.4倍。

实际上,他通过将简短版本重写为

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);
}
 类似资料:
  • 我有一个超时执行任务的方法。我使用ExecutorServer.submit()获取一个Future对象,然后调用future.get()并超时。这很好,但是我的问题是处理我的任务可能抛出的检查异常的最好方法。下面的代码工作正常,并且保留了被检查的异常,但是如果方法签名中被检查的异常的列表改变了,它看起来非常笨拙并且容易出错。 关于如何解决这个问题的任何建议?我需要以Java 5为目标,但我也很好

  • 问题内容: 我有一个方法可以执行一些超时任务。我使用ExecutorServer.submit()获取Future对象,然后使用超时调用future.get()。这工作正常,但是我的问题是处理可能由我的任务引发的检查异常的最佳方法。以下代码可以正常工作,并保留检查的异常,但是如果方法签名中的检查的异常列表发生更改,则显得非常笨拙且容易中断。 对于如何解决这个问题,有任何的建议吗?我需要以Java

  • 问题内容: 我正在使用Spring批处理下载一个大文件来处理它。场景很简单: 无需保存输入文件数据。 我们可能同时运行多个(相同场景的)作业实例 我正在寻找最佳实践来处理这种情况。 我是否应该创建Tasklet以便在本地下载文件,而不是通过常规步骤开始处理它? 在这种情况下,我需要考虑一些临时文件问题(确保删除了该文件,确保不覆盖其他临时文件,等等。) 另一方面,我可以下载它并将其保留在内存中,但

  • 问题内容: 我有多个3 GB的制表符分隔文件。每个文件中有2000万行。所有行都必须独立处理,任何两行之间都没有关系。我的问题是,什么会更快A.使用以下命令逐行阅读: 还是B.将文件分块读取到内存中并进行处理,例如一次250 MB? 处理不是很复杂,我只是在column1到column2的值中抓取值,等等。可能需要将一些列值加在一起。 我在具有30GB内存的Linux机器上使用python 2.7

  • 因此,我有一个Javascript脚本,它在一个循环中将小的分数相加,它有可能将0.2加到0.1。然后,这个值被输入到另一个函数,但问题是,我需要0.3来精确输入,而不是0.3000000000000004。 什么是最简单的方法,以确保数字是正确和准确的。注意,它可能得到0.25+0.125等,被添加到简单的四舍五入到小数点1不会解决问题。 也有可能添加0.2+0.1000000000000000

  • 我有一个瞬移工作,接受Kafka的主题,通过一堆操作员。我想知道什么是最好的方法来处理中间发生的异常。 假设存在异常,使用并在catch块中输出到,并在调用外部服务以更新另一个相关作业状态的末尾为提供单独的接收器函数 但是,我的问题是,通过这样做,我似乎仍然需要调用并传入一个空值,以便继续到下面的运算符并进入最后一个阶段,在这个阶段,将流入单独的接收器函数。这样做对吗? 另外,我不确定如果不在操作