CとC ++のほぼ同一のコード間の実行時間の大きな違い(x9)


85

私はwww.spoj.comからこの演習を解決しようとしていました:FCTRL-階乗

あなたは本当にそれを読む必要はありません、あなたが興味を持っているならそれを読んでください:)

最初に私はそれをC ++で実装しました(これが私の解決策です):

#include <iostream>
using namespace std;

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    std::ios_base::sync_with_stdio(false); // turn off synchronization with the C library’s stdio buffers (from https://stackoverflow.com/a/22225421/5218277)

    cin >> num_of_inputs;

    while (num_of_inputs--)
    {
        cin >> fact_num;

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        cout << num_of_trailing_zeros << "\n";
    }

    return 0;
}

g ++ 5.1のソリューションとしてアップロードしました

結果は:時間0.18 Memの3.3M C ++の実行結果

しかし、その後、実行時間が0.1未満であると主張するコメントがいくつか見られました。より高速なアルゴリズムについて考えることができなかったので、Cで同じコードを実装しようとしました:

#include <stdio.h>

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    scanf("%d", &num_of_inputs);

    while (num_of_inputs--)
    {
        scanf("%d", &fact_num);

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        printf("%d", num_of_trailing_zeros);
        printf("%s","\n");
    }

    return 0;
}

gcc5.1のソリューションとしてアップロードしました

今回の結果は以下のとおりであった:時間0.02 Memの2.1M C実行結果

これでコードはほぼ同じになりましstd::ios_base::sync_with_stdio(false);た。ここで提案しようにC ++コードに追加して、Cライブラリのstdioバッファーとの同期をオフにしました。また、printf("%d\n", num_of_trailing_zeros);を分割してprintf("%d", num_of_trailing_zeros); printf("%s","\n");、の二重呼び出しを補正operator<<cout << num_of_trailing_zeros << "\n";ます。

しかし、CコードとC ++コードでは、x9のパフォーマンスが向上し、メモリ使用量が少なくなっています。

何故ですか?

編集

Cコードで修正unsigned longunsigned intました。あるべきでunsigned intあり、上記の結果は新しい(unsigned int)バージョンに関連しています。


