web-dev-qa-db-ja.com

テキストファイルの整数を合計する最速の方法

質問

大きなASCIIテキストファイルがあり、各行に0から1,000,000,000の範囲のランダムな非負整数があるとします。ファイルには100,000,000行あります。最も速い方法は何ですか。ファイルを読み、すべての整数の合計を計算するには?

制約:使用するRAMは10MBです。ファイルのサイズは1GBなので、全部を読み込んで処理する必要はありません。

これが私が試したさまざまなソリューションです。結果はかなり驚くべきものでした。

私が逃したより速い何かがありますか?

ご注意ください:以下に示すタイミングはすべてアルゴリズムを実行するためのものです10回合計(1回実行して破棄、タイマーを開始、10回実行、タイマーを停止)。マシンはかなり遅いCore 2 Duoです。

方法1:自然なアプローチ

最初に試すことは明白なアプローチです:

private long sumLineByLine() throws NumberFormatException, IOException {
    BufferedReader br = new BufferedReader(new FileReader(file));
    String line;
    long total = 0;
    while ((line = br.readLine()) != null) {
        int k = Integer.parseInt(line);
        total += k;
    }
    br.close();
    return total;
}

可能な最大戻り値は10 ^ 17ですが、これはlongに簡単に収まるため、オーバーフローを心配する必要はありません。

私のマシンでは、これを11回実行し、最初の実行を割り引くには約92.9秒かかります。

方法2:マイナーツイーク

この質問 へのコメントに触発されて、私は新しいint kを作成せずに、行の解析結果を保存する代わりに、解析された値をtotalに直接追加しました。したがって、この:

    while ((line = br.readLine()) != null) {
        int k = Integer.parseInt(line);
        total += k;
    }

これになる:

    while ((line = br.readLine()) != null)
        total += Integer.parseInt(line);

これは何の違いもないと確信しており、コンパイラが2つのバージョンに対して同じバイトコードを生成する可能性が非常に高いと考えました。しかし、驚いたことに、それは少しの時間を節約しました:92.1秒になりました。

方法3:整数を手動で解析する

これまでにコードについて気になっていたことの1つは、Stringintに変換し、最後に追加することです。私たちが進むにつれて、追加する方が速くないのではないでしょうか? Stringを自分で解析するとどうなりますか?このようなもの...

private long sumLineByLineManualParse() throws NumberFormatException,
        IOException {
    BufferedReader br = new BufferedReader(new FileReader(file));
    String line;
    long total = 0;
    while ((line = br.readLine()) != null) {
        char chs[] = line.toCharArray();
        int mul = 1;
        for (int i = chs.length - 1; i >= 0; i--) {
            char c = chs[i];
            switch (c) {
            case '0':
                break;
            case '1':
                total += mul;
                break;
            case '2':
                total += (mul << 1);
                break;
            case '4':
                total += (mul << 2);
                break;
            case '8':
                total += (mul << 3);
                break;
            default:
                total += (mul*((byte) c - (byte) ('0')));   
            }
            mul*=10;
        }
    }
    br.close();
    return total;
}

これは、特に乗算を行うためのビットシフトの最適化によって、少し時間を節約できると思いました。しかし、文字配列への変換のオーバーヘッドは、あらゆる利益を圧倒する必要があります。これには、現在148.2秒かかります。

方法4:バイナリでの処理

最後に、ファイルをバイナリデータとして処理することができます。

整数の長さを知らなければ、前から整数を解析するのは面倒です。逆方向の解析ははるかに簡単です。最初の数字は単位で、次の数字は10です。したがって、全体にアプローチする最も簡単な方法は、ファイルを逆に読むことです。

(たとえば)8MBのbyte[]バッファーを割り当てると、ファイルの最後の8MBでそれを埋め、処理してから、前の8MBを読み取ることができます。次のブロックに移動するときに解析の最中の数値を台無しにしないように少し注意する必要がありますが、それが唯一の問題です。

ある数字に出会ったら、それを(数字の適切な位置に応じて適切に乗算して)合計に追加し、係数に10を乗算して、次の数字の準備を整えます。数字ではない何か(CRまたはLF)に遭遇した場合は、係数をリセットするだけです。

private long sumBinary() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[8*1024*1024];
    int mul = 1;
    long total = 0;
    while (lastRead>0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead-len);
        raf.readFully(buf, 0, len);
        lastRead-=len;
        for (int i=len-1; i>=0; i--) {
            //48 is '0' and 57 is '9'
            if ((buf[i]>=48) && (buf[i]<=57)) {
                total+=mul*(buf[i]-48);
                mul*=10;
            } else
                mul=1;
        }
    }
    raf.close();
    return total;
}

