File-Slurp-9999.01 > File-Slurp-9999.01/extras/slurp_article

Perl Slurp概要

はじめに

Perlでよくみかけるコードはテキストファイルを行単位に処理することです:

    while( <FH> ) {
        do something with $_
    }

このコードは複数の変数を持っています。しかしキーポイントは各ループの 繰り返しの度にファイルから1行だけ読み込むことです。これには メモリを1行に使われるだけに制限できる、(STDINを経由してパイプされた データも含めて)どんな大きさのファイルも扱うことができる、Perl初心者にも 教えやすく理解しやすいといった、いくつかの利点があります。実際のところ、 初心者は以下のような馬鹿げたことをしがちです:

    while( <FH> ) {
        push @lines, $_ ;
    }

    foreach ( @lines ) {
        do something with $_
    }

行毎の処理は素晴らしいのですが、ファイルの読み込みを処理する唯一の方法では ありません。他のよくあるスタイルはファイル全体をスカラーや配列に読み込む ことです。これは俗に丸呑み(slurping)といわれています。現在、丸呑みの 評判はあまりよくありません。この記事はそれを社会復帰させようとしています。 ファイルの丸呑みには利点と制限があります。行毎の処理がよい場合には 単純に行うべきものではありません。一度に処理するため、メモリ上に ファイル全体を必要とするときには最もよいことです。 適切に行われれば、メモリ処理を伴う丸呑みは、行ごとに処理するよりも 速く、コードをシンプルにします。

丸呑みを見ていて一番の問題はファイルの大きさです。非常に大きなファイル やSTDINからデータの量がわからないものを丸呑みすることは、メモリ使用に 損害をもたらし、スワップ・ディスクをスラッシングさせてしまうかもしれません。 メモリ使用に悪影響を与えることなく最大サイズの入力を扱うことが分かっている 場合にのみ、STDINを丸呑みすることができます。そこで私はディスク・ファイルだけを、 それもほどほどの大きさであることを知っていて、ファイルを丸ごと処理する ちゃんとした理由がある場合にのみ丸呑みすることを主張します。 今日の、ほどほどの大きさはRAMが限られていた昔よりも大きいことに 注意してください。メガバイトを丸呑みしても、ほとんどのシステムでは 問題はないでしょう。しかし私が丸呑みしようとする、ほとんどのファイルは それよりも大分小さくなりがちです。丸呑みがうまくいく典型的なファイルは 設定ファイル、(ミニ-)言語スクリプト、いくつかのデータ(特にバイナリ)ファイル、 そして早く処理する必要がある大きさが分かっている他のファイルです。

行単位よりに丸呑みが勝利する大きなもう一つの点は速度です。Perlの IOシステムは(他の多くと同様に)遅いものです。行ごとに<>を呼び出す ことは、行末をチェックし、EOFをチェックし、行をコピーし、内部ハンドル 構造体を加工するなどを行います。行の読み込みのために各行のたくさんの 作業が行われます。これに反して丸呑みは、もし正しければ、通常 1回だけのI/O呼び出しを伴い、余分なデータのコピーはありません。 ディスクへの書きこみでも同じことがいえます。そして私たちは それもカバーします(端末丸呑みが伝統的な読みこみ操作であっても、 ファイル全体へのI/Oを1回の操作で行うという考え方のため、 "丸呑み"という言葉を使います)。

最後にファイル全体をメモリ上に丸呑みしたときには、行単位の処理を 行うことは可能ではないか、簡単ではないデータを操作することができます。 これには(改行は無視した)グローバルな検索/置換、//gを呼び出すことで 全てのマッチを捕らえること、複雑な解析(これは多くの場合、改行を無視 しなければなりません)、*MLの処理(そこでは行の末尾は単なる空白です)、 テンプレート展開のような複雑な変形が含まれます。

グローバルな操作

丸呑みされたファイル全体に対して早く簡単に行うことができる、いくつかの 簡単なグローバルな操作を以下に示します。それらは行単位の処理によっても 行うこともできます。しかしそれはより遅くより多くのコードを必要と します。

よくある問題は、キー/値の組を持ったファイルを読み込むことです。これを 行うモジュールもあります。しかし簡単なフォーマットのためにそれらが 必要な人はいるでしょうか?単純にファイルを丸呑み、全てのキー/値のペアを 全て捕らえるために一回の解析を行ってください。

    my $text = read_file( $file ) ;
    my %config = $test =~ /^(\w+)=(.+)$/mg ;