31
C ++ストリームは、設計上非常に低速です。ゆっくりと着実にレースに勝つからです。:P(
炎上

7
遅さは安全性や適応性によるものではありません。これは、すべてのストリームフラグで過剰に設計されています。
Karoly Horvath 2015

8
@AlexLop。astd::ostringstreamを使用して出力を累積し、最後にstd::cout 一度すべてに送信すると、時間が0.02に短縮されます。std::coutループでの使用は環境内で単純に遅く、それを改善する簡単な方法はないと思います。
高炉2015

6
これらのタイミングがideoneを使用して取得されたという事実に他の誰も心配していませんか?
ildjarn 2015

6
@Olaf:同意できないのではないかと思います。この種の質問は、選択したすべてのタグで非常に話題になっています。CとC ++は一般に十分に近いので、このようなパフォーマンスの違いは説明を求めます。見つけてよかったです。結果として、GNU libc ++を改善する必要があるかもしれません。
chqrlie 2015

回答:


56

どちらのプログラムもまったく同じことをします。それらはまったく同じアルゴリズムを使用し、複雑さが低いため、パフォーマンスは主に入力および出力処理の効率に依存します。

scanf("%d", &fact_num);片側と反対側で入力をスキャンすることは、cin >> fact_num;どちらの方法でもそれほどコストがかからないようです。実際、変換のタイプはコンパイル時に認識されており、正しいパーサーはC ++コンパイラーによって直接呼び出すことができるため、C ++ではコストが低くなるはずです。同じことが出力にも当てはまります。の別の呼び出しを作成することもできますprintf("%s","\n");が、Cコンパイラはこれをの呼び出しとしてコンパイルするのに十分putchar('\n');です。

したがって、I / Oと計算の両方の複雑さを見ると、C ++バージョンはCバージョンよりも高速であるはずです。

のバッファリングを完全に無効にstdoutすると、Cの実装が遅くなり、C ++バージョンよりもさらに遅くなります。fflush(stdout);最後の後にAlexLopによる別のテストでprintfは、C ++バージョンと同様のパフォーマンスが得られます。出力は一度に1バイトではなく小さなチャンクでシステムに書き込まれるため、バッファリングを完全に無効にするほど遅くはありません。

これはC ++のライブラリ内の特定の行動を指すように思える:私はあなたのシステムの実装を疑うcincoutに出力をフラッシュするcout入力が要求されたときcin。一部のCライブラリもこれを実行しますが、通常は端末との間で読み取り/書き込みを行う場合に限ります。www.spoj.comサイトによって行われたベンチマークは、おそらくファイルとの間で入力と出力をリダイレクトします。

AlexLopは別のテストを行いました。ベクトル内のすべての入力を一度に読み取り、続いてすべての出力を計算して書き込むと、C ++バージョンが非常に遅い理由を理解するのに役立ちます。これにより、パフォーマンスがCバージョンのパフォーマンスに向上します。これは私の主張を証明し、C ++フォーマットコードに対する疑念を取り除きます。

Blastfurnaceによる別のテストでは、すべての出力をに保存std::ostringstreamし、最後に1回のブラストでフラッシュすることで、C ++のパフォーマンスを基本的なCバージョンのパフォーマンスに向上させます。QED。

からの入力cinとへの出力をインターレースするcoutと、非常に非効率的なI / O処理が発生し、ストリームバッファリングスキームが無効になるようです。パフォーマンスが10分の1に低下します。

PS:アルゴリズムが正しくないのはfact_num >= UINT_MAX / 5fives *= 5がオーバーフローする前にオーバーフローしてラップアラウンドするため> fact_numです。これらのタイプのいずれかが。より大きい場合はfivesunsigned longまたはを作成することでこれを修正できます。フォーマットとしても使用できます。www.spoj.comの人たちは、ベンチマークがそれほど厳しくないのは幸運です。unsigned long longunsigned int%uscanf

編集:後でvitauxによって説明されるように、この動作は実際にC ++標準によって義務付けられています。 デフォルトでにcin関連付けられcoutています。cin入力バッファの補充が必要な入力操作により、cout保留中の出力がフラッシュされます。OPの実装では、体系的cinにフラッシュするようにcout見えますが、これは少しやり過ぎで、目に見えて非効率的です。

イリヤ・ポポフはこれに対する簡単な解決策を提供しました:に加えて別の魔法の呪文を唱えるcinことによって解くことができます:coutstd::ios_base::sync_with_stdio(false);

cin.tie(nullptr);

また、このような強制フラッシュは、で行末を生成するstd::endl代わりに'\n'を使用する場合にも発生することに注意してくださいcout。出力行をよりC ++の慣用的で無邪気な外観に変更するとcout << num_of_trailing_zeros << endl;、同じようにパフォーマンスが低下します。


2
あなたはおそらくストリームフラッシングについて正しいでしょう。aに出力を収集std::ostringstreamし、最後にすべてを1回出力すると、Cバージョンと同等の時間が短縮されます。
高炉2015

2
@ DavidC.Rankin:私は推測を思い切って(cinを読むとcoutがフラッシュされる)、それを証明する方法を考案し、AlexLopがそれを実装し、説得力のある証拠を提供しますが、Blastfurnaceは私の主張と彼のテストを証明する別の方法を考え出しました同様に説得力のある証拠を与える。私はそれを証明と見なしますが、もちろん、C ++ソースコードを見ると、完全に正式な証明ではありません。
chqrlie 2015

2
ostringstream出力に使用してみましたが、時間は0.02QEDでした:)。に関してはfact_num >= UINT_MAX / 5、GOODポイント!
アレックスロップ。

1
すべての入力をに収集してからvector(なしでostringstream)計算を処理すると、同じ結果が得られます。時間0.02。両方vectorを組み合わせると、ostringstreamそれ以上改善されません。同じ時間0.02
アレックス垂れ。

2
場合でも動作しますシンプルな修正sizeof(int) == sizeof(long long)、これは次のとおりです。後にループの本体にテストを追加するnum_of_trailing_zeros += fact_num/fives;かどうかをチェックするためにfivesその最大に達している:if (fives > UINT_MAX / 5) break;
chqrlie

44

メイクへのもう一つのトリックiostream秒より速くあなたは両方を使用cinしてcoutコールにあります

cin.tie(nullptr);