これは30.8秒で実行されます!これは、3倍の速度の増加です。

フォローアップの質問

  1. なぜこれがはるかに速いのですか?私はそれが勝つことを期待していましたが、それほど印象的ではありませんでした。それは主にStringへの変換のオーバーヘッドですか?そして、キャラクターセットなどの裏側のすべての心配ですか?
  2. MappedByteBufferを使用して、これより良いことはできますか?特にバッファーから逆方向に読み取る場合、バッファーから読み取るメソッドを呼び出すオーバーヘッドによって処理速度が低下すると感じています。
  3. 逆方向ではなく順方向にファイルを読み取る方が良いでしょうか?それでもバッファを逆方向にスキャンしますか?アイデアは、ファイルの最初のチャンクを読み取ってから逆方向にスキャンし、最後に半数を破棄するというものです。次に、次のチャンクを読み取るときに、破棄した数の先頭から読み取るようにオフセットを設定します。
  4. 私が考えていないことが何か大きな違いを生む可能性があるのでしょうか?

更新:より意外な結果

まず、観察。以前にそれが発生したはずですが、Stringベースの読み取りが非効率的である理由は、すべてのStringオブジェクトを作成するのにかかる時間ではなく、短期間であるという事実です。ガベージコレクターが処理する1億個。それはそれを混乱させるにちがいない。

今、人々が投稿した回答/コメントに基づいたいくつかの実験。

私はバッファのサイズで不正行為をしていますか?

1つの提案は、BufferedReaderは16KBのデフォルトバッファーを使用し、8MBのバッファーを使用したため、likeとlikeを比較していないということです。より大きなバッファーを使用すると、より高速になるはずです。

ここに衝撃があります。 sumBinary()メソッド(メソッド4)は、8MBバッファーを使用して、昨日30.8秒で実行されました。今日、コードは変更されていません。風向が変更され、30.4秒になりました。バッファサイズを16KBに下げて、どれほど遅くなるかを確認すると、速くなります!これで23.7秒。クレイジー。その人が来るのを見たのは誰ですか?

少し実験をすると、16KBがほぼ最適であることがわかります。たぶんJavaみんな同じ実験をしたので、16KBでした!

問題はI/Oバウンドですか?

これも気になった。ディスクアクセスに費やされた時間と、数値の計算に費やされた時間はどれくらいですか。提案された回答の1つに対する十分にサポートされたコメントによって示唆されているように、それがほとんどすべてのディスクアクセスである場合、私たちが何をしても、あまり改善することはできません。

これは、すべての解析と数値処理をコメントアウトしてコードを実行することで簡単にテストできますが、読み取りはそのままです。

private long sumBinary() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int mul = 1;
    long total = 0;
    while (lastRead > 0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead - len);
        raf.readFully(buf, 0, len);
        lastRead -= len;
        /*for (int i = len - 1; i >= 0; i--) {
            if ((buf[i] >= 48) && (buf[i] <= 57)) {
                total += mul * (buf[i] - 48);
                mul *= 10;
            } else
                mul = 1;
        }*/
    }
    raf.close();
    return total;
}

これは3.7秒で実行されます!これは、I/Oに拘束されているようには見えません。

もちろん、I/O速度の一部はディスクキャッシュヒットによるものです。しかし、それは本当の意味ではありません。CPU時間は20秒かかります(これもLinuxのtimeコマンドを使用して確認されています)。これは、CPU時間を削減するのに十分な大きさです。

後方スキャンではなく前方スキャン

元の投稿では、ファイルを順方向ではなく逆方向にスキャンするのに十分な理由があると考えていました。私はそれをあまりよく説明しませんでした。これは、数値を前方にスキャンする場合、スキャンした数値の合計値を累積してから加算する必要があるという考えでした。逆方向にスキャンする場合は、進むにつれて累積合計に追加できます。私の潜在意識はそれ自体に何らかの意味を成していましたが(それについては後で)回答の1つで指摘されている1つの重要なポイントを逃しました:逆方向にスキャンするために、反復ごとに2つの乗算を行っていましたが、前方にスキャンする場合、必要なのは1つだけです。だから私はフォワードスキャンバージョンをコード化しました:

