web-dev-qa-db-ja.com

テキストの行数を見つける最速の方法(C ++)

そのファイルでいくつかの操作を行う前に、ファイルの行数を読み取る必要があります。ファイルを読み取って、繰り返しごとにline_count変数をインクリメントしようとすると、eofに到達します。私の場合、それほど速くはありませんでした。 ifstreamとfgetsの両方を使用しました。彼らはどちらも遅かった。これを行うためのハッキーな方法はありますか?これは、たとえば、BSD、Linuxカーネル、またはバークレーdbでも使用されます(ビット単位の操作を使用する場合があります)。

前に言ったように、そのファイルには何百万行もあり、それが大きくなり続けると、各行は約40または50文字になります。 Linuxを使用しています。

注:DBバカを使用すると言う人もいると思います。しかし、私の場合、簡単に言えば、dbを使用できません。

22
systemsfault

行数を見つける唯一の方法は、ファイル全体を読み取り、行末文字の数を数えることです。これを行うための最も速い方法は、1回の読み取り操作でファイル全体を大きなバッファーに読み取り、 '\ n'文字を数えるバッファーを通過することです。

現在のファイルサイズは約60Mbと思われるため、これは魅力的なオプションではありません。ファイル全体を読み取るのではなく、チャンク単位で読み取ることで、ある程度の速度を得ることができます。たとえば、サイズは1MBです。また、データベースは問題外であるとも言われていますが、それは本当に最良の長期的な解決策になりそうです。

編集:これについて小さなベンチマークを実行しただけで、バッファアプローチ(バッファサイズ1024K)を使用すると、getline()で一度に1行ずつ読み取るよりも2倍以上速いようです。これがコードです-私のテストは-O2最適化レベルを使用してg ++で行われました:

#include <iostream>
#include <fstream>
#include <vector>
#include <ctime>
using namespace std;

unsigned int FileRead( istream & is, vector <char> & buff ) {
    is.read( &buff[0], buff.size() );
    return is.gcount();
}

unsigned int CountLines( const vector <char> & buff, int sz ) {
    int newlines = 0;
    const char * p = &buff[0];
    for ( int i = 0; i < sz; i++ ) {
        if ( p[i] == '\n' ) {
            newlines++;
        }
    }
    return newlines;
}

int main( int argc, char * argv[] ) {
    time_t now = time(0);
    if ( argc == 1  ) {
        cout << "lines\n";
        ifstream ifs( "lines.dat" );
        int n = 0;
        string s;
        while( getline( ifs, s ) ) {
            n++;
        }
        cout << n << endl;
    }
    else {
        cout << "buffer\n";
        const int SZ = 1024 * 1024;
        std::vector <char> buff( SZ );
        ifstream ifs( "lines.dat" );
        int n = 0;
        while( int cc = FileRead( ifs, buff ) ) {
            n += CountLines( buff, cc );
        }
        cout << n << endl;
    }
    cout << time(0) - now << endl;
}
17
anon

C++ stl文字列とgetline(またはCのfgets)を使用しないでください。Cスタイルのrawポインターのみを使用し、ページサイズのチャンクでブロックするか、ファイルをmmapします。

次に、システムのネイティブのWordサイズ(つまり、uint32_tまたはuint64_t)で マジックアルゴリズム 'レジスタ内のSIMD(SWAR)操作'のいずれかを使用してブロックをスキャンします。 Word内のバイトをテストするため。例は here ;です。 0x0a0a0a0a0a0a0a0aLLを含むループは、改行をスキャンします。 (そのコードは、ファイルの各行の正規表現に一致する入力バイトあたり約5サイクルになります)

ファイルがほんの数十または百メガバイト程度であり、それが成長し続ける(つまり、何かが書き込みを続ける)場合、Linuxがメモリにキャッシュしている可能性が高いため、ディスクではありませんIO制限あり、ただしメモリ帯域幅制限あり。

ファイルが追加されるだけの場合は、行数と以前の長さを覚えて、そこから開始することもできます。