デフォルトでは、から何かを入力するとcin、がフラッシュされcoutます。入力と出力をインターリーブすると、パフォーマンスが大幅に低下する可能性があります。これは、いくつかのプロンプトを表示してからデータを待つコマンドラインインターフェイスの使用に対して行われます。

std::string name;
cout << "Enter your name:";
cin >> name;

この場合、入力を待つ前に、プロンプトが実際に表示されていることを確認する必要があります。もし上記の行ではそのネクタイを破る、cinそしてcout自立。

C ++ 11以降、iostreamでパフォーマンスを向上させるもう1つの方法は、次のようにstd::getlineと一緒に使用するstd::stoiことです。

std::string line;
for (int i = 0; i < n && std::getline(std::cin, line); ++i)
{
    int x = std::stoi(line);
}

この方法では、パフォーマンスがCスタイルに近づくか、それを超える可能性がありscanfます。getchar特にgetchar_unlocked手書きの解析と一緒に使用すると、パフォーマンスが向上します。

PS。オンラインの審査員に役立つ、C ++で数値を入力するいくつかの方法を比較した投稿を書きましが、ロシア語のみです。申し訳ありません。ただし、コードサンプルとファイナルテーブルは理解できるはずです。


1
ソリューションの説明や+1をありがとう、しかし、あなたの提案の代替とstd::readlineしてstd::stoi機能のOPコードと同等ではありません。両方ともcin >> x;scanf("%f", &x);区切り文字としてant空白を受け入れます。同じ行に複数の数字が存在する可能性があります。
chqrlie 2015

27

問題は、cppreferenceを引用することです

std :: cinからの入力、std :: cerrへの出力、またはプログラムの終了により、std :: cout.flush()が強制的に呼び出されます。

これはテストが簡単です:交換する場合

cin >> fact_num;

scanf("%d", &fact_num);

と同じですcin >> num_of_inputscout、C ++バージョン(またはIOStreamバージョン)でもC1とほぼ同じパフォーマンスが得られます。

ここに画像の説明を入力してください

保持するcinが交換する場合も同じことが起こります

cout << num_of_trailing_zeros << "\n";

printf("%d", num_of_trailing_zeros);
printf("%s","\n");

簡単な解決策は、解くことでcoutありcin、IlyaPopovが述べたように:

cin.tie(nullptr);

標準ライブラリの実装では、特定の場合にフラッシュの呼び出しを省略できますが、常にそうとは限りません。これがC ++ 14 27.7.2.1.3からの引用です(chqrlieに感謝します):

クラスbasic_istream :: sentry:まず、is.tie()がnullポインターでない場合、関数はis.tie()-> flush()を呼び出して、出力シーケンスを関連する外部Cストリームと同期します。is.tie()のput領域が空の場合、この呼び出しを抑制できることを除いて。さらに、実装は、is.rdbuf()-> underflow()の呼び出しが発生するまで、フラッシュの呼び出しを延期することができます。歩哨オブジェクトが破壊される前にそのような呼び出しが発生しない場合、フラッシュの呼び出しは完全に排除される可能性があります。


説明してくれてありがとう。しかし、C ++ 14を引用します。27.7.2.1.3:クラスbasic_istream :: sentryまず、is.tie()がnullポインターでない場合、関数はis.tie()->flush()出力シーケンスを関連する外部Cストリームと同期するために呼び出します。ただし、のput領域is.tie()が空の場合、この呼び出しを抑制することができます。さらに、実装は、の呼び出しがis.rdbuf()->underflow()発生するまで、フラッシュの呼び出しを延期することができます。歩哨オブジェクトが破壊される前にそのような呼び出しが発生しない場合、フラッシュの呼び出しは完全に排除される可能性があります。
chqrlie 2015

C ++でいつものように、物事は見た目よりも複雑です。OPのC ++ライブラリは、標準で許可されているほど効率的ではありません。
chqrlie 2015

cppreferenceリンクをありがとう。私は☺かかわらず、印刷画面で「間違った答え」好きではない
アレックス垂れ。

@AlexLop。おっと、「間違った答え」の問題を修正しました=)。他のcinを更新するのを忘れました(ただし、これはタイミングには影響しません)。
vitaut 2015

@chqrlieそうですが、アンダーフローの場合でも、stdioソリューションと比較してパフォーマンスが低下する可能性があります。標準参照をありがとう。
vitaut 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.