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


1840

PythonとC ++を使用してstdinから文字列入力の行を読み取ることを比較したかったのですが、私のC ++コードが同等のPythonコードよりも桁違いに実行されるのを見てショックを受けました。私のC ++はさびており、私はまだPythonのエキスパートではないので、何か間違っているのか、何かを誤解しているのかどうか教えてください。


(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

14
テストを複数回実行しましたか?おそらくディスクキャッシュの問題です。
Vaughn Cato、2012

9
@JJC:2つの可能性があります(Davidによって提案されたキャッシュの問題を削除したと仮定):1)<iostream>パフォーマンスが悪い。初めてのことではありません。2)Pythonは、forループでデータをコピーしないように十分に賢いです。あなたは、使用しようとして再テストができscanfchar[]。または、文字列で何かが行われるようにループを書き直すこともできます(たとえば、5番目の文字を保持し、結果に連結します)。
JN、2012

15
問題はstdioとの同期です-私の回答を参照してください。
Vaughn Cato、2012

19
C ++で追加の行を取得する理由について誰も言及していないようです。テストしないでくださいcin.eof()!! 入れてgetline「if`ステートメントにコールを。
Xeo

21
wc -lストリームを一度に複数行読み取るため、高速です(fread(stdin)/memchr('\n')組み合わせの場合があります)。Pythonの結果は同じ大きさです。たとえば、wc-l.py
jfs

回答:


1644

デフォルトでcinは、はstdioと同期されます。これにより、入力バッファリングが回避されます。これをメインの一番上に追加すると、パフォーマンスが大幅に向上します。

std::ios_base::sync_with_stdio(false);

通常、入力ストリームがバッファリングされている場合、一度に1文字を読み取るのではなく、ストリームはより大きなチャンクで読み取られます。これにより、通常は比較的コストのかかるシステムコールの数が減ります。ただし、FILE*ベースでstdioあり、iostreams多くの場合、個別の実装、したがって個別のバッファーがあるため、両方を一緒に使用すると問題が発生する可能性があります。例えば:

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

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

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

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


142
これが一番上になるはずです。ほぼ間違いなく正しいです。答えは、読み取りをfscanf呼び出しに置き換えることにあるわけではありません。これは、Pythonほど単純には機能しないためです。Pythonは文字列にメモリを割り当てる必要があります。これは、既存の割り当てが不十分であると見なされるため、C ++によるのアプローチとまったく同じstd::stringです。このタスクはほぼ確実にI / Oバウンドでありstd::string、C ++でオブジェクトを作成したり、オブジェクト<iostream>自体を使用したりするコストについては、FUDが多すぎます。
Karl Knechtel、2012

51
はい、この行をオリジナルのwhileループのすぐ上に追加すると、Pythonをも超えるようにコードが高速化されます。結果を最終編集として投稿します。再度、感謝します!
JJC

6
はい、これは実際にはcout、cerr、clogにも当てはまります。
Vaughn Cato

2
cout、cin、cerr、clogを高速化するには、このようにしてstd :: ios_base :: sync_with_stdio(false);を実行します。
01100110 2012年

56
sync_with_stdio()静的メンバ関数であり、任意のストリームオブジェクト(例えば、上でこの関数を呼び出すcinための同期オンまたはオフに)トグル全ての標準入出力ストリームオブジェクト。
John Zwinck、2015年

171

好奇心から、私は内部で何が起こるかを調べ、各テストでdtruss / straceを使用しました。

C ++

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

シスコール 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

パイソン

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

シスコール sudo dtruss -c ./a.py < in

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

159

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

元の投稿の「編集4/5/6」では、次の構文を使用しています。

$ /usr/bin/time cat big_file | program_to_benchmark

これは、いくつかの異なる点で間違っています。

  1. 実際catには、ベンチマークではなく、の実行のタイミングをとっています。によって表示される「ユーザー」および「sys」のCPU使用率は、ベンチマークプログラムtimecatはなくのCPU使用率です。さらに悪いことに、「実際の」時間も必ずしも正確ではありません。catローカルOSのパイプラインの実装によってはcat、リーダープロセスがその作業を完了するずっと前に、最後の巨大なバッファーを書き込んで終了する可能性があります。

  2. の使用catは不要であり、実際には逆効果です。可動部品を追加しています。十分に古いシステム(つまり、単一のCPUと、特定の世代のコンピューターでは、CPUよりもI / Oが速い)を使用している場合、cat実行していたという事実だけで結果がかなり色付けされる可能性があります。また、入力および出力のバッファリングやその他の処理の対象となりcatます。(私がRandal Schwartzだった場合、これはおそらく「Useless Use Of 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、パイプラインに追加しても、実際には何の影響もありません。

しかし、私は少しためらうことで最後のことを言います。「編集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ここでは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が、100分の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ライブラリーは、異なるアクションを実行する可能性があります。mmap(2)明示的なread(2)システムコールを使用する代わりに、入力ファイルをそのアドレス空間にマップするために使用できます。これらの違いは、catバイナリを実行するための小さなコストよりもベンチマーク結果にはるかに大きな影響を与える可能性があります。

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


26
うわー、それはかなり洞察に満ちていました!プログラムの標準入力への入力をフィードするために猫は不要であり、<シェルリダイレクトが優先されることを知っていましたが、前者の方法では視覚的に保持されるデータの左から右への流れのため、通常猫に固執していますパイプラインについて考えるとき。このような場合のパフォーマンスの違いはごくわずかです。しかし、私はあなたが私たちを教育してくれたことに感謝します、ベラ。
JJC 2017年

11
リダイレクトは初期段階でシェルコマンドラインから解析されます。これにより、左から右へのフローの外観がより快適になる場合は、これらのいずれかを実行できます。 $ < big_file time my_program $ time < big_file my_program これは、どのPOSIXシェルでも機能するはずです(つまり、「csh」ではありません) `そして、` rc`のようなエキゾチックについてはわかりません:)
Bela Lubkin

