そして 貴女は夜のパール身につけ また旅に 戻る

Perlでread関数使ってバイナリファイルを読み込んだら、1度に読み込むデータ量で処理速度が全然違った件

バイナリファイルを比較して、ファイルの壊れ具合をチェックするという処理をperlで書いて実行した。

 use Time::Piece;
#中略
 my $ngstart = 0;#undef(未定義)やnullでは都合が悪い。
 my $megabyte =1048576;
 open(SourceFILE0, "<", $arg[0]) or die("Cannt open $arg[0] !\n");
 binmode SourceFILE0;
 open(SourceFILE1, "<", $arg[1]) or die("Cannt open $arg[1] !\n");
 binmode SourceFILE1;
 for(my $i=1;$i <$file_size0;$i = $i +4){
  my ($buf0, $buf1);
  read(SourceFILE0, $buf0, 4, $i);
  my @data0 = unpack('C4', $buf0);
  read(SourceFILE1, $buf1, 4, $i);
  my @data1 = unpack('C4', $buf1);
  for(my $j =0;$j <4;$j++){
   if(($i +$j) >$file_size0){
    last;
   }
   if($data0[$j] != $data1[$j]){
    if($ngstart == 0){
      $ngstart = $i  + $j;
    }
   }else{
    if($ngstart){
     my $ngend = $i  + $j -1;
     print "違!  $ngstart$ngend\n";
     $ngstart = 0;
    }
   }
  }
  if($i -1 == $megabyte){
   my $t = localtime;
   my $megatta = $megabyte / 1048576;
   print "$megatta MB $t\n";
   $megabyte = $megabyte + 1048576;
  }
 }

当初は上記の設定で動かしたのだが、

1 MB Wed Jul 29 13:01:28 2020
2 MB Wed Jul 29 13:07:13 2020

