ファイルの行数を取得する一番いい方法は?(Perl5)

[上に] [前に] [次に]
CZ 1999/12/04(土) 19:11:28
for ($i = 0; $i <= $#data; $i++) {
print << "END";
ファイル名: $data[$i][0]
行数: $data[$i][1]
最初の行: $data[$i][2]
END
}

上のような配列@dataを得るために一番効率が良く処理が早いのはどういう方法でしょうか。ファイル中の行数を調べる段階で悩んでいます。

自分で幾つか考えてみましたが、どれがいいでしょう。あるいは他にいい方法はないでしょうか。

Perl5を使っています。@filesにはファイル名のリストが入っています。

(1) 一行ずつ読み込む方法。
for ($i = 0; $i <= $#files; $i++) {
    open(READ, "$files[$i]");
    $data[$i][0] = $files[$i];
    $data[$i][1] = 0;
    while (<READ>) {
        # 最初の行だけ記録する
        if ($data[$i][2] eq '') { $data[$i][2] = $_; }
        $data[$i][1]++;
    }
    close(READ);
}

(2) ファイルの内容を一度に読み込み、改行の数で行数を判定する方法。
for ($i = 0; $i <= $#files; $i++) {
    open(READ, "$files[$i]");
    $data[$i][0] = $files[$i];
    read (READ, $str, (-s "$files[$i]"));
    $data[$i][2] = substr($str, 0, index($str, "\n"));
    $data[$i][1] = ($str =~ s|\n||g);
    close(READ);
}

(3) ファイルの内容を一度に配列に読み込む方法。
for ($i = 0; $i <= $#files; $i++) {
    open(READ, "$files[$i]");
    $data[$i][0] = $files[$i];
    @str = <READ>;
    $data[$i][1] = $#str + 1;
    $data[$i][2] = $str[0];
    close(READ);
}

(2)と(3)はファイルの内容を一度に読み込んでしまうので負荷が大きいと思います。かといって(1)は遅そうで……。

CZ 1999/12/04(土) 19:28:47
(2)の
> $data[$i][2] = substr($str, 0, index($str, "\n"));

$data[$i][2] = substr($str, 0, index($str, "\n") + 1);
の間違いでした。

1999/12/04(土) 23:59:56
この件だけの為に、ベンチマークを取ったわけではありませんが、私の(浅い)経験上では、1の方法が負荷も少なく、速度も速かったように思います。

CZ 1999/12/06(月) 11:01:27
[[解決]]
なるほど。確かにそのようですね。最近の発言を眺めていたら、過去ログ ../199906/99060273.htm が紹介されていましたが、どんなに小さいファイルでも一気に読み込むより一行ずつ読み込む方が早そうです。ありがとうございました。

CZ-634C TN + CZ-614D TN 1999/12/06(月) 11:42:33
済のところ失礼しますm(__)m
UNIXでバッククォートが使えるならWCを使うって手もあります。

andi 1999/12/06(月) 12:45:58
ついでに質問させて下さい!
use Bentchmark;
を使用したのですが、
「274行目のtimesを実行できない。」
って言われちゃいました。
Win95+AnHTTPd+Perl5じゃぁ
無理なんでしょうか?

ふじ 1999/12/06(月) 15:55:44
>「274行目のtimesを実行できない。」
>って言われちゃいました。

Perl5 のバージョンは?(コマンドラインで perl -v )
確か古いのでは使えなかった気が。
#最新の Active Perl だと大丈夫です。

J.Naka 1999/12/06(月) 17:16:52
199906/99060273.txt のスクリプトを perl -wc で走らすと、以下のメッセージが出ます。
>Name "main::t" used only once: possible typo at FujiCodeORG.pl line 27.

バージョンは、 version 5.003_07です。何処がおかしいのでしょうか?

--------------------------

 サーバー上で走らすPERLスクリプトは、なるだけ一行毎処理をするが良いと解かりましたが、ローカル上のコンピューター(WIN95)占有状態でも配列等の使用はスピードという点ではメリットないのでしょうか?

 ここいらを上記のスクリプトで確かめてみたいところです。
 
 もしかしてNT用ライブラリだったら精度落ちますがtime()関数当たりで代用してみようかと思ってます。(実際やってみましたが、例により何故か動かん(笑))

#しかし配列処理がスピード的にはデメリットになるとは驚きです(*_*)

ふじ 1999/12/06(月) 18:39:05
> 199906/99060273.txt のスクリプトを perl -wc で走らすと、以下のメッセージが出ます。
> >Name "main::t" used only once: possible typo at FujiCodeORG.pl line 27.
>バージョンは、 version 5.003_07です。何処がおかしいのでしょうか?

それは警告です。(w オプション指定しているから)
@t てのをスクリプト内で一度しか使ってないから警告されているんですけど。

#私も余りよく Benchmark.pm を理解してないけど、
#timethese を使うためのダミー、てことでいいのかな。

スクリプト自体は動きませんでしたか?

