Zen言語の標準ライブラリ紹介〜Buffered I/O Stream〜

はじめに

けっこう標準ライブラリが充実しているわけですが、ドキュメントがないのがもったいないですね。 まとまった時間が取れないので、ちょこちょこ書いていくシリーズです。

リクエストあれば、優先する、かも?

ファイルへの入出力をバッファリングして、大量のデータを高速に読み書きします。

Unbuffered I/O Stream

まず、バッファリングしない方です。 次のプログラムは、標準入力から1行ずつデータを受け取って、標準出力にそのまま出力します。

const std = @import("std");
const fs = std.fs;

pub fn main() anyerror!void {
    var stdin = try fs.getStdIn();

    var stdout = try fs.getStdOut();

    var buffer: [4096]u8 = undefined;
    while (true) {
        const line = fs.read.until(&stdin, &buffer, '\n') catch |err| switch (err) {
            error.EndOfStream => break,
            else => return err,
        };
        try fs.write.print(&stdout, "{}\n", .{ line });
    }
}

さて、ここに250MBほどの入力ファイルがあります。

$ ls -lha input25000000.txt 
-rw-r--r-- 1 tomoyuki tomoyuki 243M Feb 11 21:15 input25000000.txt

$ head input25000000.txt 
>ONE Homo sapiens alu
GGCCGGGCGCGGTGGCTCACGCCTGTAATCCCAGCACTTTGGGAGGCCGAGGCGGGCGGA
TCACCTGAGGTCAGGAGTTCGAGACCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACT
AAAAATACAAAAATTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCAGCTACTCGGGAG
GCTGAGGCAGGAGAATCGCTTGAACCCGGGAGGCGGAGGTTGCAGTGAGCCGAGATCGCG
CCACTGCACTCCAGCCTGGGCGACAGAGCGAGACTCCGTCTCAAAAAGGCCGGGCGCGGT
GGCTCACGCCTGTAATCCCAGCACTTTGGGAGGCCGAGGCGGGCGGATCACCTGAGGTCA
GGAGTTCGAGACCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACTAAAAATACAAAAA
TTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCAGCTACTCGGGAGGCTGAGGCAGGAG
AATCGCTTGAACCCGGGAGGCGGAGGTTGCAGTGAGCCGAGATCGCGCCACTGCACTCCA

実行してみると、私の環境では約3分ほどかかることがわかりました。

$ time ./zen-cache/bin/buffered-io < input25000000.txt > output.txt

real    2m48.474s
user    1m14.399s
sys     1m33.866s

Buffered I/O Stream

続いてバッファリングする方です。

Zenの標準ライブラリ (std/io) には、BufferedInStreamBufferedOutStreamというそのままの構造体が定義されています。 (正確には、任意のエラー型に対応する構造体を返すジェネリック関数です)

BufferedInStreamunbuffered_in_streamフィールドを、BufferedOutStreamunbuffered_out_streamフィールドをそれぞれ渡して初期化します。 今回の場合は、どちらもstd.fs.File構造体の参照を渡しています。

const std = @import("std");
const fs = std.fs;
const File = fs.File;
const io = std.io;
const BufferedOutStream = io.BufferedOutStream;
const BufferedInStream = io.BufferedInStream;

pub fn main() anyerror!void {
    var unbeffered_stdin = try fs.getStdIn();
    var stdin = BufferedInStream(File.ReadError){
        .unbuffered_in_stream = &unbeffered_stdin
    };

    var unbuffered_stdout = try fs.getStdOut();
    var stdout = BufferedOutStream(File.WriteError){
        .unbuffered_out_stream = &unbuffered_stdout
    };
    defer _ = stdout.flush() catch unreachable;

    var buffer: [4096]u8 = undefined;
    while (true) {
        const line = fs.read.until(&stdin, &buffer, '\n') catch |err| switch (err) {
            error.EndOfStream => break,
            else => return err,
        };
        try fs.write.print(&stdout, "{}\n", .{ line });
    }
}

とりあえず、実行してみましょう。 今度は20秒強でプログラムが停止します。 速さが段違いですね!

$ time ./zen-cache/bin/buffered-io < input25000000.txt > output.txt

real    0m22.351s
user    0m21.641s
sys     0m0.508s

ここで重要なことは、バッファリングなしの場合でも、バッファリングする場合でも、初期化以外では同じコードが使えている、ということです。 2つのコードを見比べて見て下さいね。

std.io.InStream / std.io.OutStream

std.io.InStreamは任意のエラー型を関連型として持つインタフェースです。 このインタフェースはread()メソッドを持ちます。

std.fs.Filestd.io.BufferedInStreamも両方、std.io.InStream(std.fs.File)インタフェースを実装しています。 そのため、両者では初期化以外、同じコードが使えるのですね。

pub fn InStream(comptime ReadError: type) type {
    return interface {
        fn read(buf: []u8) ReadError![]u8;
    };
}

std.io.OutStreamも同様です。