web-dev-qa-db-ja.com

C++では、stdinからの行の読み取りがPythonよりはるかに遅いのはなぜですか?

私はPythonとC++を使って標準入力からの文字列入力の読み取り行を比較したいと思っていましたが、私の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
1661
JJC

デフォルトでは、cinはstdioと同期されているため、入力バッファリングを回避できます。これをメインの一番上に追加すると、はるかに優れたパフォーマンスが得られます。

std::ios_base::sync_with_stdio(false);

通常、入力ストリームがバッファリングされると、一度に1文字ずつ読み込まれるのではなく、ストリームはより大きな塊で読み込まれます。これにより、通常は比較的高価なシステムコールの数が減ります。しかし、FILE*ベースのstdioiostreamsは別々の実装を持ち、したがって別々のバッファを持つことが多いため、両方を一緒に使用すると問題が発生する可能性があります。例えば:

int myvalue1;
cin >> myvalue1;
int myvalue2;
scanf("%d",&myvalue2);

実際に必要な量よりも多くの入力がcinによって読み取られた場合、2番目の整数値は、独自の独立したバッファーを持つscanf関数には使用できません。これは予期しない結果につながります。

これを避けるために、デフォルトでは、ストリームはstdioと同期されます。これを実現する一般的な方法の1つは、cin関数を使用して、必要に応じてstdioに各文字を1文字ずつ読み取らせることです。残念ながら、これは多くのオーバーヘッドをもたらします。少量の入力の場合、これは大きな問題ではありませんが、何百万もの行を読んでいるときには、パフォーマンスの低下が大きくなります。

幸い、ライブラリの設計者は、自分が何をしているのかを知っていれば、この機能を無効にしてパフォーマンスを向上させることもできると判断したので、 sync_with_stdio メソッドを提供しました。

1486
Vaughn Cato

好奇心の余地がないので、私はボンネットの下で何が起こるのかを調べました。そして、それぞれのテストで dtruss/strace を使いました。

C++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

syscalls Sudo dtruss -c ./a.out < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            6
pread                                           8
mprotect                                       17
mmap                                           22
stat64                                         30
read_nocancel                               25958

Python

./a.py < in
Read 6512402 lines in 1 seconds. LPS: 6512402

syscalls Sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29
149
2mia

私はここ数年遅れています、しかし:

元の記事の「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だった場合、これはおそらくあなたに 「猫の無用な使用」 賞を獲得するでしょう。

より良い構造は次のようになります。

$ /usr/bin/time program_to_benchmark < big_file

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

私は考えられる2つの可能性があるが実際には間違っている 'fix'に言及します(しかし、これらは元の記事で間違っていたものではないので、私は異なった '番号'を付けます)。

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` isが必要な場合があるかもしれません(例:読み込むファイルがアクセスするためにある種の特権を必要とし、ベンチマークの対象となるプログラムにその特権を与えたくない場合:Sudo cat/dev/sda |/usr/bin/time my_compression_test --no-output`)

  • 実際には、最近のマシンでは、パイプラインに追加された `cat`はおそらく本当の意味ではありません

しかし、ちょっと躊躇しながら最後のことを言います。 「編集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

残りの0.49秒しかかかりませんでした。そうではないかもしれません: `cat`は、ファイルを 'disk'(実際にはバッファキャッシュ)から転送したread()システムコール(あるいはそれと同等のもの)と、それらを` wc`に渡すパイプライトを支払う必要がありました。それでも正しいテストでは、これらのread()呼び出しを実行する必要があります。 write-to-pipeおよびread-from-pipe呼び出しだけが保存されているはずで、それらはかなり安くあるはずです。

それでも、私はあなたが `猫のファイルの違いを測定することができるだろうと予測します。 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)を費やしたと主張していることに注意してください。これは、パイプラインを認識しているShell(Bash)の組み込みの 'time'コマンドを使用しているためです。そして、私はマルチコアマシンを使用しています。パイプライン内の別々のプロセスが別々のコアを使用して、リアルタイムよりも速いCPU時間を蓄積できるのです。/usr/bin/timeを使用すると、リアルタイムよりもCPU時間が短くなります。これは、1つのパイプライン要素がコマンドラインで渡したタイミングにしか対応できないことを示しています。また、シェルの出力にはミリ秒が表示され、/ 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`を実行すると、プログラムは実際のファイルへのオープンファイル記述子を受け取ります。あなたのプログラム - or - それが書かれた言語のI/Oライブラリ - は通常のファイルを参照するファイルディスクリプタを与えられたときに異なる動作をするかもしれません。明示的なread(2)システムコールを使用する代わりに、mmap(2)を使用して入力ファイルをそのアドレス空間にマッピングすることができます。これらの違いは `cat`バイナリを実行するためのわずかなコストよりもベンチマーク結果にはるかに大きな影響を与える可能性があります。

もちろん、2つのケースで同じプログラムのパフォーマンスが大きく異なる場合、これは興味深いベンチマーク結果です。実際、プログラムまたはそのI/Oライブラリareがmmap()の使用のように、何か面白いことをしていることがわかります。したがって、実際にはベンチマークを両方向に実行するのが良いかもしれません。 `cat`の実行コストを「許す」ために、` cat`の結果を何らかの小さな要因で割り引くことはおそらく可能です。

122
Bela Lubkin

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

whileループの直前にC++バージョンに次のステートメントを追加すると、 Python versionとインラインになります。

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

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

85
karunski

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);

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

36
Stu

ちなみに、C++バージョンの行数がPythonバージョンの行数より1大きいのは、eofを超えて読み込もうとした場合にのみeofフラグが設定されるためです。正しいループは次のようになります。

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

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

次のコードは、これまでにここに投稿した他のコードよりも高速でした。

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倍以上優れています。

15
Petter

2番目の例(scanf()を使用)でこれがまだ遅いのは、scanf( "%s")が文字列を解析して任意の空白文字(space、tab、newline)を探すためです。

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

13
davinchi

答えの最初の要素:<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;
}
11
J.N.

さて、あなたの2番目の解決策ではcinからscanfに切り替えたことがわかります。 scanfからfgetsに切り替えると、パフォーマンスがさらに向上します。fgetsは、文字列入力用の最速のC++関数です。

ところで、同期については知りませんでした、ニース。しかし、あなたはまだfgetsを試すべきです。