行の始まりにあるキー(/m修飾子のために文字列のどこにあっても)、 =文字、そして行の末尾にあるテキスト(再び/mがその働きをおこないます)に マッチします。実際には末尾$も必要ですらありません。というのも .は通常は改行にマッチしないからです。キーと値が捕らえられ、m///g修飾子付きでリスト・コンテキストであるために、それは全ての キー/値の組を捕らえ、それらを返します。%configハッシュは、このリストが 代入され、完全に解析されたファイルをハッシュに持っていることになります。

私が働いてきたさまざまなプロジェクトでは、いくつかの簡単なテンプレートを 必要としていました。そして私は完全なモジュールを使う気にはなれませんでした。 (お願いですから、あなたの好きなテンプレート・モジュールについて熱くならない でください :-)。そこで私はテンプレート・ファイルを読みこみ、 テンプレート・ハッシュを設定し、これを1行で行ってきました:

    $text =~ s/<%(.+?)%>/$template{$1}/g ;

これはファイル全体が丸呑みされている場合にのみ機能します。ほんの少しの 余分な作業で拡張されるテキストの固まりを扱うことができます:

    $text =~ s/<%(\w+)_START%>(.+)<%\1_END%>/ template($1, $2)/sge ;

マーカーの間のテキストを展開するためにtemplateサブルーチンを与えて みてください。そして最小限のコードで簡単なシステムを持つことになります。 これは上手くいき、/s修飾のために複数行を捕らえることに注意してください。 これは行単位の処理では扱いにくいものです。

これは非常に簡単なテンプレート・システムです。そして直接、ネストしたタグを やそのほかの複雑な機能を扱うことはできないことに注意してください。しかし CPAN上にある数多くのテンプレート・モジュールの一つを使うとしても、 ファイルの読みこみと書き込みのための、より早い方法を持つことによって 利益を得ます。

配列にファイルを丸呑みすることにも、いくつかの便利な利点があります。 1つの簡単な例は、各レコードが:のような文字により分割されたフィールドを 持っているフラットなデータベース・ファイルを読み込むことです:

    my @pw_fields = map [ split /:/ ], read_file( '/etc/passwd' ) ;

丸呑みされたファイルの行にランダムなアクセスすることは他の利点があります。 行の配列を検索することを早くするために行インデックスを構築することも できます。

伝統的な丸呑み

Perlは常に最小限のコードでファイルの丸呑みをサポートしてきました。 ファイルを行のリストに読み込むことはささいなことです。単純に<> 演算子をリスト・コンテキストで呼び出すだけです:

    my @lines = <FH> ;

そしてスカラーに丸呑みすることは、さらに多くの作業はありません。 組込変数$/(入力レコード分割子)を単純に未定義値に設定し、 <>でファイルを読み込むだけです:

    {
        local( $/, *FH ) ;
        open( FH, $file ) or die "sudden flaming death\n"
        $text = <FH>
    }

local()を使っていることに注意してください。これは$/undefに設定し、 スコープを抜けるときには、$/は前の値に戻します(ほとんどの場合にはおそらく "\n"です)。

以下のPerlのコードは$text変数が宣言されることを可能にしています、 しっかりとネストされたブロックには何も必要ありません。doブロックは スカラーコンテキストで<FH> を実行し、ファイルを$textに 読みこみます:

    local( *FH ) ;
    open( FH, $file ) or die "sudden flaming death\n"
    my $text = do { local( $/ ) ; <FH> } ;

それらの読み込は両方とも、5.005との互換性のためにロカール化された ファイル・ハンドルを使っています。以下のものは5.6.0の 自動的に生成されるレキシカルなハンドルを使っています:

    {
        local( $/ ) ;
        open( my $fh, $file ) or die "sudden flaming death\n"
        $text = <$fh>
    }

    open( my $fh, $file ) or die "sudden flaming death\n"
    my $text = do { local( $/ ) ; <$fh> } ;

そしてこれは、open呼び出しの必要を削除した、そのコードの変形です:

    my $text = do { local( @ARGV, $/ ) = $file ; <> } ;

$fileの中のファイル名はローカル化された@ARGVに設定され、 nullのファイル・ハンドルは@ARGVの中のファイルからデータを読み込むために 使われます。

スカラーに代入する代わりに、上記の丸呑み全てを配列に代入することができます。 そしてそれはファイルを取得しますが、($/が行の終わりの印として 使われて)行に分割されます。