C++ stlアルゴリズムでmmapを使用し、std :: foreachに渡すファンクターを作成できることが指摘されています。私はそうすることができないのでそれをするべきではないことを提案しましたが、そうするために追加のコードを書くことには利益がありません。あるいは、すべてを処理するboostのmmappedイテレーターを使用することもできます。しかし、私がリンクしたコードがこれのために書かれた問題については、はるかに遅く、問題はスタイルではなくスピードについてでした。

9
Pete Kirkham

あなたはそれが大きくなり続けると書いた。これは、新しい行が追加されているが既存の行は変更されていないログファイルまたは同様のもののように聞こえます。これが事実である場合、あなたはインクリメンタルアプローチを試すことができます。

ファイルの終わりまで解析します。行数とEOFのオフセットを覚えておいてください。ファイルがオフセットまでfseek大きくなったら、EOFに解析し、行数とオフセットを更新します。

9

カウンティングラインとカウンティングラインセパレーターには違いがあります。正確な行数を取得することが重要である場合に注意すべきいくつかの一般的な問題:

  1. ファイルのエンコーディングは何ですか?バイトごとのソリューションはASCIIとUTF-8で機能しますが、UTF-16または値が1のバイトであることを保証しないマルチバイトエンコーディングがある場合は注意してください改行は必ず改行をエンコードします。

  2. 多くのテキストファイルには、最終行の終わりに行区切り記号がありません。したがって、ファイルに_"Hello, World!"_と記載されている場合は、1ではなく0のカウントになる可能性があります。行の区切り文字をカウントするだけでなく、追跡するための単純な状態マシンが必要になります。

  3. 一部の非常にあいまいなファイルでは、より一般的なキャリッジリターンやラインフィードではなく、Unicode _U+2028 LINE SEPARATOR_(または_U+2029 PARAGRAPH SEPARATOR_)を行区切りとして使用しています。 U+0085 NEXT LINE (NEL)にも注意する必要があります。

  4. 他の制御文字を改行として数えるかどうかを検討する必要があります。たとえば、_U+000C FORM FEED_または_U+000B LINE TABULATION_(別名垂直タブ)は、新しい行に行くと見なす必要がありますか?

  5. 古いバージョンのMac OS(OS Xより前)のテキストファイルは、改行(_U+000D_)ではなく改行(_U+000A_)を使用して行を区切ります。生のバイトをバッファに読み込んで(たとえば、ストリームをバイナリモードで)スキャンしている場合、これらのファイルのカウントは0になります。 PCファイルは通常両方で行を終了するため、キャリッジリターンとラインフィードの両方をカウントすることはできません。繰り返しますが、単純な状態マシンが必要です。 (または、バイナリモードではなくテキストモードでファイルを読み取ることができます。テキストインターフェイスは、プラットフォームで使用されている規則に準拠するファイルの場合、行区切り文字を_'\n'_に正規化します。他のプラットフォームからファイルを読み取る場合、状態マシンでバイナリモードに戻ります。)

  6. ファイルに非常に長い行がある場合、getline()アプローチは例外をスローし、少数のファイルで単純な行カウンターが失敗する可能性があります。 (これは、Mac以外のプラットフォームで古いMacファイルを読み込んでいる場合に特に当てはまり、getline()はファイル全体を1つの巨大な行として表示します。)チャンクを固定サイズのバッファーに読み込んで、ステートマシンを使用して、それを防弾にすることができます。

受け入れられた回答のコードは、これらのトラップのほとんどの影響を受けます。あなたがそれを速くする前にそれを正しくしなさい。

6
Adrian McCarthy

すべてのfstreamがバッファリングされることに注意してください。したがって、実際にはそれらはチャンクで読み取られるため、この機能を再作成する必要はありません。だからあなたがする必要があるのはバッファをスキャンすることだけです。文字列のサイズを強制するため、getline()は使用しないでください。そのため、STL std :: countとストリームイテレータを使用します。

#include <iostream>
#include <fstream>
#include <iterator>
#include <algorithm>