J.Naka 1999/12/06(月) 19:58:57
スクリプト動きませんです。
エラーメッセージです。意味が良く解かりません(^^;

>Benchmark: timing 1000 iterations of ALL , STEP...
>times not implemented at F:\Perl\lib/Benchmark.pm line 274.

#OSはWIN95です。

ふじ 1999/12/06(月) 21:50:03
>スクリプト動きませんです。
とりあえず Win95 + ActivePerl 522 では動きました。

#Perl5 for Win32 Build316 では同様のエラーが出て動かない、
#という話が以前 CGI-ML で流れてました。それと同じかと。

J.Naka 1999/12/06(月) 22:10:56
ども! ふじさん。動作するのはActiveが冠してあるやつなんですね、、、怪しいバージョンだ(笑)

えっと最後のお願いなんですが、199906/99060273.txt のサンプルコードの配列一気読み込みの配列をサブルーチンに入る前にあらかじめ固定領域確保をやってもらえないでしょうか? 
図々しいお願いは重々に承知してますんで、超お暇で気がごっつう向いた時で結構でおます(^^)

ふじ 1999/12/07(火) 02:41:07
>固定領域確保
それをやっても、(あのスクリプトの場合)
その恩恵を受けるのは何回もループする最初の1回では。
# 効率よく領域確保するためには、あらかじめファイルの大きさ、
# 行数、各行のバイト数を調べないといけないし、それやるぐらいなら
# 突っ込んじゃえ、って思うんですが (^^;

で、話が最初の話題からそれているので、今までの経緯を参考にまとめます。

[ファイルの行数を得る方法]

ALL : ファイル全体を変数に読み込んで、改行の数を数える
ARRAY : ファイル全体を配列に読み込んで、配列の要素数を数える
STEP : ファイルを一行ずつ読んで、行数を数える
WC : UNIX の wc コマンドを使う(wc --lines ファイル名)

これらのベンチマークをとるスクリプトを
http://www.aleph.co.jp/~fujiwara/perl/lc.pl
に置いてあります。

[行数を数える対象のファイル]
lc.pl (46 lines , 739 bytes)
jcode.pl (733 lines, 19900 bytes)
test.txt (17989 lines, 9229902 bytes)

[結果]
(Linux 2.2.13, Perl5.005_03, K6-2 400, 128MB RAM)

$ perl lc.pl lc.pl 10000
Benchmark: timing 10000 iterations of ALL, ARRAY, STEP, WC...
       ALL:  2 wallclock secs ( 0.77 usr +  0.73 sys =  1.50 CPU)
     ARRAY:  5 wallclock secs ( 4.24 usr +  1.00 sys =  5.24 CPU)
      STEP:  3 wallclock secs ( 2.98 usr +  0.53 sys =  3.51 CPU)
        WC: 75 wallclock secs ( 2.15 usr  6.05 sys + 36.30 cusr 30.17 csys =  0.00 CPU)

$ perl lc.pl jcode.pl 1000
Benchmark: timing 1000 iterations of ALL, ARRAY, STEP, WC...
       ALL:  1 wallclock secs ( 0.53 usr +  0.23 sys =  0.76 CPU)
     ARRAY:  8 wallclock secs ( 7.94 usr +  0.24 sys =  8.18 CPU)
      STEP:  6 wallclock secs ( 5.22 usr +  0.15 sys =  5.37 CPU)
        WC:  8 wallclock secs ( 0.14 usr  1.86 sys +  3.64 cusr  2.37 csys =  0.00 CPU)

$ perl lc.pl test.txt 100
Benchmark: timing 100 iterations of ALL, ARRAY, STEP, WC...
       ALL:  3 wallclock secs ( 2.61 usr +  0.81 sys =  3.42 CPU)
     ARRAY: 30 wallclock secs (29.12 usr +  0.89 sys = 30.01 CPU)
      STEP: 21 wallclock secs (20.58 usr +  0.49 sys = 21.07 CPU)
        WC:  2 wallclock secs ( 0.01 usr  0.19 sys +  0.94 cusr  1.27 csys =  0.00 CPU)

[講評(笑)]
意外に ALL が早いです。 WC は、プロセス起動のオーバーヘッドのためか、
ファイルが小さい場合は不利ですが、ファイルが大きくなると
高速ですね(...餅は餅屋)。

この結果だけ見るなら、ファイルが比較的小さいなら ALL 、
大きいなら WC 、メモリ消費をできるだけ押えたいなら STEP が
良いようです。

# いちおう、WindowsNT環境でもやってみましたが(除 WC)
# まったく同じ傾向です。

長々と失礼しました。

andi 1999/12/07(火) 12:50:22
自分も500307でしたが
結局使えないって結論なんですね?
残念です。

何か他に良い方法ってありますでしょうか。
性能を測る手段。こういうの詳しくないもので。

CZ 1999/12/07(火) 13:01:12
うーん、すごい。勉強になります。プラットフォームを問わないスクリプトを書きたいことと、対象とするそれぞれのファイルがあまり大きくないので、WCは使わないこととします。となるとALLかSTEPですね。

ふじさんのlc.plを私の意図するスクリプトに改造して実験しました。環境はWindows95+Perl5.005_03です。

2ファイル、45行(44行+1行)、30.2KB
>perl lc.pl dir1 1000
Benchmark: timing 1000 iterations of ALL, ARRAY, STEP...
       ALL: 37 wallclock secs (36.97 usr +  0.00 sys = 36.97 CPU)
     ARRAY: 29 wallclock secs (29.44 usr +  0.00 sys = 29.44 CPU)
      STEP: 27 wallclock secs (26.31 usr +  0.00 sys = 26.31 CPU)

8ファイル、799行(各100行程度)、299KB
>perl lc.pl dir2 200
Benchmark: timing 200 iterations of ALL, ARRAY, STEP...
       ALL: 20 wallclock secs (19.22 usr +  0.00 sys = 19.22 CPU)
     ARRAY: 42 wallclock secs (42.40 usr +  0.00 sys = 42.40 CPU)
      STEP: 23 wallclock secs (23.35 usr +  0.00 sys = 23.35 CPU)

23ファイル、2243行(各100行程度)、1.36MB
>perl lc.pl dir3 100
Benchmark: timing 100 iterations of ALL, ARRAY, STEP...
       ALL: 24 wallclock secs (23.51 usr +  0.00 sys = 23.51 CPU)
     ARRAY: 50 wallclock secs (50.42 usr +  0.00 sys = 50.42 CPU)
      STEP: 34 wallclock secs (33.89 usr +  0.00 sys = 33.89 CPU)

こうするとやはりALLが早いみたいです。でも共用サーバーで使う場合にはメモリを食わない方が重要な気がします。STEPの処理速度も(ファイル構成がこちらの意図通りである限りにおいては)ALLと比べて三分の一程度の違いなので、許容範囲かと思います。それで、結論としては(1)の一行ずつ読み込む方法を採用したいと思います。

いろいろと教えてくださりありがとうございました。

J.Naka [E-Mail] 1999/12/07(火) 14:13:12
ども! ふじさん、CZさん、興味深い実験レポート有り難う御座います。
#連続した1つ領域である1ッコの文字列変数が結構早いというのは納得いきます。

え〜、自分で実験できないのが残念至極なのですが、配列が遅い理由を推察 to 列挙してみたいと思います。
#少し混乱してる所は堪忍です(^^; 誰かもっとスマートに纏めてくれると嬉しい。

1.perlの配列は物理メモリ上に連続確保されるものではない。
 perlは文字列と数値を同一に扱うために全ての変数は文字列(これ自体が配列なわけ)領域してメモリ上の散在的に取られる。結果、これへのアクセスはポインタの追っかけが生じるためにダイレクトアクセスと同等のポテンシャルは無い。

2.perlの変数アクセス(配列を含む)は常に文字列としてのアクセスがある。
 これは、配列格納時に要素毎に文字列処理が必要という事で、これがスピードの足かせになる。

3.perlではファイルハンドルから配列へ一括読み込みの場合、一行チェック(改行サーチ)が同時に行われる仕様である(当たり前(^^;)。
 この時のチェックコードがどの程度の性能を持つのか怪しい、ひょっとしてタコなルーチンかもしれない。

4.perlでは、配列などの単純変数よりも複雑な構造を持つ物でも、オブジェクト指向のObjectのようには内部にその諸々の属性は静的に保持してなく、結果としてアクセス毎に常に、配列の各要素を算出しなおす作業が生じ、これがスピード低下になっている。

-----------------------------
ふじさん、
>それをやっても、(あのスクリプトの場合)
>その恩恵を受けるのは何回もループする最初の1回では。
ループの外であらかじめ、大き目の領域を持つ文字列の配列として確保。という意味でしたが、配列要素格納時点で、領域調整が行われれば、意味ないですね(^^;いわゆるガベージコレクションというやつになるのでしょうか。

 えーと、結論というか最終見解というか(それほどのものではない(^^;)ですが、文字列配列生成時には、文字列の格納に文字列領域確保作業が生じるので1変数領域生成よりは時間がかかるのは当然ですが、生成後にその文字列を伸縮しないアクセスのみであれば配列のスピードメリットはでるはずでよすね。これが無いと全くなんの配列か解からない。単にソース上表記の簡略化&ロジック簡略化の為の物に成り下がりますね。

ふじ 1999/12/07(火) 14:51:35
> 単にソース上表記の簡略化&ロジック
> 簡略化の為の物に成り下がりますね。
データ構造が見通しよく記述できるってのは**大きな**メリットでは?

要素の大きさも数も不定の配列に、自由に push pop 等
が行えるのは、アルゴリズムを素直にコーディングする上で非常に楽です。
そのために実行速度が犠牲になるのは仕方ないでしょう。
速度が欲しければ C や C++ で書けば良いんですから。

# 開発速度は上がりますよ ;-)

[上に] [前に] [次に]