速度 なぜC++では標準読み込みの行がPythonよりずっと遅いのですか?




zig 言語 (8)

私はPythonとC ++を使ってstdinから文字列を読み込む行を比較したいと思っていました。私のC ++コードが相当するPythonコードよりも遅く実行されているのがわかりました。 私のC ++は錆びていて、私はまだPythonistaの専門家ではないので、何か間違っているのか、何かを誤解しているのかどうか教えてください。

(TLDR答え:ステートメントを含めます: cin.sync_with_stdio(false)か、代わりにfgets使用してください。

TLDRの結果:私の質問の一番下までスクロールして、テーブルを見てください。)

C ++コード:

#include <iostream>
#include <time.h>

using namespace std;

int main() {
    string input_line;
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    while (cin) {
        getline(cin, input_line);
        if (!cin.eof())
            line_count++;
    };

    sec = (int) time(NULL) - start;
    cerr << "Read " << line_count << " lines in " << sec << " seconds.";
    if (sec > 0) {
        lps = line_count / sec;
        cerr << " LPS: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

// Compiled with:
// g++ -O3 -o readline_test_cpp foo.cpp

同等のPython:

#!/usr/bin/env python
import time
import sys

count = 0
start = time.time()

for line in  sys.stdin:
    count += 1

delta_sec = int(time.time() - start_time)
if delta_sec >= 0:
    lines_per_sec = int(round(count/delta_sec))
    print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
       lines_per_sec))

ここに私の結果があります:

$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889

$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. LPS: 5570000

私はMac OS X v10.6.8(Snow Leopard)とLinux 2.6.32(Red Hat Linux 6.2)の両方でこれを試したことに注意してください。 前者はMacBook Proであり、後者は非常に軽いサーバーであり、これはあまり適切ではありません。

$ for i in {1..5}; do echo "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP:   Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in  1 seconds. LPS: 5570000

小さなベンチマーク補遺と要約

完全性のために、私は元の(同期された)C ++コードで同じボックスの同じファイルの読み込み速度を更新すると思った。 ここでも、これは高速ディスク上の100M行ファイル用です。 ここでは、いくつかのソリューション/アプローチを用いて比較します。

Implementation      Lines per second
python (default)           3,571,428
cin (default/naive)          819,672
cin (no sync)             12,500,000
fgets                     14,285,714
wc (not fair comparison)  54,644,808