それらの丸呑みのよくある変形があります。これは非常に遅く、よいコードでは ありません。ざっとみてください、これはほとんど常にお荷物のコードです:

    my $text = join( '', <FH> ) ;

入力ファイルを不必要に行に分割し(join<FH>をリスト・コンテキスト にします)、そしてそれらの行を再びつなげています。この慣用句の元のコーダは 明らかにperlvarを読んだことがなく、スカラーの丸呑みを可能にするための $/の使い方を学んだことがありません。

書き込みのための丸呑み

ファイル全体を一度に読み込むのは一般的ですが、ファイル全体を書き込む こともできます。ファイルの読み込みのとき、私たちはそれを"丸呑み(slurping)" と呼びました。しかし書き込み操作について一般的に受け入れられている言葉は ありません。私は何人かのPerl仲間に尋ねてみました。すると2つの興味深い 推薦をえることができました。Peter Scottはそれを"burping"(=丸出しする) ("slurping"と韻をふみ、逆方向での動きを示唆します)。推薦されたもう1つは "spewing"(=吐き出す)です。これはより強い視覚的なイメージを持っています :-) あなたの好みを教えるか、あなた独自のものを提案してください。このセクションでは 私は両方を使います。そのためそれらがどのように機能するかが分かるでしょう。

ファイルの吐き出しは丸呑みより、さらに簡単な操作です。心配するような コンテキストの問題はありません。そしてバッファを返すことについて効率の 問題は何もありません。以下に簡単な吐き出しサブルーチンです:

    sub burp {
        my( $file_name ) = shift ;
        open( my $fh, ">$file_name" ) || 
                 die "can't create $file_name $!" ;
        print $fh @_ ;
    }

入力テキストをコピーせず、@_を直接printに渡していることに注意して ください。後ほどより速い変形を見ることになります:

CPAN上での丸呑み

あなたが予想するようにCPANにはあなたに代わってファイルを丸呑みするモジュール があります。私はSlurp.pm(Rob Casey - CPANではROBAU)と File::Slurp.pm (David Muir Sharnoff - CPANではMUIR)という2つを見つけました。

Slurp.pmからのコードを以下に示します:

    sub slurp { 
    local( $/, @ARGV ) = ( wantarray ? $/ : undef, @_ ); 
    return <ARGV>;
    }

    sub to_array {
    my @array = slurp( @_ );
    return wantarray ? @array : \@array;
    }

    sub to_scalar {
    my $scalar = slurp( @_ );
    return $scalar;
    }

+slurp()サブルーチンは、スカラーや配列に丸呑みすることをサポートするために $/を未定義値にするという方法と特別なファイルハンドルARGVを使っています。 また呼び出し元が読み込みのコンテキストを制御することを可能にする2つの ラッパー・サブルーチンを提供しています。そしてto_array()wantarrayを チェックすることにより、その呼び出し元のコンテキストに従って丸呑みされた 行のリストや無名リストを返します。@EXPORTには'slurp'が入っており、 @EXPORT_OKには3つのサブルーチンが入っています。

<脚注: Slurp.pm は名前の付け方としてはよくありません。それはトップレベルの 名前空間にあるべきではありません。>

元のFile::Slurp.pmにはこのコードが入っています:

sub read_file { my ($file) = @_;

    local($/) = wantarray ? $/ : undef;
    local(*F);
    my $r;
    my (@r);

    open(F, "<$file") || croak "open $file: $!";
    @r = <F>;
    close(F) || croak "close $file: $!";

    return $r[0] unless wantarray;
    return @r;
}

このモジュールはread_file()も含めていくつかのサブルーチンを提供しています (他のものについては後述します)。read_file()は呼び出し元のコンテキストに よって、行のリストや1つのスカラーを丸呑みするという点でSlurp::slurp()と 同じように振舞います。これもスカラーの丸呑みのために$/を未定義値に するという方法を取っています。しかしローカル化された@ARGVを使い、 他のモジュールが行うのではなく、明示的なopen呼び出しを使っています。 行の無名配列を取得する方法も提供していません。 しかしそれは、無名配列コンストラクタ[]の内側で、それを呼ぶことに より簡単に修正することができます。

これらのモジュールの両方はPerlコーダにファイルを丸呑みすることを簡単にします。 それは両方ともスカラー・モードで読み込むために$/の方法を、 行で丸呑みするためのリスト・コンテキストでは<>の自然な動きを 用いています。しかしどちらもスピードの最適化されていませんし、 バイナリあるいはUnicodeファイルをサポートするためにbinmode()を 扱うこともできません。読み込みの機能やスピードアップについての詳細は 下記をご覧ください。