6
繰り返しになりますが、 `cat`バイナリが同時に実行されていることによる、おそらく興味をそそらない増分パフォーマンスの違いは別として、テスト中のプログラムが入力ファイルをmmap()できる可能性をあきらめています。これは結果に大きな違いをもたらす可能性があります。これは、「ファイルからの入力行」イディオムのみを使用して、さまざまな言語で自分でベンチマークを記述した場合でも当てはまります。それは、さまざまなI / Oライブラリの詳細な動作に依存します。
Bela Lubkin

2
補足:Bashのビルトインtimeは、最初のプログラムではなくパイプライン全体を測定しています。time seq 2 | while read; do sleep 1; done2秒/usr/bin/time seq 2 | while read; do sleep 1; done印刷、0秒印刷
folkol

1
@folkol-はい、<< 2つのパイプラインの結果が[リアルタイム]より[より多く]のCPUを[表示](Bash)する組み込み[time]コマンドを使用することに注意してください。... / usr / bin / time ...は、コマンドラインで渡された単一のパイプライン要素のみを計測できます。>> '
Bela Lubkin

90

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秒に低下しました。


5
より有用な情報を得るために、さまざまなバッファサイズを試すことをお勧めします。私はあなたが急速に減少するリターンを見ることを疑います。
Karl Knechtel、2012

8
私は返事が早すぎた。バッファサイズをデフォルト以外の値に設定しても、それほど大きな違いはありませんでした。
karunski

109
スタックに1MBのバッファを設定することも避けます。これはスタックオーバーフローにつながる可能性があります(私はそれについて議論するのに良い場所だと思います!)
Matthieu M.