struct TestEOL
{
    bool operator()(char c)
    {
        last    = c;
        return last == '\n';
    }
    char    last;
};

int main()
{
    std::fstream  file("Plop.txt");

    TestEOL       test;
    std::size_t   count   = std::count_if(std::istreambuf_iterator<char>(file),
                                          std::istreambuf_iterator<char>(),
                                          test);

    if (test.last != '\n')  // If the last character checked is not '\n'
    {                       // then the last line in the file has not been 
        ++count;            // counted. So increement the count so we count
    }                       // the last line even if it is not '\n' terminated.
}
4
Martin York

アルゴリズムが原因で遅くなることはありませんが、IO操作が遅いため遅くなります。単純なO(n)アルゴリズムを使用していると思いますは単にファイルを順番に処理するだけです。その場合、プログラムを最適化できるnoより高速なアルゴリズムがあります。

ただし、より高速なアルゴリズムはないと述べましたが、「メモリマップファイル」と呼ばれるより高速なメカニズムがあります。マップされたファイルにはいくつかの欠点があり、適切ではない場合があります。それについて読んで、自分で理解する必要があります。

メモリマップファイルでは、O(n)より優れたアルゴリズムを実装できませんが、mayはIOアクセス時間を短縮します。

3
user88637

改行文字を探してファイル全体をスキャンすることによってのみ、最終的な回答を得ることができます。それを回避する方法はありません。

ただし、考慮すべき可能性がいくつかあります。

1 /単純なループを使用している場合は、一度に1文字ずつ読み込んで改行をチェックしません。 I/Oがバッファリングされている場合でも、関数呼び出し自体は時間とコストがかかります。

1つのI/O操作でファイルの大きなチャンク(たとえば5M)をメモリに読み込み、それを処理することをお勧めします。 Cランタイムライブラリはとにかく最適化されるので、おそらく特別なアセンブリ命令についてあまり心配する必要はありません-単純なstrchr()がそれを行う必要があります。

2 /一般的な行の長さが約40-50文字で、exact行数が必要ない場合は、ファイルサイズを取得して45(または平均値)で割ります。使用すると見なされます)。

3 /これがログファイルのようなものであり、1つのファイルに保持するhaveを行わない場合(システムの他の部分でのやり直しが必要になる場合があります)、ファイルを定期的に分割することを検討してください。

たとえば、5Mになったら、それ(たとえば、x.log)を日付の付いたファイル名(たとえば、x_20090101_1022.log)に移動し、その時点の行数を調べます(x_20090101_1022.countに保存してから、新しいx.logログファイルを開始します)ログファイルの特性は、作成されたこの日付の付いたセクションが決して変更されないため、行数を再計算する必要がないことを意味します。

ログ「ファイル」を処理するには、cat x_*.logではなく、何らかのプロセスパイプを介してcat x.logを実行します。 「ファイル」の行数を取得するには、現在のx.logでwc -lを実行し(比較的高速)、それをx_*.countファイルのすべての値の合計に追加します。

3
paxdiablo

時間がかかるのは、40 MB以上をメモリに読み込むことです。これを行う最速の方法は、メモリマップするか、大きなバッファに一度にロードすることです。何らかの方法でメモリに格納すると、\n文字を探すデータを走査するループは、それがどのように実装されているかに関係なく、ほとんど瞬時に実行されます。

したがって、実際には、最も重要なトリックは、ファイルをできるだけ速くメモリにロードすることです。そして、それを行う最も速い方法は、単一の操作としてそれを行うことです。

それ以外の場合、アルゴリズムを高速化するために多くのトリックが存在する可能性があります。行が追加されるだけで、変更も削除もされず、ファイルを繰り返し読み取る場合は、以前に読み取った行をキャッシュして、次にファイルを読み取る必要がある場合は、新しく追加された行のみを読み取ることができます。

または、既知の「\ n」文字の場所を示す個別のインデックスファイルを保持して、ファイルのこれらの部分をスキップできるようにすることもできます。

ハードドライブから大量のデータを読み取るのは遅いです。それを回避する方法はありません。

1
jalf