丸呑みAPIの設計

CPANにある丸呑みモジュールは非常に簡単なAPIを持っています。そして binmode()をサポートしていません。このセクションは、リファレンスによる 効率的な戻り値、binmode()、そして呼び出しのバリエーションなど、 さまざまなAPI設計問題をカバーします。

呼び出しのバリエーションから始めましょう。読み込まれたファイルは4つの フォーマットで返される可能性があります:単一のスカラー、スカラーへのリファレンス、 行のリスト、行の無名配列。しかし呼び出しものは2つのコテキストしか 提供できません:スカラーかリストかです。そこで私たちは、(Slurp.pmが やっているように)1つ以上のサブルーチンでAPIを提供するか、 File::Slurpがやっているようにスカラーか(無名配列ではなく)リストを 返す1つのしかサブルーチンを提供しないかどちらかです。

私は独自のread_file()サブルーチンを長年使ってきました。しかし それはFile::Slurpと同じAPIを持っています:コンテキストによって スカラーか行の配列を返す1つのサブルーチンでした。しかしファイル 丸呑みのために無名配列を欲しがっている人の興味を理解しています。 あるサブルーチンから他のサブルーチンに渡すのがより簡単です、 それはreturnによる行の余分なコピーを除去します。そこで私のモジュールは たった1つの丸呑みサブルーチンを提供します。それは コンテキストと渡されたフォーマットのオプションを基にファイルデータを 返します。スカラーやリストで丸呑みする特別なサブルーチンは必要ありません。 汎用的なread_file()サブルーチンが適切なコンテキストでデフォルトで それを行います。もしread_file()にスカラー・リファレンスや 行の無名配列を返して欲しければ、それらのフォーマットをオプションで 要求することができます。スカラーにリファレンスを渡し(例えば 前もって確保されているバッファ)、丸呑みされたデータを入れさせることもできます (そしてこれは最も速い丸呑みモードの1つです。詳細はベンチマークの セクションをご覧ください)。もしスカラーを配列に丸呑みさせたければ、 単純に望まれる配列要素を選択するだけです。すると read_file()サブルーチンにスカラー・コンテキストを提供します。

カバーする次の領域は、丸呑みサブルーチンの名前です。私はread_file()で いきます。これは叙述的ですし、現在の簡単なものと互換性があります。 そして'slurp'とうあだ名を使いません(そのあだ名はモジュール名ですけれども)。 また私はFile::Slurp名前空間を保持することにしました。これは親切なことに、 その現在の所有者であるDavid Muirにより私へ渡されました。