11
Matthieu、Macはデフォルトで8MBのプロセススタックを使用します。Linuxは、デフォルトのスレッドあたり4MBのIIRCを使用します。スタック深度が比較的浅い入力を変換するプログラムにとって、1MBはそれほど問題ではありません。ただし、さらに重要なことに、バッファがスコープ外になるとstd :: cinはスタックを破棄します。
2014年

22
@SEK Windowsのデフォルトのスタックサイズは1MBです。
エティエンヌ2014年

39

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

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


20

次のコードは、これまでにここに投稿した他のコードよりも高速でした:(Visual Studio 2013、64ビット、500 MBファイル、行長は[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倍以上に上回っています。


readバッファリングされていないsyscallを長さの静的バッファに反復的に作成するかBUFSIZE、同等の対応するmmapsyscalls を介して反復的に作成し、次に改行をカウントするそのバッファをさっと動かす、小さなカスタムで完全に単純なCプログラムを使用すると、それよりもさらに速くなりfor (char *cp = buf; *cp; cp++) count += *cp == "\n"ます。BUFSIZEただし、ご使用のシステムに合わせて調整する必要があります。stdioによってすでに実行されています。しかし、そのforループは、ボックスのハードウェア用の驚くほど高速なアセンブラー言語命令にコンパイルされるはずです。
tchrist

3
count_ifとラムダもコンパイルされ、「驚くほど高速なアセンブラ」にコンパイルされます。
ペッター、

17

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

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

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

70
本当に正しいループは次のようになります。 while (getline(cin, input_line)) line_count++;
ジョナサンWakely

2
@JonathanWakely私はかなり遅れていることを知っていますが、使用++line_count;line_count++;ます。
ヴァルはモニカを復活させる

7
@valを使用すると、コンパイラにバグが生じます。変数はa longであり、コンパイラーは、増分の結果が使用されていないことを通知できます。ポストインクリメントとプレインクリメントで同じコードが生成されない場合は、壊れています。
ジョナサンウェイクリー

2
実際、適切なコンパイラは、インクリメント後の誤用を検出し、代わりにインクリメント前に置き換えることができますが、コンパイラは必須ではありません。したがって、コンパイラーが置換を実行しなくても、問題はありません。さらに、++line_count;代わりに書いてline_count++;も害はありません:)
Fareanor

1
@valsaysReinstateMonicaこの特定の例では、なぜどちらが好ましいのでしょうか。結果はどちらの方法でもここでは使用されないため、の後に読み取られwhileますよね?なんらかのエラーがあり、それline_countが正しいことを確認したい場合はどうでしょうか?私は推測しているだけですが、なぜそれが問題になるのか理解できません。
TankorSmash

14

(scanf()を使用した)2番目の例では、これがさらに遅い理由は、scanf( "%s")が文字列を解析し、スペース文字(スペース、タブ、改行)を探すためです。

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


12

答えの最初の要素<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;
}

3回目の編集を行うまでこの投稿は表示されませんでしたが、ご提案いただきありがとうございます。奇妙なことに、上記のedit3のscanf行を使用すると、Pythonと比較して2倍のヒットはありません。ちなみに私は2.7を使っています。
JJC 2012

10
c ++バージョンを修正した後、このstdioバージョンは私のコンピューターのc ++ iostreamsバージョンよりもかなり遅くなります。(3秒vs 1秒)
カルンスキー

10

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

ところで、同期については知りませんでした。しかし、あなたはまだ試す必要がありますfgets


2
fgets不完全な行の追加のチェックなしで(そして行を実際に使用する必要がある場合は、ループ間で行を分割するという点で)間違っていることを除いて、不完全な行の追加のチェックなしで(そしてそれを補正しようとすると、不必要に大きなバッファーを割り当てる必要があります) 、std::getline再配置を処理して、実際の入力とシームレスに一致させます)。速くて間違っているのは簡単ですが、「少し遅いが正しい」を使用することはほとんど常に価値sync_with_stdioがあります。
ShadowRanger 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.