と、1MBの処理に6分近くかかった。当初読み込むデータ量を4byteにしたのは、32bitのCPUなんだから、1度に扱えるのは4byteだろうと思ったから。
でもあまりにも遅すぎるので、思い切って1KB読み込むように書き換えた。

 for(my $i=1;$i <$file_size0;$i = $i +1024){
  if($i >$file_size0){
   last;
  }
  my ($buf0, $buf1);
  read(SourceFILE0, $buf0, 1024, $i);
  my @data0 = unpack('C1024', $buf0);
  read(SourceFILE1, $buf1, 1024, $i);
  my @data1 = unpack('C1024', $buf1);
  for($j =0;$j <1024;$j++){

すると

1 MB Wed Jul 29 13:17:09 2020
2 MB Wed Jul 29 13:17:12 2020
3 MB Wed Jul 29 13:17:15 2020
4 MB Wed Jul 29 13:17:21 2020
5 MB Wed Jul 29 13:17:27 2020

と、爆速(当社比)になった。さらに4KBずつ読み込むように書き換えたところ、体感での差は出なかった。

1 MB Wed Jul 29 13:41:41 2020
2 MB Wed Jul 29 13:41:43 2020
3 MB Wed Jul 29 13:41:46 2020
4 MB Wed Jul 29 13:41:51 2020
5 MB Wed Jul 29 13:41:55 2020

結局、読み込むデータ量を少なくしすぎることで、HDDからメモリへの読み込みの頻度が多くなって、そのHDDアクセスがオーバーヘッドとなって、全体の速度低下を招いていたのだろう。

上のスクリプトを実行すると、どんどん実行速度が遅くなる件

1KBずつ読み込む方のスクリプトでは、下記のようなログとなった。

200 MB Wed Jul 29 16:39:23 2020
201 MB Wed Jul 29 16:41:17 2020
202 MB Wed Jul 29 16:43:12 2020
203 MB Wed Jul 29 16:45:05 2020
204 MB Wed Jul 29 16:46:59 2020
205 MB Wed Jul 29 16:48:52 2020
206 MB Wed Jul 29 16:50:47 2020
207 MB Wed Jul 29 16:52:44 2020

当初は数秒で1MBを処理していたのに、1MBを処理する時間が2分に迫る速度低下である。
原因はどこにあるのか?
forループや whileループ内で "my" で定義した変数は、その場限りの変数であり、ループ実行後にシステムが破棄してくれる。次のループになるとこれらの my変数は再度定義される。どうもその処理が予想以上に重い処理であるようだ。

ループ内で my変数を定義するからいけないんだ!
ということで、ループ外で変数を定義して同じ変数を再利用することにするが、再初期化忘れというバグを発生させる元となるので、基本的にはお勧めしない。

 my $ngstart = 0;
 my ($buf0, $buf1,@data0, @data1);
 my $megabyte =1048576;
 for(my $i=1;$i <$file_size0;$i = $i +4096){
  read(SourceFILE0, $buf0, 4096, $i);
  @data0 = unpack('C4096', $buf0);
  $buf0 = '';
  read(SourceFILE1, $buf1, 4096, $i);
  @data1 = unpack('C4096', $buf1);
  $buf1 = '';
  for(my $j =0;$j <4096;$j++){
   if(($i  + $j)>$file_size0){
    last;
   }
   if($data0[$j] != $data1[$j]){
    if($ngstart == 0){
      $ngstart = $i  + $j;
    }
   }else{
    if($ngstart){
     my $ngend = $i  + $j -1;
     print "違!  $ngstart$ngend\n";
     $ngstart = 0;
    }
   }
  }
  @data0 =();
  @data1 =();
  if($i -1 == $megabyte){
   my $t = localtime;
   my $megatta = $megabyte / 1048576;
   print "$megatta MB $t\n";
   $megabyte = $megabyte + 1048576;
  }
 }

結果は

1 MB Wed Jul 29 17:56:07 2020
2 MB Wed Jul 29 17:56:08 2020
3 MB Wed Jul 29 17:56:10 2020
4 MB Wed Jul 29 17:56:11 2020
5 MB Wed Jul 29 17:56:13 2020
・・・
(中略)
・・・
200 MB Wed Jul 29 18:58:03 2020
201 MB Wed Jul 29 18:58:59 2020
202 MB Wed Jul 29 18:59:52 2020
203 MB Wed Jul 29 19:00:43 2020
204 MB Wed Jul 29 19:01:34 2020
205 MB Wed Jul 29 19:02:26 2020
206 MB Wed Jul 29 19:03:21 2020
207 MB Wed Jul 29 19:04:14 2020

処理速度低下はまぬがれていないものの、処理速度は向上した。

念の為書いておきますけど、これはサンプルコードであって、同じ処理をするならコマンドラインで下記のコマンドを打って、出力結果を加工するほうが速い。

% cmp -l ファイル1 ファイル2

2020/07/31 追記

ここまでのperlスクリプトにはread関数にオフセット値を付けていたが、オフセット値をつけることでいちいちファイルの先頭に移動して、また戻ってくるということをしていたのではなかろうか?という疑問が湧いたので、オフセット値を付けないことにした。

use Time::Piece;
#中略
 open(SourceFILE0, "<", $arg[0]) or die("Cannot open $arg[0] !\n");
 binmode SourceFILE0;
 open(SourceFILE1, "<", $arg[1]) or die("Cannot open $arg[1] !\n");
 binmode SourceFILE1;
 my $ngstart = 0;
 my ($buf,$megatta, $j);
 my $megabyte =104857600;
 for(my $i=1;$i <$file_size0;$i = $i +4096){
  read(SourceFILE0, $buf, 4096);# オフセット値の $i を削除
  my @data0 = unpack('C4096', $buf);# @data0の使い回しをやめた(後述)
  $buf = '';
  read(SourceFILE1, $buf, 4096);# オフセット値の $i を削除
  my @data1 = unpack('C4096', $buf);# @data1の使い回しをやめた(後述)
  $buf = '';
  for($j =0;$j <4096;$j++){
   if(($i  + $j)>$file_size0){
    last;
   }
   if($data0[$j] != $data1[$j]){
    if($ngstart == 0){
      $ngstart = $i  + $j;
    }
   }else{
    if($ngstart){
     my $ngend = $i  + $j -1;
     print "違!  $ngstart$ngend\n";
     $ngstart = 0;
    }
   }
  }
  if(($i + $j -1) == $megabyte){
   my $t = localtime;
   $megatta = $megabyte / 104857600;
   print "$megatta 00MB $t\n";
   $megabyte = $megabyte + 104857600;
  }
 }
 close SourceFILE0;
 close SourceFILE1;

とすると、

1 00MB Fri Jul 31 18:40:12 2020
2 00MB Fri Jul 31 18:41:47 2020
3 00MB Fri Jul 31 18:43:15 2020
4 00MB Fri Jul 31 18:44:46 2020
5 00MB Fri Jul 31 18:46:31 2020
6 00MB Fri Jul 31 18:48:02 2020
7 00MB Fri Jul 31 18:49:33 2020
8 00MB Fri Jul 31 18:51:05 2020
9 00MB Fri Jul 31 18:52:41 2020
10 00MB Fri Jul 31 18:54:10 2020

と爆速(当社比)となった。

perlのメモリ使用量がどんどん増える件

上記のスクリプトをそのまま動かし続けると、処理速度はどんどん遅くなった。

600 MB Thu Jul 30 05:09:35 2020
601 MB Thu Jul 30 05:11:53 2020
602 MB Thu Jul 30 05:14:12 2020
603 MB Thu Jul 30 05:16:29 2020
604 MB Thu Jul 30 05:18:46 2020
605 MB Thu Jul 30 05:21:04 2020
606 MB Thu Jul 30 05:23:22 2020
607 MB Thu Jul 30 05:25:42 2020

別の問題が発生していて、メモリ使用量も読み込んだデータに比例して増えている。

f:id:ta-nuki:20200730054456p:plain
perlのメモリ使用量

CPU使用量には余裕があるので2プロセス同時に動かしている。
こんな性能では全然実用に耐えない。本当に cmpコマンド使ったほうがマシである。

2020/07/31 追記

前項で高速化のために @data0, @data1をループ外で定義して全ループ共通で同じものを使いまわしたのだが、これがメモリリークを起こす原因なのではないかと思い、read関数のオフセット値を外した時に同時に、@data0, @data1の使い回しをやめたのだが、これが効果があったらしく、スクリプト実行中の急激なメモリ増加はなくなった。

おまけ

という訳で、cmpコマンドを使うように書き換えてみた。

 open(my $cmpout, "cmp -l $filename0 $filename1 2>&1 |");
 my @outlist = <$cmpout>;
 close($cmpout);
 if(!(defined $outlist[0])){
  die("cmpコマンドからの出力がありませんでした\n多分、完全に一致しているのでしょう\n");
 }
 $/='\n';
 print @outlist;

で、そもそも完全に一致しているファイルでは問題なかったのですが、一致していない部分が多かったファイル同士を比べたところ、メモリを使い果たしてOSがリブートする羽目になった。
perlがOSを落とした時と同じファイル同士を cmpコマンドで比較したところ

% cmp -l ファイル0 ファイル1 >diff.txt
% ls -la
  • rw-rw-rw- 1 tanuki staff 3933286178 7 30 2020 diff.txt

4GB弱のファイルサイズとなっていた。なので、一旦cmpコマンドでの結果をファイル出力してから、その出力ファイルを使って作業するしか仕方がないだろう。

% head -n 5 diff.txt
     98305   1   0
     98307   2   0
     98309  73   0
     98313 102   0
     98314 325   0

diff.txtの中身を見て「はぁ?」って言ってしまった。1バイトは8bitだから 28 = 256 で、0から255までの値しか格納できないはずなのに、325という数値がある。
「なんぞこれ?」と思って調べると、cmpが出力する値は 8進数なのだという。
というわけで、diff.txtを元に、0埋めされた部分を復元するperlスクリプトを作成した。

 open(DiffFILE, "<", $arg[1]) or die("Cannot open $arg[1] !\n");
 open(SourceFILE, "<", $arg[0]) or die("Cannot open $arg[0] !\n");
 binmode SourceFILE;
 open(NEWFILE, ">", $arg[2]) or die("Cannot open $arg[2] !\n");
 binmode NEWFILE;
 my $address =1;
 my ($diffdata,$seekadd,$buff) ;
# my ($seekchecks, $seekcheckn) ;
 while(<DiffFILE>){
  $_ =~ s/ +/ /g;
  $_ =~ s/^ //;
  my @diffdatas = split(/ /,$_);
  for(;$address < $diffdatas[0];$address++){
   read(SourceFILE, $buf, 1);
   print NEWFILE $buf;
   $buf ='';
  }
  read(SourceFILE, $buf, 1);
  if(unpack('C', $buf)==0){
   if($diffdatas[1]){
    $diffdata = oct($diffdatas[1]);
   }else{
    $diffdata = oct($diffdatas[2]);
   }
   $diffdata = pack('C',$diffdata);
   print NEWFILE $diffdata;
  }else{
   close SourceFILE;
   close NEWFILE;
   close DiffFILE;
   die("書き込み位置のデータが0ではありません");
  }
  $buf='';
  @diffdatas =();
=pod
  $seekcheckn =tell(NEWFILE);
  $seekchecks =tell(SourceFILE);
  if($seekcheckn != $seekchecks ||$seekchecks != $address){
   close SourceFILE;
   close NEWFILE;
   close DiffFILE;
   die("書き込み位置がズレています\n書き込み位置$seekcheckn: ソースファイル:$seekchecks アドレス:$address");
  }
=cut
  $address++;
 }
 while(read(SourceFILE, $buf, 1)){
  print NEWFILE $buf;
  $buf ='';
 }
 close SourceFILE;
 close NEWFILE;
 close DiffFILE;
 my $file_size2 = -s $arg[2];
 if($file_size2 != $file_size0){
  die("$arg[2]のファイルサイズが異なります\n$file_size0 != $file_size2 \n");
 }