private long sumBinaryForward() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int fileLength = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int acc = 0;
    long total = 0;
    int read = 0;
    while (read < fileLength) {
        int len = Math.min(buf.length, fileLength - read);
        raf.readFully(buf, 0, len);
        read += len;
        for (int i = 0; i < len; i++) {
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                total += acc;
                acc = 0;
            }
        }
    }
    raf.close();
    return total;
}

これは20.0秒で実行され、バックワードスキャンバージョンを少しだけ打ち破ります。いいね。

乗算キャッシュ

しかし、夜間に気付いたのは、反復ごとに2つの乗算を実行しているにもかかわらず、これらの乗算を格納するためにキャッシュを使用する可能性があるため、逆方向反復中にそれらを実行する必要がないということです。目が覚めたとき、誰かが同じ考えを持っていたのを見て嬉しかったです!

重要なのは、スキャンしている数値には最大で10桁があり、可能な数字は10桁しかないため、累積合計に対する数字の値の可能性は100しかないということです。これらを事前に計算してから、逆方向スキャンコードで使用できます。乗算を完全に取り除くことができたので、それは順方向走査バージョンに勝るはずです。 (乗算はアキュムレータで行われるため、これはフォワードスキャンでは実行できないことに注意してください。これは、10 ^ 9までの任意の値を取る可能性があります。両方のオペランドがいくつかの可能性に制限されているのは、バックワードの場合のみです。)

private long sumBinaryCached() throws IOException {
    int mulCache[][] = new int[10][10];
    int coeff = 1;
    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 10; j++)
            mulCache[i][j] = coeff * j;
        coeff *= 10;
    }

    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int mul = 0;
    long total = 0;
    while (lastRead > 0) {
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead - len);
        raf.readFully(buf, 0, len);
        lastRead -= len;
        for (int i = len - 1; i >= 0; i--) {
            if ((buf[i] >= 48) && (buf[i] <= 57))
                total += mulCache[mul++][buf[i] - 48];
            else
                mul = 0;
        }
    }
    raf.close();
    return total;
}

これは26.1秒で実行されます。控えめに言ってもがっかりです。逆方向の読み取りはI/Oの点では効率的ではありませんが、I/Oがここでの大きな頭痛の種ではないことがわかりました。これは大きなポジティブな変化をもたらすと期待していました。おそらく、配列のルックアップは、置き換えた乗算と同じくらい高価です。 (私は配列を16x16にして、ビットシフトを使用してインデックスを作成しようとしましたが、役に立ちませんでした。)

フォワードスキャンが重要な位置を占めているようです。

MappedByteBufferの使用

次に追加するのはMappedByteBufferで、生のRandomAccessFileを使用するよりも効率的かどうかを確認します。コードを大幅に変更する必要はありません。

private long sumBinaryForwardMap() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    byte buf[] = new byte[16 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    int acc = 0;
    long total = 0;
    while (mb.hasRemaining()) {
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        for (int i = 0; i < len; i++)
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                total += acc;
                acc = 0;
            }
    }
    ch.close();
    raf.close();
    return total;
}

これは少し改善するようです:19.0秒になりました。私たちは私たちの個人的なベストからさらに1秒を取りました!

マルチスレッドについてはどうですか?

提案された答えの1つは、複数のコアの使用を含みます。それが私に起こらなかったことを少し恥ずかしいです!

それはI/Oバウンドの問題であるという仮定のため、答えはいくつかの棒のために来ました。これは、I/Oの結果に照らして、少し厳しいようです。いずれにしても、試してみる価値は確かにあります。

これは、fork/joinを使用して行います。ファイルの一部の計算結果を表すクラスは次のとおりです。左側に部分的な結果(数値の途中から始めた場合)と右側に(部分的な結果がある場合)バッファは数値の途中で終了しました)。このクラスには、このような2つの結果を結合して、2つの隣接するサブタスクの結合された結果を生成するためのメソッドもあります。

private class SumTaskResult {
    long subtotal;
    int leftPartial;
    int leftMulCount;
    int rightPartial;

    public void append(SumTaskResult rightward) {
        subtotal += rightward.subtotal + rightPartial
                * rightward.leftMulCount + rightward.leftPartial;
        rightPartial = rightward.rightPartial;
    }
}

ここで重要なのは、結果を計算するRecursiveTaskです。小さな問題(64文字未満)の場合は、computeDirectly()を呼び出して単一のスレッドで結果を計算します。より大きな問題の場合は、2つに分割され、2つのサブ問題が別々のスレッドで解決され、結果が結合されます。