scanf( "%s")は文字列を解析し、任意のスペース(char、space、tab、newline)を探すため、これがまだ遅い理由は2番目の例(scanf

また、はい、CPythonは、ハードディスクの読み取りを避けるためにいくつかのキャッシュを行います。


getline 、ストリーム演算子、 scanf 、ファイルの読み込み時間や小さなテキストファイルを読み込んでいる場合に便利です。 しかし、パフォーマンスが気になるものなら、ファイル全体をメモリにバッファリングするだけで済みます(それが合うと仮定して)。

ここに例があります:

//open file in binary mode
std::fstream file( filename, std::ios::in|::std::ios::binary );
if( !file ) return NULL;

//read the size...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

//read into memory buffer, then close it.
char *filebuf = new char[length+1];
file.read(filebuf, length);
filebuf[length] = '\0'; //make it null-terminated
file.close();

必要に応じて、このようなより便利なアクセスのために、バッファを囲むようにストリームをラップすることができます:

std::istrstream header(&filebuf[0], length);

また、ファイルを管理している場合は、テキストの代わりにフラットなバイナリデータ形式を使用することを検討してください。 空白のあいまいさをすべて処理する必要がないため、読み書きがより確実です。 また、解析するのがより小さく、はるかに高速です。


次のコードは、今までここに投稿されている他のコードよりも速いものでした(Visual Studio 2013,64ビット、行長が一様に500MBのファイル、[0、1000])。

const int buffer_size = 500 * 1024;  // Too large/small buffer is not good.
std::vector<char> buffer(buffer_size);
int size;
while ((size = fread(buffer.data(), sizeof(char), buffer_size, stdin)) > 0) {
    line_count += count_if(buffer.begin(), buffer.begin() + size, [](char ch) { return ch == '\n'; });
}

それは、私のPythonのすべての試みを2倍以上打ちました。


私はMac上でg ++を使って私のコンピュータ上で元の結果を再現しました。

whileループの直前にC ++バージョンに次のステートメントを追加すると、 Pythonバージョンでインライン展開されます。

std::ios_base::sync_with_stdio(false);
char buffer[1048576];
std::cin.rdbuf()->pubsetbuf(buffer, sizeof(buffer));

sync_with_stdioは速度を2秒に改善し、大きなバッファを設定すると1秒に短縮しました。


私はここ数年遅れているが、

元の投稿の 'Edit 4/5/6'では、次のような構成を使用しています:

$ /usr/bin/time cat big_file | program_to_benchmark

これはいくつかの異なる方法で間違っています:

  1. 実際には、あなたのベンチマークではなく、 `cat`の実行をタイミングしています。 `time 'で表示される' user 'と' sys 'のCPU使用率は、ベンチマークされたプログラムではなく` cat`のものです。 さらに悪いことに、「本当の」時間も必ずしも正確ではありません。 あなたのローカルOSの `cat`とパイプラインの実装に依存して、` cat`が最終的な巨大なバッファを書き、読者の処理が終わるずっと前に出る可能性があります。

  2. `cat`の使用は不要で、実際は逆効果です。 可動部分を追加しています。 もしあなたが十分に古いシステム(つまり、単一のCPUと、ある世代のコンピュータではCPUよりも速いI / O)を使っていたら、 `cat`が実行されているという事実だけでは、 あなたはまた、入力と出力のバッファリングや `cat`が行う他の処理の対象となります。 (私がRandal Schwartzの場合、これはおそらくあなたに「Catless Of Use Cat」賞を与えるでしょう。

より良い構築は次のようになります:

$ /usr/bin/time program_to_benchmark < big_file

この文では、big_fileをオープンしてプログラムに渡すシェルです (実際には `time`にプログラムをサブプロセスとして実行します)。すでに開いているファイル記述子です。 ファイルの読み込みの100%は厳密にベンチマークしようとしているプログラムの責任です。 これは、あなたに偽の合併症なしにパフォーマンスの真の読書を得させます。

私は考えられるかもしれない2つの可能性のある、実際には間違った「修正」についても言及することにする(ただし、これらは元の投稿に間違ったものではないので別に番号を付ける)。

A.プログラムのタイミングだけで修正できます:

$ cat big_file | /usr/bin/time program_to_benchmark

B.またはパイプライン全体のタイミングをとることによって:

$ /usr/bin/time sh -c 'cat big_file | program_to_benchmark'

これらは#2と同じ理由で間違っています:彼らはまだ `cat`を不必要に使用しています。 私はいくつかの理由でそれらを言及します:

  • POSIXシェルのI / Oリダイレクション機能を完全に快適にしていない人にとっては、より自然なものです

  • `cat` 必要な場合があります(例えば、読み込むファイルにアクセスするための何らかの特権が必要で、ベンチマークするプログラムにその特権を与えたくない場合など)。 `sudo cat / dev / sda | / usr / bin / time my_compression_test --no-output`)を実行します。

  • 実際には現代のマシンでは、パイプラインに追加された `cat`はおそらく本当の結果にはならないでしょう

しかし、私は最後のことを少し躊躇して言います。 「Edit 5」の最後の結果を調べると、

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

- これは、 `cat`がテスト中にCPUの74%を消費したと主張しています。 確かに1.34 / 1.83は約74%です。 おそらく実行:

$ /usr/bin/time wc -l < temp_big_file

.49秒しか残っていないだろう! おそらく `cat 'は` disk'(実際にはバッファ・キャッシュ)からファイルを転送したread()システム・コール(またはそれに相当するもの)と、 `wc`へのパイプ書き込みを支払う必要がありました。 正しいテストでもread()呼び出しを実行しなければなりませんでした。 パイプへの書込みとパイプからの読出しだけが保存され、それらはかなり安価でなければなりません。

それでも、私はあなたが `cat file | wc -l`と `wc -l <​​file`を実行して、目立つ(2桁のパーセンテージ)差を見つけます。 遅いテストのそれぞれは、絶対時間で同様のペナルティを支払ったでしょう。 しかし、それは、より大きな合計時間のうちのより小さい割合になるであろう。

実際には、Linux 3.13(Ubuntu 14.04)システムで、1.5ギガバイトのゴミファイルを使っていくつかの簡単なテストを行い、これらの結果を得ました(これらは実際にはキャッシュのプライミング後、3つの最良の結果です)。

$ time wc -l < /tmp/junk
real 0.280s user 0.156s sys 0.124s (total cpu 0.280s)
$ time cat /tmp/junk | wc -l
real 0.407s user 0.157s sys 0.618s (total cpu 0.775s)
$ time sh -c 'cat /tmp/junk | wc -l'
real 0.411s user 0.118s sys 0.660s (total cpu 0.778s)

2つのパイプラインの結果は、リアルタイムよりも多くのCPU時間(user + sys)を消費したと主張しています。 これは私がシェル(Bash)の組み込みの 'time'コマンドを使用しているためです。これはパイプラインを認識しています。 私はパイプライン内の別々のプロセスが別々のコアを使用してリアルタイムよりも速くCPU時間を累積するマルチコアマシンです。 / usr / bin / timeを使用すると、リアルタイムよりもCPU時間が短いことがわかります。コマンドラインで渡された単一のパイプライン要素の時間を計測できるだけであることを示しています。 また、シェルの出力にはミリ秒が与えられ、/ usr / bin / timeは1秒の時間だけを与えます。

したがって、 `wc -l`の効率レベルでは、` cat`は409/283 = 1.453または45.3%もっとリアルタイムで、775/280 = 2.768、または177%多いCPUが使用されます。 私のランダムには、その時にはテストボックスでした。

私は、これらのスタイルのテストの間に少なくとも1つの他の重要な相違があることを付け加えておきます。それが利益であるか間違っているかは言えません。 あなたはこれを自分で決めなければなりません:

`cat big_file |`を実行すると、 / usr / bin / time my_program`の場合、あなたのプログラムは `cat`によって送られた正確なペースでパイプからの入力を受け取り、` cat`で書かれたものより大きなチャンクで入力を受け取ります。

`/ usr / bin / time my_program <big_file`を実行すると、プログラムは実際のファイルへのオープンファイル記述子を受け取ります。 あなたのプログラムは、多くの場合、書かれた言語のI / Oライブラリは、通常のファイルを参照するファイルディスクリプタで表示されると、異なる動作をすることがあります。 明示的なread(2)システムコールを使用する代わりに、mmap(2)を使用して入力ファイルをそのアドレス空間にマップすることができます。 これらの違いは、 `cat`バイナリを実行するための小さなコストよりも、ベンチマークの結果にはるかに大きな影響を与える可能性があります。

もちろん、同じプログラムが2つのケースで大きく異なる場合は、面白いベンチマーク結果になります。 実際には、プログラムやそのI / Oライブラリ 、mmap()のように面白いことをしていることを示しています。 実際には両方の方法でベンチマークを実行すると良いかもしれません。 おそらく `cat`の結果を` cat`を実行するコストを「許して」いくらかの小さな要素で割り引いているかもしれません。


答えの最初の要素: <iostream>は遅いです。 くそー。 私は以下のようにscanf大きなパフォーマンス向上を見せていますが、それでもPythonより2倍遅いです。

#include <iostream>
#include <time.h>
#include <cstdio>

using namespace std;

int main() {
    char buffer[10000];
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    int read = 1;
    while(read > 0) {
        read = scanf("%s", buffer);
        line_count++;
    };
    sec = (int) time(NULL) - start;
    line_count--;
    cerr << "Saw " << line_count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}

ちなみに、C ++バージョンの行数がPythonバージョンの数よりも大きい理由は、eofを超えて読み込もうとするとeofフラグがセットされるということだけです。 正しいループは次のようになります:

while (cin) {
    getline(cin, input_line);

    if (!cin.eof())
        line_count++;
};

さて、2番目のソリューションでは、 cinからscanf切り替えたことがわかりました。これは、私が最初に提案したものです(cinはsloooooooooooowです)。 今度は、 scanfからfgets切り替えると、パフォーマンスがさらに向上しますfgetsは文字列入力のための最も速いC ++関数です。

BTWは、その同期のことについて、良い知らなかった。 しかし、あなたはまだfgets試してみるべきです。





getline