为什么这个Perl循环遍历字符串然后字符这么慢?

时间:2014-12-18 19:28:29

标签: c string perl

当$ insertPoint为101000且$ records为3167时,以下Perl代码段中的循环运行需要210秒。等效C代码在此机器上运行一秒左右(见下文)。通常当出现运行时间大约200倍的因素时,这意味着正在发生非常低效的事情。

知道它可能在这里吗?

@alignedSeqs是一个字符串数组,长度相同。该机器具有80G的RAM和每个CPU 8M的缓存。据我所知,这段代码通过内存“正确的方式”。

   my $i;
   my $j;
   my @As=(0) x $insertPoint; # possibly faster than an array of arrays?
   my @Gs=(0) x $insertPoint;
   my @Cs=(0) x $insertPoint;
   my @Ts=(0) x $insertPoint;
   my @Is=(0) x $insertPoint;
   for($i=0;$i<$records;++$i){
      for($j=0; $j <$insertPoint; ++$j){
         my $base=uc(substr($alignedSeqs[$i],$j,1));
         if(   $base eq "A"){ $As[$j]++; }
         elsif($base eq "G"){ $Gs[$j]++; }
         elsif($base eq "C"){ $Cs[$j]++; }
         elsif($base eq "T"){ $Ts[$j]++; }
         else{                $Is[$j]++; }
      }
   }

此更改:

  my $aSeq=$alignedSeqs[$i];
  for($j=0; $j <$insertPoint; ++$j){
     my $base=uc(substr($aSeq,$j,1));

在运行时间方面没有显着差异。

问题性质的一个线索可能是此过程运行时“顶部”显示的内存使用情况,该代码在此代码启动之前达到峰值12G以上,并在本节中保留。然后我更改了脚本以显式释放此程序中的其他大型数据结构,并降至10G。假设剩下的10G中的大部分都在对齐数据中,而不是非常有效的存储,因为每个字符只有一个字节,它只需要大约320M来保存这些字符串,或者如果Perl使用4字节unicode则可能需要1.28G。

这是一个小型的C测试程序,足以运用相同的代码。很难用我的手表准确计时,但“计数”部分在这样运行时需要1-2秒:

gcc -std = c99 --pedantic -Wall -o test test.c 时间./test&gt; / dev / null

整个事情在7.1s完成。

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h> //for toupper

// prototypes
void boom(char *string);

int main(void){

int records =   3167;
int slen    = 101000;
char **aligned=NULL;
malloc(sizeof(char *)*slen);
int i,j;
unsigned int k;


   aligned=malloc(sizeof(char *)*slen);
   if(!aligned)boom(" Could not allocate first array");

   fprintf(stderr,"DEBUG:  filling with random data\n");
   for(i=0;i<records;i++){
      aligned[i]=malloc(sizeof(char)*(slen+1));
      if(!aligned[i])boom(" Could not allocate an aligned array");
      for(j=0;j<slen;j++){
         k=rand();
         k -=((k>>10)<<10);
         switch(k){
            case 0:  aligned[i][j]='A'; break;
            case 1:  aligned[i][j]='C'; break;
            case 2:  aligned[i][j]='G'; break;
            case 3:  aligned[i][j]='T'; break;
            default: aligned[i][j]='-'; break;
         }
      } 
   }

   fprintf(stderr,"DEBUG:  allocating space for counts\n");
   int *As=calloc(sizeof(int),slen);
   int *Gs=calloc(sizeof(int),slen);
   int *Cs=calloc(sizeof(int),slen);
   int *Ts=calloc(sizeof(int),slen);
   int *Is=calloc(sizeof(int),slen);
   if(!As || !Gs || !Cs || !Ts || !Is)boom(" Could not allocate memory for counts");
   fprintf(stderr,"DEBUG:  counting\n");
   for(i=0;i<records;i++){
      for(j=0; j <slen; j++){
         int base=toupper(aligned[i][j]);
         if(     base == 'A'){ As[j]++; }
         else if(base == 'G'){ Gs[j]++; }
         else if(base == 'C'){ Cs[j]++; }
         else if(base == 'T'){ Ts[j]++; }
         else{                 Is[j]++; }
      }
   }
   fprintf(stderr,"DEBUG:  emitting\n");
   for(j=0;j<slen;j++){
       fprintf(stdout,"%5d %4d %4d %4d %4d %4d\n",j,As[j],Gs[j],Cs[j],Ts[j],Is[j]);
   }

}

void boom(char *string){
   printf("Fatal error: %s\n",string);
   exit(EXIT_FAILURE);
}

2 个答案:

答案 0 :(得分:2)

我误解了你的问题。这是解决它的另一种尝试:

#!/usr/bin/perl
use warnings;
use strict;

use Data::Dumper;
use Time::HiRes qw{ time };

my $insertPoint = 100000;
my @alignedSeqs;
push @alignedSeqs, join '', map qw(A C G T X)[rand 5], 1 .. $insertPoint for 1 .. 3000;

my $start = time();

my %count;
for my $i (0 .. $#alignedSeqs) {
    my $j = 0;
    for my $ch (split //, $alignedSeqs[$i]) {
        ++$count{$ch}[$j++]
    }
}
for my $ch (keys %count) {
    next if $ch =~ /[ACTGI]/;
    $count{I}[$_] += $count{$ch}[$_] for 0 .. $insertPoint;
    delete $count{$ch};
}


my $end = time();

print Dumper \%count;
print $count{A}[0] + $count{C}[0] + $count{G}[0] + $count{T}[0] + $count{I}[0], "\n";
print $end - $start, " seconds\n";

或者,稍快一些(90对60秒),使用substr引用:

my $s = $alignedSeqs[0];
my @p = map \ substr($s, $_, 1), 0 .. $insertPoint - 1;


for my $i (0 .. $#alignedSeqs) {
    $s = $alignedSeqs[$i];
    my $j = 0;
    for my $ch (@p) {
        ++$count{$$ch}[$j++]
    }
}
for my $ch (keys %count) {
    next if $ch =~ /[ACTGI]/;
    $count{I}[$_] += $count{$ch}[$_] for 0 .. $insertPoint - 1;
    delete $count{$ch};
}

答案 1 :(得分:1)

my @As=(0) x $insertPoint;可能是浪费时间和记忆。使用一堆标量(每个标量分配内存)预加载数组效率不高。您可能正在寻找的是my @As; $#As = $insertPoint + 1,它只会在不填充数组的情况下增长。但是Perl在增长阵列方面是有效的。此步骤是不必要的,并且会导致问题,因为数组不会返回其真实长度。

至于循环,我会尝试使用数组散列来消除if / else链。并且要使用循环迭代器而不是手动循环,Perl可以更好地优化它们。

for my $i (0..$records-1) {
   for my $j (0..$insertPoint-1) {
      my $base = uc(substr($alignedSeqs[$i],$j,1));
      $Bases{$base}[$j]++;
   }
}

您可能希望将原始数据问题作为另一个问题发布,并获得一些新方法。

最后,您要找出代码花费时间的唯一方法是使用Devel::NYTProf之类的分析器。而且,最后,处理整数和逐个字符是C真正快速的事情,并且很难被击败。