private class SumForkTask extends RecursiveTask<SumTaskResult> {

    private byte buf[];
    // startPos inclusive, endPos exclusive
    private int startPos;
    private int endPos;

    public SumForkTask(byte buf[], int startPos, int endPos) {
        this.buf = buf;
        this.startPos = startPos;
        this.endPos = endPos;
    }

    private SumTaskResult computeDirectly() {
        SumTaskResult result = new SumTaskResult();
        int pos = startPos;

        result.leftMulCount = 1;

        while ((buf[pos] >= 48) && (buf[pos] <= 57)) {
            result.leftPartial = result.leftPartial * 10 + buf[pos] - 48;
            result.leftMulCount *= 10;
            pos++;
        }

        int acc = 0;
        for (int i = pos; i < endPos; i++)
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else {
                result.subtotal += acc;
                acc = 0;
            }

        result.rightPartial = acc;
        return result;
    }

    @Override
    protected SumTaskResult compute() {
        if (endPos - startPos < 64)
            return computeDirectly();
        int mid = (endPos + startPos) / 2;
        SumForkTask left = new SumForkTask(buf, startPos, mid);
        left.fork();
        SumForkTask right = new SumForkTask(buf, mid, endPos);
        SumTaskResult rRes = right.compute();
        SumTaskResult lRes = left.join();
        lRes.append(rRes);
        return lRes;
    }

}

これは、MappedByteBuffer全体ではなく、byte[]で動作していることに注意してください。その理由は、ディスクアクセスをシーケンシャルに保ちたいからです。かなり大きなチャンクをとり、フォーク/ジョインして、次のチャンクに移動します。

これを行うメソッドは次のとおりです。バッファーサイズを最大1 MBにプッシュしていることに注意してください(以前は最適ではありませんが、ここではより賢明です)。

private long sumBinaryForwardMapForked() throws IOException {
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    ForkJoinPool pool = new ForkJoinPool();

    byte buf[] = new byte[1 * 1024 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    SumTaskResult result = new SumTaskResult();
    while (mb.hasRemaining()) {
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        SumForkTask task = new SumForkTask(buf, 0, len);
        result.append(pool.invoke(task));
    }
    ch.close();
    raf.close();
    pool.shutdown();
    return result.subtotal;
}

これが魂を破壊する失望です:このうまくマルチスレッド化されたコードは今32.2秒かかります。なぜそんなに遅いのですか?私はひどく間違ったことをしたと思って、これをデバッグするのにかなりの時間を費やしました。

必要なのは1つの小さなTweakだけでした。小さな問題と大きな問題の間の64のしきい値は妥当なものだと思いました。それはまったくばかげたことでした。

このように考えてください。サブ問題はまったく同じサイズであるため、ほぼ同時に完了するはずです。そのため、利用可能なプロセッサの数を超える数に分割しても意味がありません。私が使用しているマシンでコアが2つしかない場合、しきい値を64に下げるのはばかげています。オーバーヘッドが増えるだけです。

これで、利用可能なコアが2つ以上ある場合でも2つのコアのみを使用するように制限したくありません。おそらく正しいことは、実行時にプロセッサの数を調べ、その数に分割することです。

いずれにしても、しきい値を512KB(バッファサイズの半分)に変更すると、13.3秒で完了します。 128KBまたは64KBに下げると、より多くのコアを使用できるようになり(それぞれ最大8または16)、ランタイムに大きな影響はありません。

したがって、マルチスレッドは大きな違いをもたらします

かなり長い道のりでしたが、92.9秒かかったところから始めて、今では13.3秒になりました...これは7倍の速度です元のコードの。そして、それは、最初から線形(最適)であった漸近的な(big-Oh)時間の複雑さを改善することによるのではありません...

良い一日の仕事。

次にGPUを試してみるべきだと思います...

追記:乱数のファイルを生成する

次のコードで乱数を生成し、実行してファイルにリダイレクトしました。明らかに、あなたが私が持っていたのとまったく同じ乱数で終わることを保証することはできません:)

public static void genRandoms() {
    Random r = new Random();
    for (int i = 0; i < 100000000; i++)
        System.out.println(r.nextInt(1000000000));
}
49

これには別の方法があると思います。

これは、古典的な複数プロセスプログラミングの問題です。 C言語には、この種の問題を解決するライブラリMPI=があります。