APIを設計するときもう1つの重大な領域は、引数の渡し方です。 read_file()サブルーチンは1つの必須な引数を取ります。それはファイル名です。 binmode()をサポートするため、私たちはもう1つのオプションの 引数を必要とします。リファレンスより丸呑みされたスカラーを返すことを サポートするため、3番目のオプションの引数が必要です。私は最初、考えたことは 位置による3つの引数-ファイル名、バッファ・リファレンス、そしてbinmodeを もつAPIを設計することでした。しかしbinmodeを設定したいけれど、バッファ・ リファレンスを渡したくなければ、2番目の引数をundefで埋めなければなりません。 これでは格好よくありません。そこで私はファイル名を位置によるものとし、他の 2つを名前付きとしました。そのサブルーチチンは以下のようにはじまります:

    sub read_file {

        my( $file_name, %args ) = @_ ;

        my $buf ;
        my $buf_ref = $args{'buf'} || \$buf ;

もう一つのサブルーチン(read_file_lines())は、オプションのbinmodeしか 取りません(そのためバイナリの分割子を持ったファイルを読み込むことができます)。 それはスカラー・コンテキストでは無名配列を返すことができるので、 バッファ・リファレンスの引数を必要としません。そのためこのモジュールは 位置の引数を使うことができますが、read_file()のAPIにそのAPIを似せるために、 オプションの引数のための名前による引渡しも使います。これは古いコードを 壊すことなく新しいオプションの引数を後から追加することができるということも 意味しています。両方のサブルーチンのためにAPIを同じことによる思わぬ贈り物は、 2つのサブルーチンがどのように、一緒に機能するように最適化されたかを 見ることができることです。

出力での丸呑み(あるいは吐き出し、丸出し :-))も、同様にそのAPIを 設計する必要があります。最も大きな問題はオプションの引数をサポートする 必要があるだけでなく、書き出される引数のリストも必要になることです。 Perl6はオプションの名前付き引数と最後のslurp引数で扱うことができるでしょう。 これはPerl 5なので、いくらか知恵を使う必要があります。 最初の引数はファイル名、そしてそれはread_fileサブルーチンと同様に 位置よる引数になります。しかしオプションの引数を、そしてデータのリストを どのように渡すことができるでしょうか?解決は、データ・リストには決して リファレンスが入らないという事実にあります。吐き出し/丸出し(=Burping/Spewing)は、 プレーンなデータにのみ機能します。そのためもし次の引数が ハッシュ・リファレンスであれば、それにはオプションの引数が入っていて、 残りの引数がデータリストだと考えることができます。そこでwrite_file()は 以下のように始まります:

    sub write_file {

        my $file_name = shift ;

        my $args = ( ref $_[0] eq 'HASH' ) ? shift : {} ;

オプションの引数が渡れても、渡されなくても、余分なコピーを最小限にするため、 データ・リストを@_に残しておきます。write_file()をこのように呼び出す ことができます:

    write_file( 'foo', { binmode => ':raw' }, @data ) ;
    write_file( 'junk', { append => 1 }, @more_junk ) ;
    write_file( 'bar', @spew ) ;

高速な丸呑み

私はある時点で$/をundefに設定するよりも速くファイルを丸呑みする方法を 学びました。そのほうほうはとても単純です。ファイルの大きさ(これは-s演算子が 提供します)でreadを一回呼び出すだけです。これはEOFをチェックをperl内部の I/Oループを迂回し、処理の全てを行います。そこで私は実験を決意し、 sysreadがさらに予想よりも速いことがわかりました。sysreadは すべてのPerlのstdioを回避し、カーネル・バッファからファイルを直接 Perlスカラーに読みこみます。これがFile::Slurpがsysopen/sysread/syswriteを 使っている理由です。コードの残りはすべてさまざまなオプションと データ処理のテクニックをサポートしているだけです。

ベンチマーク

ベンチマークは明白にさせ、有益で、いらいらさせたり、勘違いさせるかも しれません。スピードも著しく向上しなければ、新しく、より複雑な 丸呑みモジュールを作り出すことを意味がないかもしれません。 そこで私はベンチマーク・スクリプトを作成しました。 ファイルの大きさと呼び出しコンテキストを変えて、さまざまな丸呑み メソッドを比較するベンチマーク・スクリプトを作成しました。 このスクリプトはtarファイルのメインのディレクトリから以下のように 実行させることができます:

    perl -Ilib extras/slurp_bench.pl

コマンドラインで一つの引数を渡すと、それはtimethese()に渡され、 それがその期間を制御します。そのデフォルトは-2で、それはcpu時間で 少なくとも2秒まで各ベンチマークを実行させます。

以下の数値は私の300Mhz sparcで行った実行からのものです。 あなたのマシンでは、もっと速いカウントを得ることでしょう。しかし 相対的なスピードはあまり変わらないはずです。どうかあなたの結果と PerlそしてOSのバージョンを送ってください。またベンチマーク・スクリプトで 遊んだり、より多くの丸呑みのバリエーションやデータファイルを追加する こともできます。

このセクションの残りでは、ベンチマークの結果について説明します。 個々のベンチマークのためのコードを見るため、extras/slurp_bench.plを 参照することができます。メンチマーク名がcpan_で始まっていれば、 Slurp.pmかFile::Slurp.pmのどちらかからのものです。new_から始まるものは 新しいFile::Slurp.pmからのものです。file_contents_で始まるものは クライアントのコードが基です。私が作った残りは、ベンチマークの ある視点を明確にするため私が作成したバリエーションです。

小さいファイル、大きいファイルのデータは以下のようにつくられます:

    my @lines = ( 'abc' x 30 . "\n")  x 100 ;
    my $text = join( '', @lines ) ;

    @lines = ( 'abc' x 40 . "\n")  x 1000 ;
    $text = join( '', @lines ) ;

そのため小さいファイルは9,100バイト、大きいファイルは121,000バイト です。

小さいファイルのスカラー丸呑み

    file_contents        651/s
    file_contents_no_OO  828/s
    cpan_read_file      1866/s
    cpan_slurp          1934/s
    read_file           2079/s
    new                 2270/s
    new_buf_ref         2403/s
    new_scalar_ref      2415/s
    sysread_file        2572/s

大きいファイルのスカラー丸呑み

    file_contents_no_OO 82.9/s
    file_contents       85.4/s
    cpan_read_file       250/s
    cpan_slurp           257/s
    read_file            323/s
    new                  468/s
    sysread_file         489/s
    new_scalar_ref       766/s
    new_buf_ref          767/s

上記の数値を見たときに得られる主要な結論は、ファイルをスカラーに 丸呑みするとき、スカラー・リファレンスによって結果を返すことによって、 ファイルが大きいほど多くの時間を節約することができるということです。 余分なバッファ・コピーは加算してしまいます。それほどない、より柔軟な 新しいモジュールのオーバーヘッドを明確にさせるために追加された非常に 単純なsysread_fileのエントリを除けば新しいモジュールは全ての中で トップになっています。リストを丸呑み、それからjoinするため、 file_contentsは常に最下位です。それは極めて遅い、古臭い初心者と お荷物崇拝(? cargo culted)スタイルです。またfile_contentsでの OOコードはさらに遅くさせています(私はこれを見せるために file_contents_no_OOエントリを入れています) 2つのCPANモジュールは小さいファイルについてはかなりのものです。 しかしファイルがより大くなると、新しいモジュールにくらべてのろまです。

小さいファイルのリスト丸呑み

    cpan_read_file          589/s
    cpan_slurp_to_array     620/s
    read_file               824/s
    new_array_ref           824/s
    sysread_file            828/s
    new                     829/s
    new_in_anon_array       833/s
    cpan_slurp_to_array_ref 836/s

大きいファイルのリスト丸呑み

    cpan_read_file          62.4/s
    cpan_slurp_to_array     62.7/s
    read_file               92.9/s
    sysread_file            94.8/s
    new_array_ref           95.5/s
    new                     96.2/s
    cpan_slurp_to_array_ref 96.3/s
    new_in_anon_array       97.2/s

これがおそらくこのベンチマークで最も面白い結果でしょう。5つの 異なるエントリが効率的にリードに結び付けられています。 論理的な結論はとしては、ファイルがどのように丸呑みされたかではなく、 入力を行に分割することが境界となる操作だということです。 これは新しいモジュールが明確な勝者でならない唯一のベンチマークです。 (大きなファイルのエントリでは勝者でした - 小さいファイルでは 僅差で2番目でした)。

注意: 全ての吐き出しエントリについてのベンチマークの情報では、 各行の終わりにある余分な数は、エントリ全体にかかる実時間での秒数です。 ベンチマークは少なくとも各エントリを2CPU秒実行します。以上に 大きい実時間は下記で説明します。

小さいファイルのスカラー吐き出し

    cpan_write_file 1035/s  38
    print_file      1055/s  41
    syswrite_file   1135/s  44
    new             1519/s  2
    print_join_file 1766/s  2
    new_ref         1900/s  2
    syswrite_file2  2138/s  2

大きいファイルのスカラー吐き出し

    cpan_write_file 164/s   20
    print_file      211/s   26
    syswrite_file   236/s   25
    print_join_file 277/s   2
    new             295/s   2
    syswrite_file2  428/s   2
    new_ref         608/s   2

スカラーの吐き出しエントリでは、スカラー・バッファへの リファレンスを渡されたとき、新しいモジュールAPIが勝っています。 syswrite_file2は小さいファイルでは、そのよりシンプルなコードにより、 それを打ち破っています。古いCPANモジュールは余分なデータのコピーと printを使っていることにより、最も遅くなっています。

小さいファイルのリスト吐き出し

    cpan_write_file  794/s  29
    syswrite_file   1000/s  38
    print_file      1013/s  42
    new             1399/s  2
    print_join_file 1557/s  2

大きいファイルのリスト吐き出し

    cpan_write_file 112/s   12
    print_file      179/s   21
    syswrite_file   181/s   19
    print_join_file 205/s   2
    new             228/s   2

ここでも、単純なprint_join_fileエントリが小さなリストのファイルへの 吐き出しのとき新しいモジュールを破っています。しかしファイルが大きくなると 新しいモジュールに負けています。最初に行の余分なコピーをおこない、それから 出力リストに対するprintを呼び出すために、それはprintにjoinによって 生成された単一のスカラーを渡すよりもだいぶ遅いのです、古いCPANモジュールは 他のものに遅れをとっています。print_fileエントリは直接@_を出力する ことの利点をあらわしています。そしてprint_join_fileはjoinの最適化を 加えています。

それでは長い実時間について考えていましょう。 吐き出しの全てのエントリのベンチマーク・コードを注意深く見れば いくつかは常に新しいファイルを出力し、あるものは既存のファイルを 上書きしていることがわかるでしょう。古いFile::Slurpがなぜoverwrite サブルーチンを持っているのかDavid Muirに聞いたところ、 彼はファイルの上書きによって、ファイルの中の幾分かは読み込むことができる ことが常に保証されると答えました。もし新しいファイルを作成すると、 ファイルが作成されたけれどもデータがないという瞬間があります。 しかし私はそれが十分な答えだとは感じませんでした。上書きの場合でも、 既存のファイルよりも小さいファイルを書き、新しい大きさにファイルを切り落とす ことができます。Windowsの種類(small race window)によっては、他のプロセスが ファイルの前のバージョンから残されたガラクタがついた新しいデータを 丸呑みすることがあるものもあります。これは一貫性のあるファイル・データを 確実にする唯一の方法はファイル・ロックを適切にするという点を 強くします。

しかしこれらの長い実時間についてはどうでしょう?そうです、それは全てファイルを 作成と既存のものを上書きの違いについてです。前のものは新しいiノード (あるいは他のファイルシステムでの同様のもの)を確保する必要があります。 そして後者は既存のiノードを再利用することができます。これは上書きすることは ディスクのシークと同時にCPU時間を節約することを意味します。実際、 ベンチマークを実行すると、吐き出し処理の間、iノードを確保するために ディスクが狂ったようになることを聞くことができます。このCPUと実時間の 両方のスピードアップは、新しいモジュールがファイルを吐き出すとき、 常に上書きを行うためです。これは適切な切捨ても行い(そしてこれは 既に以前書かれた大きいファイルの後で小さいものを吐き出すことにより、 テストの中でチェックされます)。overwriteサブルーチンはwrite_file への単なるtypeglobエイリアスであり、古いFile::Slurpモジュールとの 後方互換性のためにあります。

ベンチマークの結論

簡単なエントリがそれを破ってしまう2、3のケースを除けば、新しいFile::Slurpは スピードのリーダーあるいはリーダーの中の1つです。そのリファレンスによりバッファを 渡す特別なAPIはスピードアップにとても有効であることを証明しました。 また、sysread/syswriteの利用や出力行のjoinなど、その他の最適化を全て使 っています。私は広く丸呑みを使っている多くのプロジェクトが特に新しいAPIの 機能を得るようコードを書き換えたならば、スピードの改善に気が付くことを 期待しています。コードを触らなくても、そして簡単なAPIを使っていても、 著しいスピードアップを得るでしょう。

エラーの取り扱い

SlurpサブルーチンはファイルがオープンできるかやI/Oエラーのような状態を 気にしています。これらのエラーをどのように扱い、呼び出し元が何を見るのかは、 APIの設計での重要な視点です。丸呑みのための旧式のエラー扱いでは、 die()、もう少しよければcroak()を呼び出してきました。 しかし時には丸呑みにwarn()/carp()やエラーを扱うためのコードを 使って欲しいでしょう。ええ、これは致命的なエラーを捕まえるため 丸呑みをevalブロックで囲むことにより行うことができます。 しかし全ての人がその特別なコードが欲しいわけではありません。そこで私は エラー扱いを選択する別のオプションを全てのサブルーチンに追加しました。 もし'err_mode'オプションが'croak'であれば(これがデフォルトでもあります)、 呼ばれたサブルーチンはcroakします。'carp'を設定すればcarpが呼ばれます。 そのほかの文字列を設定すると(明示的にしたければ'quiet'を使ってください)、 エラー・ハンドラは何も呼ばれません。そして呼び出しものは呼び出しからの エラー・ステータスを使うことができます。

write_file()データのためには戻り値を使いません。そのため エラーを示すためにfalseステータスを返すことができます。 read_file()はその戻り値をデータのために使います。 しかしそれでもエラー・ステータスを戻させることができます。 スカラーモードでの正常終了した読み込みは定義されたデータ文字列か スカラーまたは配列へのリファレンスになります。そこでそのままの returnがここで機能するでしょう。しかしリスト・コンテキストで行で 丸呑みするのであれば、そのままのreturnは空リストを返してしまいます。 これは既存であっても空のファイルから取得したときと同じ値です。そこで、 read_file()は私が強く主張しているようなことをします。 つまり明示的なundefを返します。スカラー・コンテキストでは、 これはまだエラーを返します。そしてリスト・コンテキストでは、 戻された最初の値がundefとなり、それは先頭の要素としては正しい 値ではありません。そのためリスト・コンテキストも検知できるエラー・ ステータスを得ることができます:

    my @lines = read_file( $file_name, err_mode => 'quiet' ) ;
    your_handle_error( "$file_name can't be read\n" ) unless
                    @lines && defined $lines[0] ;

File::FastSlurp

    sub read_file {

        my( $file_name, %args ) = @_ ;

        my $buf ;
        my $buf_ref = $args{'buf_ref'} || \$buf ;

        my $mode = O_RDONLY ;
        $mode |= O_BINARY if $args{'binmode'} ;

        local( *FH ) ;
        sysopen( FH, $file_name, $mode ) or
                    carp "Can't open $file_name: $!" ;

        my $size_left = -s FH ;

        while( $size_left > 0 ) {

            my $read_cnt = sysread( FH, ${$buf_ref},
                    $size_left, length ${$buf_ref} ) ;

            unless( $read_cnt ) {

                carp "read error in file $file_name: $!" ;
                last ;
            }

            $size_left -= $read_cnt ;
        }

    # voidコンテキストの取り扱い(バッファ・リファレンスによりスカラーを返す)

        return unless defined wantarray ;

    # リスト・コンテキストの取り扱い

        return split m|?<$/|g, ${$buf_ref} if wantarray ;

    # スカラー・コンテキストの取り扱い

        return ${$buf_ref} ;
    }

    sub write_file {

        my $file_name = shift ;

        my $args = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
        my $buf = join '', @_ ;


        my $mode = O_WRONLY ;
        $mode |= O_BINARY if $args->{'binmode'} ;
        $mode |= O_APPEND if $args->{'append'} ;

        local( *FH ) ;
        sysopen( FH, $file_name, $mode ) or
                    carp "Can't open $file_name: $!" ;

        my $size_left = length( $buf ) ;
        my $offset = 0 ;

        while( $size_left > 0 ) {

            my $write_cnt = syswrite( FH, $buf,
                    $size_left, $offset ) ;

            unless( $write_cnt ) {

                carp "write error in file $file_name: $!" ;
                last ;
            }

            $size_left -= $write_cnt ;
            $offset += $write_cnt ;
        }

        return ;
    }

Perl 6での丸呑み

Perl 6では通常、この記事でかかれたことの多くはお払い箱になるでしょう。 Perl 6はファイル・ハンドルにに'slurp'オプションを設定することを可能とし、 そのようなハンドルから読み込むとき、ファイルは丸呑みされます。リストと スカラーのコンテキストは、サポートされます。そのため行やスカラーに 丸呑みすることができます。特別なコードを呼び出すきっかけに'slurp'プロパティを 使うことが出来るので、私はPerl 6での丸呑みのサポートが最適化され、 stdioサブシステムを回避することを期待しています。 そうでなければ、単に何人かの冒険心に富んだ人たちがPerl 6のための File::FastSlurpをつくるでしょう。Perl 5 モジュールでのコードは簡単に Perl 6の文法と意味に修正できます。ボランティアはいませんか?

まとめて

古臭い行単位の処理とファイル全体をメモリ上で扱うことを比較してきました。 ファイルの丸呑みは、適切におこなわれれば、あなたのプログラムを スピードアップさせ、コードを簡単にします。途方もないファイル(ログ、 DNAシーケンスなど)やどれくらいのデータを読み込むのか分からないSTDINを 丸呑みしないことに気が付くべきです。しかしメガバイトの大きさのファイルを 丸呑みすることは典型的な量のRAMがインストールされている今日のシステムでは 大きな問題にはならないでしょう。Perlが最初に深く使われるようになったとき (Per 4)、丸呑みは10年前のRAMサイズより小さいものに制限されていました。 設定、ソースコード、データなど中に何が入っているかに関わらず、 ほどほどの大きさのファイルであればほとんど丸呑みすることができます。

謝辞

翻訳者

川合孝典 ([email protected])

翻訳の言い訳

この中ではslupingを「丸呑み」とし、反対の出力のものをspewingを「吐き出し」、 burpingを「丸出し」としてみました。他にいい訳語を思いついたら教えてください。