web-dev-qa-db-ja.com

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

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

あなたは本当にそれを読む必要はありません、あなたが好奇心があるならちょうどそれをしてください:)

最初に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のソリューションとしてアップロードしました

結果は:Time0.18Mem3.3M C++ execution results

しかし、その後、私は彼らの時間の実行が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;
}
_

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

今回は、結果は:Time0.02Mem2.1M C execution results

コードはとほぼ同じで、提案されたようにstd::ios_base::sync_with_stdio(false);をC++コードに追加しました here との同期をオフにします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";_の二重呼び出しを補正します。

しかし、私はまだx9のパフォーマンスの向上とCとC++のコードのメモリ使用量の削減を見ました。

何故ですか?

[〜#〜] edit [〜#〜]

Cコードで_unsigned long_を_unsigned int_に修正しました。これは_unsigned int_であるはずであり、上記の結果は新しい(_unsigned int_)バージョンに関連しています。

85
Alex Lop.

両方のプログラムはまったく同じことを行います。それらは同じ正確なアルゴリズムを使用し、その複雑さが低いことを考えると、パフォーマンスは主に入出力処理の効率に制限されます。

一方をscanf("%d", &fact_num);で、もう一方をcin >> fact_num;で入力をスキャンすることは、どちらの方法でもそれほど費用がかかりそうにありません。実際、C++では変換の種類がコンパイル時にわかっており、C++コンパイラーによって正しいパーサーを直接呼び出すことができるため、C++ではコストが低くなります。出力についても同じことが言えます。あなたは、printf("%s","\n");の別の呼び出しを書くことさえしますが、Cコンパイラはこれをputchar('\n');の呼び出しとしてコンパイルするのに十分です。

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

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

これは、C++ライブラリの特定の動作を指しているようです:cinのシステムの実装は、coutからの入力が要求されたときに、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 / 5は、fives *= 5になる前にオーバーフローしてラップアラウンドするため、アルゴリズムは> fact_numに対して正しくありません。これらのタイプのいずれかがunsigned longより大きい場合、fivesunsigned long longまたはunsigned intにすることでこれを修正できます。また、%uscanf形式として使用します。幸運なことに、www.spoj.comのメンバーは、ベンチマークが厳しすぎていません。

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

イリヤ・ポポフは、このための簡単な解決策を提供しました。cinは、std::ios_base::sync_with_stdio(false);に加えて別の魔法の呪文を唱えることにより、coutから解くことができます。

cin.tie(nullptr);

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

56
chqrlie

iostreamcinの両方を使用するときにcoutsを高速化する別のトリックは、

cin.tie(nullptr);

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

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

この場合、入力を待機する前に、プロンプトが実際に表示されることを確認する必要があります。上記の行でその関係を破ると、cincoutが独立します。

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++で入力する数値のいくつかの方法を比較しました。オンライン審査員にとっては便利ですが、ロシア語でしかありません。ただし、コードサンプルとファイナルテーブルは理解できるはずです。

44
Ilya Popov

問題は、引用 cppreference

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

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

cin >> fact_num;

scanf("%d", &fact_num);

cin >> num_of_inputsでも同じですが、coutを保持すると、C++バージョン(またはIOStreamバージョン)でC 1とほぼ同じパフォーマンスが得られます。

enter image description here

cinを保持するが、置き換える場合も同じことが起こります

cout << num_of_trailing_zeros << "\n";

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

簡単な解決策は、Ilya Popovが述べたようにcoutcinを解くことです:

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()の呼び出しが発生するまでフラッシュする呼び出しを延期することができます。 sentryオブジェクトが破棄される前にそのような呼び出しが発生しない場合、flushの呼び出しは完全に削除されます。

27
vitaut