それのアイデアは、例えば4つの部分で整数のリストをチャンクすることであり、すべての部分は異なるプロセスによって合計されます。終了後、プロセスは合計されます。

Javaでは、これはスレッド(疑似並列)とJava同時実行で実行できます。

たとえば、リストの4つの異なる部分を合計する4つの異なるスレッド。最後に、それらは一緒に合計されます。

電話会社は、この種の並列プログラミング技術を実行するグ​​リッドコンピューターを使用して、トランザクションを合計しています。

ここでの唯一の問題(ボトルネック)はIO操作です。ファイルの読み取りには時間がかかります。どういうわけか、複数のスレッドにを異なるように読み取らせることができますファイルの一部...これは非常に複雑なアプローチであり、多くのスレッドで使用されているからといってディスクが高速で回転しないため、これはあまりうまくいかないと思いますが、同様のことを行う他のテクニックがあります。これについては、こちらをお読みください: 複数のスレッドを介してファイルにアクセスする およびここ 複数のスレッドで単一のファイルを読み取る:高速化する必要がありますか?

3
anchor

主なボトルネックはファイルIOです。ファイルI/Oがディスクを待機している間に別のスレッドで実行できるため、数値を解析して合計することはアルゴリズムに影響を与えないはずです。

数年前、私はファイルから可能な限り最速の方法でファイルを読み取る方法を調査し、いくつかの優れたアドバイスに出会いました-以下のようにスキャンルーチンとして実装しました:

// 4k buffer size.
static final int SIZE = 4 * 1024;
static byte[] buffer = new byte[SIZE];

// Fastest because a FileInputStream has an associated channel.
private static void ScanDataFile(Hunter p, FileInputStream f) throws FileNotFoundException, IOException {
    // Use a mapped and buffered stream for best speed.
    // See: http://nadeausoftware.com/articles/2008/02/Java_tip_how_read_files_quickly
    final FileChannel ch = f.getChannel();
    long red = 0L;
    do {
        final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
        final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
        int nGet;
        while (mb.hasRemaining() && p.ok()) {
            nGet = Math.min(mb.remaining(), SIZE);
            mb.get(buffer, 0, nGet);
            for (int i = 0; i < nGet && p.ok(); i++) {
                p.check(buffer[i]);
                //size += 1;
            }
        }
        red += read;
    } while (red < ch.size() && p.ok());
    // Finish off.
    p.close();
    ch.close();
    f.close();
}

データを探すためにHunterと呼ばれるインターフェース化されたオブジェクトを利用しているので、速度をテストする前にこの手法を調整することをお勧めします。

ご覧のとおり、アドバイスは2008年に導出されたものであり、Java以降、多くの機能強化が行われているため、改善が見られない可能性があります。

追加されました

私はこれをテストしていませんが、これはあなたのテストに適合し、同じテクニックを使用するはずです:

class Summer {

    long sum = 0;
    long val = 0;

    public void add(byte b) {
        if (b >= '0' && b <= '9') {
            val = (val * 10) + (b - '0');
        } else {
            sum += val;
            val = 0;
        }
    }

    public long getSum() {
        return sum + val;
    }
}

private long sumMapped() throws IOException {
    Summer sum = new Summer();
    FileInputStream f = new FileInputStream(file);
    final FileChannel ch = f.getChannel();
    long red = 0L;
    do {
        final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
        final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
        int nGet;
        while (mb.hasRemaining()) {
            nGet = Math.min(mb.remaining(), SIZE);
            mb.get(buffer, 0, nGet);
            for (int i = 0; i < nGet; i++) {
                sum.add(buffer[i]);
            }
        }
        red += read;
    } while (red < ch.size());
    // Finish off.
    ch.close();
    f.close();
    return sum.getSum();
}
11
OldCurmudgeon

なぜこれがずっと速いのですか?

文字列の作成は、小さな数学よりもはるかにコストがかかります。

MappedByteBufferヘルプを使用して、これより良いことはできますか?

少し、はい。その私が使用するもの。メモリからメモリへのコピーを保存します。つまり、byte []は必要ありません。

バッファーから読み取るメソッドを呼び出すオーバーヘッドが物事を遅くするだろうと私は感じています

単純な場合、メソッドはインライン化されます。

特に、バッファーから逆方向に読み取る場合。

2つではなく1つの*を使用するため、実際には転送の解析がよりシンプル/高速になります。

逆方向ではなく順方向にファイルを読み取る方が良いでしょうか?それでもバッファを逆方向にスキャンしますか?

なぜ後ろ向きに読む必要があるのか​​、私にはわかりません。

アイデアは、ファイルの最初のチャンクを読み取ってから逆方向にスキャンし、最後に半数を破棄するというものです。次に、次のチャンクを読み取るときに、破棄した数の先頭から読み取るようにオフセットを設定します。

不必要に複雑に聞こえます。私は1回のパスでファイル全体のメモリマッピングを一度に読み取ります。ファイルのサイズが2 GB以上でない限り、チャンクを使用する必要はありません。それでも私は1つのパスで読みます。

私が考えていないことが何か大きな違いを生む可能性があるのでしょうか?

データがディスクキャッシュにある場合は、他の何よりも違いが大きくなります。

9
Peter Lawrey

バッファサイズを大きくして、String(Unicodeへ)をより高速にコーディングできます。

BufferedReader br = new BufferedReader(new InputStreamReader(
        new FileInputStream(file), StandardCharsets.US_ASCII),
        1_024_000_000);

バイナリのInputStream/RandomAccessFileを使用して文字列の使用を排除する方法は価値があります。

次に、ソースファイルが圧縮の場合も、Niceになる可能性があります。 Unixでは、gzip形式を選択します。ここで、xxx.txt.gzxxx.txtに解凍されます。それはGZipInputStreamで読むことができます。サーバーのディレクトリとの間のファイル転送が全体的に高速化されるという利点があります。

4
Joop Eggen

このコメント に基づいて、「単純にすべてのバイトを合計する方が速い」と、私は受け入れられた回答のバリエーションを提案します。

受け入れられた答えは、問題をチャンクに分割し、マルチスレッドを使用して各チャックの合計を計算し、最後にそれらを加算することを提案しています。

このアイデアは、逆引きスキャンで乗算の数をO(1)に削減するために使用できます。テーブルの検索やスレッド化は行われません(またはスレッド化と組み合わせます)。単純に乗算が加算より分散する方法を利用して、1のすべての数字を1つのアキュムレータに追加し、10を個別の1に追加し、数百および数千を独自のアキュムレータに追加します。これには、乗算は一切ありません。

複数のスレッドからの結果を組み合わせる削減ステップは、場所ごとのアキュムレータを使用して実行することもできます。合計を計算する最後のステップでは乗算が必要になります(または、10に2ビットしか設定されておらず、ビットシフトと加算を使用するという事実を利用します)が、9回の乗算で十分です。

2
Ben Voigt

ソース: http://nadeausoftware.com/articles/2008/02/Java_tip_how_read_files_quickly

最高のJava読み取りパフォーマンスを得るには、4つのことを覚えておく必要があります。

  • 一度に1バイトではなく配列を一度に読み取ることにより、I/O操作を最小限に抑えます。 8Kバイトの配列が適切なサイズです。
  • 一度に1バイトではなく配列でデータを取得することにより、メソッド呼び出しを最小限に抑えます。配列のインデックスを使用して、配列のバイト数を取得します。
  • スレッドセーフが必要ない場合は、スレッド同期ロックを最小限に抑えます。スレッドセーフクラスへのメソッド呼び出しを少なくするか、FileChannelやMappedByteBufferなどの非スレッドセーフクラスを使用します。
  • JVM/OS、内部バッファ、アプリケーションアレイ間でのデータのコピーを最小限に抑えます。 FileChannelをメモリマッピングで使用するか、直接またはラップされた配列ByteBufferを使用します。
2
Margus

ここにはいくつかの問題があります。

  1. 行の読み取りに基づくソリューションは、各文字を2回処理します。たとえば、コンパイラはこれを行わず、一度に1つの文字を読み取り、その文字を直接ディスパッチします。
  2. readLine()に基づくソリューションは、文字列を作成します。
  3. 異なるバッファーサイズを使用しています。
  4. さまざまなI/Oテクノロジを使用しています。
  5. 文字変換を使用している場合もあれば、使用していない場合もあります。
  6. ファイルを過度に分析しています。数値が互いに分離されている限り、空白がどこにあるのか、またはどれだけあるのかは気にしません。

私の解決策:

    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8*1024*1024/2);
    long    total = 0;
    int i;
    while ((i = bis.read()) != -1)
    {
        byte    b = (byte)i;
        long    number = 0;
        while (b >= '0' && b <= '9')
        {
            number = number*10+b-'0';
            if ((i = bis.read()) == -1)
                break;
            b = (byte)i;
        }
        total += number;
    }
1
user207421