C ++では、食べていないものの代金を払っていますか?


170

CおよびC ++での次のhello worldの例を考えてみましょう。

main.c

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

main.cpp

#include <iostream>

int main()
{
    std::cout<<"Hello world"<<std::endl;
    return 0;
}

godboltでそれらをコンパイルしてアセンブリすると、Cコードのサイズはわずか9行(gcc -O3)になります。

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret

ただし、C ++コードのサイズは22行(g++ -O3)です。

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edx, 11
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

...はるかに大きいです。

C ++では、あなたがあなたが食べるものに対して支払うことは有名です。それで、この場合、私は何を払っていますか?


3
コメントは拡張ディスカッション用ではありません。この会話はチャットに移動さました
Samuel Liew


26
eatC ++に関連する用語を聞いたことがない。私はあなたが意味するものを信じます:「あなたはあなたが使うものに対してのみ支払う」?
ジャコモアルゼッタ2018

7
@GiacomoAlzetta、それは食べ放題のビュッフェのコンセプトを適用した口語表現です。より正確な用語を使用することは、世界中の聴衆にとって確かに望ましいですが、ネイティブアメリカンの英語を話す人として、このタイトルは私にとって意味があります。
チャールズダフィー

5
@ trolley813メモリリークは、引用やOPの質問とは関係ありません。「使用した分だけ支払う」/「使用しない分だけ支払う」のポイントは、特定の機能や抽象化を使用しない場合、パフォーマンスへの影響はないということです。メモリリークはこれとはまったく関係がなく、この用語eatはあいまいであり、避ける必要があることを示しているだけです。
ジャコモアルゼッタ

回答:


60

あなたが払っているのは、重いライブラリを呼び出すことです(コンソールに出力するほどではありません)。ostreamオブジェクトを初期化します。いくつかの隠されたストレージがあります。次に、のstd::endl同義語ではないwhich を呼び出します\niostreamライブラリには、多くの設定を調整し、プログラマではなく、プロセッサに負担をかけることができます。これはあなたが払っているものです。

コードを見てみましょう:

.LC0:
        .string "Hello world"
main:

ostreamオブジェクト+ coutの初期化

    sub     rsp, 8
    mov     edx, 11
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

cout新しい行を印刷してフラッシュするために再度呼び出す

    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
    xor     eax, eax
    add     rsp, 8
    ret

静的ストレージの初期化:

_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

また、言語とライブラリを区別することも不可欠です。

ところで、これは話のほんの一部です。呼び出す関数に何が書かれているかはわかりません。


5
追加の注記として、徹底的なテストにより、C ++プログラムの前に "ios_base :: sync_with_stdio(false);"を付加することが示されます。および「cin.tie(NULL);」coutはprintfよりも高速になります(Printfにはフォーマット文字列のオーバーヘッドがあります)。1つ目cout; printf; coutは、書き込みを順番どおりに行うことによるオーバーヘッドを排除します(独自のバッファーがあるため)。2番目はとを同期解除coutcincout; cin最初にユーザーに情報を要求する可能性があります。フラッシュすると、実際に必要な場合にのみ強制的に同期されます。
ニコラスピピトーネ2018年

こんにちはニコラス、これらの便利なメモを追加していただきありがとうございます。
アラッシュ

「言語とライブラリを区別することは不可欠です」:はい、しかし言語に付属している標準ライブラリはどこでも利用できる唯一のライブラリであるため、どこでも使用されています(そして、はい、C標準ライブラリは一部です) C ++仕様に準拠しているため、必要に応じて使用できます)。「呼び出している関数に何が書かれているかわからない」について:本当に知りたい場合は静的にリンクできます。実際、調べた呼び出しコードはおそらく無関係です。
ピーター-モニカを

211

それで、この場合、私は何を払っていますか?

std::coutはより強力で複雑ですprintf。ロケール、ステートフルなフォーマットフラグなどをサポートしています。

それらが必要ない場合は、std::printfまたはを使用しstd::putsてください<cstdio>。これらはで使用できます。


C ++では、あなたがあなたが食べるものに対して支払うことは有名です。

また、C ++ != C ++標準ライブラリであることも明確にしたいと思います。標準ライブラリは汎用で「十分に高速」であると想定されていますが、必要なものの特殊な実装よりも遅くなることがよくあります。

一方、C ++言語は、不必要な追加の隠しコスト(オプトインvirtual、ガベージコレクションなし)を支払うことなくコードを記述できるように努めています。


4
標準ライブラリが汎用で「十分に高速」であると想定されていることを +1しますが、多くの場合、必要なものの特殊な実装よりも遅くなります。多くは、パフォーマンスへの影響を考慮せずに、独自のSTLコンポーネントを使用するようです。
Craig Estey、2018年

7
@Craig OTOH標準ライブラリの多くの部分は、通常、代わりに作成できるものよりも高速で正確です。
ピーター-モニカを

2
@ PeterA.Schneider OTOH、STLバージョンが20倍から30倍遅い場合、独自のローリングは良いことです。ここで私の答えを参照してください:codereview.stackexchange.com/questions/191747/…その中で、他の人も[少なくとも部分的に]自分でロールすることを提案しました。
Craig Estey、2018年

1
@CraigEsteyベクトルは(特定のインスタンスで最終的にどの程度の作業が行われるかに応じて重要になる可能性がある初期の動的割り当ては別として)、C配列ほど効率的ではありません。されて設計されたことはありません。それをコピーしたり、最初に十分なスペースを確保したりしないように注意する必要がありますが、これらはすべて配列でも行う必要があり、安全性が低下します。リンクされた例に関して:はい、ベクトルのベクトルは(最適化されない限り)2D配列と比較して余分な間接参照が発生しますが、20倍の効率はそこに根ざしているのではなく、アルゴリズムにあると思います。
ピーター-モニカを

174

CとC ++を比較していません。あなたはとを比較printfしてstd::coutいますが、はさまざまな機能(ロケール、ステートフルフォーマットなど)に対応しています。

比較のために次のコードを使用してみてください。Godboltは両方のファイルに対して同じアセンブリを生成します(gcc 8.2、-O3でテスト済み)。

main.c:

#include <stdio.h>

int main()
{
    int arr[6] = {1, 2, 3, 4, 5, 6};
    for (int i = 0; i < 6; ++i)
    {
        printf("%d\n", arr[i]);
    }
    return 0;
}

main.cpp:

#include <array>
#include <cstdio>

int main()
{
    std::array<int, 6> arr {1, 2, 3, 4, 5, 6};
    for (auto x : arr)
    {
        std::printf("%d\n", x);
    }
}


同等のコードを示し、理由を説明してくれてありがとう。
HackSlash

134

あなたのリストは確かにリンゴとオレンジを比較していますが、他のほとんどの回答で示唆されている理由ではありません。

あなたのコードが実際に何をしているのかチェックしましょう:

C:

  • 単一の文字列を出力し、 "Hello world\n"

C ++:

  • 文字列"Hello world"をストリームするstd::cout
  • std::endlマニピュレータをにストリームしますstd::cout

どうやらあなたのC ++コードは2倍の仕事をしています。公平に比較​​するには、これを組み合わせる必要があります。

#include <iostream>

int main()
{
    std::cout<<"Hello world\n";
    return 0;
}

…そして突然、あなたのアセンブリコードはmainCのものと非常に似ています:

main:
        sub     rsp, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        xor     eax, eax
        add     rsp, 8
        ret

実際、CコードとC ++コードを1行ずつ比較できますが、違いほとんどありません

sub     rsp, 8                      sub     rsp, 8
mov     edi, OFFSET FLAT:.LC0   |   mov     esi, OFFSET FLAT:.LC0
                                >   mov     edi, OFFSET FLAT:_ZSt4cout
call    puts                    |   call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
xor     eax, eax                    xor     eax, eax
add     rsp, 8                      add     rsp, 8
ret                                 ret

唯一の真の違いは、C ++ではoperator <<2つの引数(std::coutおよび文字列)を使用して呼び出すことです。fprintfストリームを指定する最初の引数も持つ、より近いC eqivalent:を使用することで、そのわずかな違いさえも取り除くことができます。

これにより、のアセンブリコードが残ります_GLOBAL__sub_I_main。これはC ++では生成されますが、Cでは生成されません。これは、このアセンブリリストに表示される唯一の真のオーバーヘッドです(もちろん、両方の言語で目に見えないオーバーヘッドがあります)。このコードは、C ++プログラムの開始時に、一部のC ++標準ライブラリ関数の1回限りのセットアップを実行します。

ただし、他の回答で説明されているように、これらの2つのプログラムの関連する違いは、main関数のアセンブリ出力には見られません。


21
ちなみに、Cランタイム設定する必要があります。これは呼び出された関数で発生します_startが、そのコードはCランタイムライブラリの一部です。とにかく、これはCとC ++の両方で発生します。
Konrad Rudolph

2
@Deduplicator:実際には、デフォルトでは、iostreamライブラリはバッファリングを行わstd::cout、代わりにI / Oをstdio実装(独自のバッファリングメカニズムを使用)に渡します。特に、インタラクティブ端末に接続されている(いわゆる)場合、デフォルトでは、への書き込み時に完全にバッファリングされた出力は表示されませんstd::cout。iostreamライブラリでの独自のバッファリングメカニズムを使用する場合は、stdioとの同期を明示的に無効にする必要がありますstd::cout

6
@KonradRudolph:実際には、printfここでストリームをフラッシュする必要はありません。実際、一般的な使用例(出力がファイルにリダイレクトされる)では、通常、printfステートメントフラッシュされないことがわかります。出力がラインバッファリングまたはバッファリングされていない場合にのみ、printfトリガーがフラッシュされます。

2
@PeterCordes:そうです、フラッシュされていない出力バッファーでブロックすることはできませんが、プログラムが入力を受け入れ、期待される出力を表示せずに行進するという驚きに遭遇する可能性があります。「ヘルプ、プログラムが入力中にハングしているが、理由がわからない!」をデバッグする機会があったので、これを知っています。これにより、別の開発者が数日間適していました。

2
@PeterCordes:私がする議論は「あなたの言っていることを書いて」です-出力が最終的に利用可能になることを意味する場合は改行が適切であり、出力がすぐに利用可能になることを意味する場合はendlが適切です。

53

C ++では、あなたがあなたが食べるものに対して支払うことは有名です。それで、この場合、私は何を払っていますか?

それは簡単です。あなたが支払うstd::cout。「食べた分だけ支払う」というのは「常に最高の価格になる」という意味ではありません。確かに、printf安いです。これstd::coutはより安全で用途が広いため、コストが高くなることは正当化されますが(コストは高くなりますが、より多くの価値を提供します)、要点を逃しています。使用せずprintf、使用するstd::coutので、使用料はかかりstd::coutます。の使用に料金はかかりませんprintf

良い例が仮想関数です。仮想関数には実行時のコストとスペースの要件がありますが、実際に使用する場合のみです。仮想関数を使用しない場合、何も支払う必要はありません。

いくつかの備考

  1. C ++コードがより多くのアセンブリー命令を評価する場合でも、それはまだ少数の命令であり、パフォーマンスのオーバーヘッドは実際のI / O操作によっておそらく小さくなります。

  2. 実際には、「C ++では、あなたが食べるものに対して支払う」よりも良い場合があります。たとえば、コンパイラーは、状況によっては仮想関数呼び出しが不要であると推定し、それを非仮想呼び出しに変換できます。つまり、無料で仮想関数を取得できるということです。すごくないですか?


6
仮想関数を無料で入手することはできません。それでも、最初にそれらを作成し、コンパイラーがコードの変換をデバッグすることによって、それが何をすべきかについての考えと一致しない場合は、そのコストを支払う必要があります。
alephzero

2
@alephzero開発コストとパフォーマンスコストを比較することが特に重要かどうかはわかりません。

駄洒落を無駄にするこのような絶好の機会...「価格」の代わりに「カロリー」という単語を使用することもできます。それから、C ++はCよりも太っていると言えます。または、少なくとも問題の特定のコード(私はC ++に偏っているので、Cを優先しているので、それ以上進むことはできません)。ああ。@Bilkokuyaすべてのケースに関連しているわけではないかもしれませんが、それは確かに無視してはならないものです。したがって、全体的に関連性があります。
プリフタン共和国

46

「printfのアセンブリリスト」は、printfではなく、プット(コンパイラの最適化の種類?)を対象としています。printfはputsよりもかなり複雑です...忘れないでください!


13
他のすべての人std::coutは、アセンブリのリストには表示されないの内部についての赤いニシンに引っかかるので、これはこれまでのところ最良の答えです。
Konrad Rudolph

12
アセンブリリストはへの呼び出しputsでありprintf、単一のフォーマット文字列とゼロの追加の引数のみを渡した場合の呼び出しと同じように見えます。(例外としてxor %eax,%eax、レジスター内のゼロFP引数を可変個関数に渡すためです。)これらはどちらも実装ではなく、ストリングへのポインターをライブラリー関数に渡すだけです。しかし、はい、最適化するprintfには、putsgccが唯一持っているフォーマットのない何かである"%s"、あるいは全くの変換、文字列の端が改行で存在しない場合。
Peter Cordes

45

ここで有効な回答がいくつかありますが、詳細についてはもう少し詳しく説明します。

このテキスト全体を読みたくない場合は、主な質問への回答を以下の要約にジャンプしてください。


抽象化

それで、この場合、私は何を払っていますか?

あなたは抽象化のためにお金を払っています。よりシンプルで人間に優しいコードを記述できるようになると、コストがかかります。オブジェクト指向言語であるC ++では、ほとんどすべてがオブジェクトです。オブジェクトを使用する場合、3つの主要なことが常に内部で行われます。

  1. オブジェクトの作成、基本的にはオブジェクト自体とそのデータのメモリ割り当て。
  2. オブジェクトの初期化(通常はいくつかのinit()メソッドによる)。通常、メモリの割り当ては、このステップの最初のこととして、内部で行われます。
  3. オブジェクトの破棄(常にではありません)。

コードには表示されませんが、オブジェクトを使用するたびに、上記の3つすべてが何らかの形で発生する必要があります。すべてを手動で行うと、コードは明らかに長くなります。

これで、オーバーヘッドを追加せずに抽象化を効率的に行うことができます。コンパイラーとプログラマーの両方がメソッドのインライン化やその他の手法を使用して、抽象化のオーバーヘッドを削除できますが、そうではありません。

C ++で実際に何が起こっているのですか?

ここに、内訳があります:

  1. std::ios_baseクラスは、I / Oは、関連するすべてのための基底クラスである、初期化されます。
  2. std::coutオブジェクトが初期化されます。
  3. 文字列が読み込まれ、に渡さstd::__ostream_insertれます。これは(既に名前からわかるように)文字列をストリームに追加するstd::cout(基本的には<<演算子)のメソッドです。
  4. cout::endlにも渡されstd::__ostream_insertます。
  5. __std_dso_handleはに渡されます__cxa_atexit。これは、プログラムを終了する前に「クリーニング」を行うグローバル関数です。__std_dso_handleそれ自体がこの関数によって呼び出され、残りのグローバルオブジェクトの割り当てを解除して破棄します。

C ==を使用して何も払っていないのですか?

Cコードでは、いくつかの手順が実行されています。

  1. 文字列が読み込まれputsediレジスタを介して渡されます。
  2. puts 呼び出されます。

オブジェクトはどこにもないため、初期化/破棄する必要はありません。

ただし、これは、Cの何に対しても「支払っていない」という意味ではありません。抽象化の費用は引き続きかかります。また、C標準ライブラリの初期化とprintf関数の動的解決(または、実際putsには、フォーマット文字列を必要としないためコンパイラーによって最適化されます)は、内部で行われます。

このプログラムを純粋なアセンブリで作成すると、次のようになります。

jmp start

msg db "Hello world\n"

start:
    mov rdi, 1
    mov rsi, offset msg
    mov rdx, 11
    mov rax, 1          ; write
    syscall
    xor rdi, rdi
    mov rax, 60         ; exit
    syscall

基本的には、write syscallが呼び出され、その後にsyscallが呼び出されexitます。今これ同じことを達成するために最低限のだろう。


要約する

Cの方がはるかに必要最低限​​であり、必要最低限​​のことだけを行い、ユーザーが完全に制御できるようにします。これにより、基本的にユーザーが望むものを完全に最適化およびカスタマイズできます。レジスタに文字列をロードし、その文字列を使用するライブラリ関数を呼び出すようにプロセッサに指示します。一方、C ++はより複雑で抽象的なものです。これは複雑なコードを書くときに非常に有利であり、より簡単で人間に優しいコードを書くことができますが、明らかにコストがかかります。このような場合、C ++と比較すると、C ++のパフォーマンスには常に欠点があります。C++は、このような基本的なタスクを実行するために必要なものより多くを提供するため、オーバーヘッドが増加します。

主な質問に答える

私が食べていないものの代金を払っていますか?

この特定のケースでは、はい。あなたは、C ++がC以外に提供する必要があるものを利用していませんが、それは、C ++があなたを助けることができる単純なコードに何もないからです。


ああ、あともう1つ!

非常にシンプルで小さなプログラムを作成したため、C ++の利点は一見すると明らかではないかもしれませんが、少し複雑な例を見て違いを確認してください(どちらのプログラムもまったく同じことを行います)。

C

#include <stdio.h>
#include <stdlib.h>

int cmp(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main(void) {
    int i, n, *arr;

    printf("How many integers do you want to input? ");
    scanf("%d", &n);

    arr = malloc(sizeof(int) * n);

    for (i = 0; i < n; i++) {
        printf("Index %d: ", i);
        scanf("%d", &arr[i]);
    }

    qsort(arr, n, sizeof(int), cmp)

    puts("Here are your numbers, ordered:");

    for (i = 0; i < n; i++)
        printf("%d\n", arr[i]);

    free(arr);

    return 0;
}

C ++

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main(void) {
    int n;

    cout << "How many integers do you want to input? ";
    cin >> n;

    vector<int> vec(n);

    for (int i = 0; i < vec.size(); i++) {
        cout << "Index " << i << ": ";
        cin >> vec[i];
    }

    sort(vec.begin(), vec.end());

    cout << "Here are your numbers:" << endl;

    for (int item : vec)
        cout << item << endl;

    return 0;
}

うまくいけば、ここで私が何を意味するのか明確に理解できるでしょう。また、Cには、使用して低レベルのメモリを管理する必要がどのように通告mallocし、freeあなたがインデックス作成とサイズの詳細に注意する必要があるか、そしてあなたが入力して印刷を撮影するときに非常に特異的である必要がありますか。


27

最初にいくつかの誤解があります。まず、C ++プログラム 22の命令を生成しません。22,000の命令に近いものです(私は帽子からその数を引き出しましたが、おおよその範囲です)。また、Cコード 9つの命令も生成され。それらはあなたが見るものだけです。

Cコードが行うことは、目に見えない多くのことを行った後、CRTから関数を呼び出し(通常は共有ライブラリとして存在する必要はありません)、戻り値やハンドルを確認しません。エラー、および救済。コンパイラーと最適化の設定によっては、実際には呼び出されない場合もありますprintfputs、もっと基本的なものです。
同じ関数を同じ方法で呼び出せば、C ++で多かれ少なかれ同じプログラム(いくつかの非表示のinit関数を除く)を作成することもできます。または、非常に正確になりたい場合は、同じ関数の前にを付けstd::ます。

対応するC ++コードは実際にはまったく同じものではありません。その全体は<iostream>、小さなプログラムに大きなオーバーヘッドを追加する太った醜い豚であることでよく知られていますが(「実際の」プログラムでは、あまり気付かないでしょう)、やや公平な解釈は、それがひどいことをするということですあなたが見ないものの多くがうまくいく。さまざまな数値形式やロケールやその他の情報、バッファリング、適切なエラー処理など、ほとんどすべての無計画なものの魔法の書式設定を含みますが、これらに限定されません。エラー処理?そうですね、文字列の出力は実際には失敗する可能性があると思います。Cプログラムとは異なり、C ++プログラムはこれを黙って無視しません。何を考えるstd::ostreamこれは実際にはかなり軽量です。情熱を持ってストリーム構文を嫌うので、私がそれを使用しているようではありません。しかし、それでも、それが何をするかを考えれば、それはかなり素晴らしいです。

しかし、確かに、C ++全体はC ほど効率的ではありません。それは同じものではありません、それはされていないので、それは効率的とすることはできませんやって同じことを。他に何もない場合、C ++は例外(および例外を生成、処理、または失敗するコード)を生成し、Cが提供しないことを保証します。したがって、確かに、C ++プログラムは必然的に少し大きくする必要があります。ただし、全体像では、これはまったく問題ではありません。それどころか、実際のプログラムでは、何らかの理由でC ++のパフォーマンスが向上することはめったにありません。特に理由がわからないので、わからない。

最善の結果を得る代わりに、正しい Cコードを記述したい場合(つまり、実際にエラーを確認し、エラーが発生してもプログラムが正しく動作する場合)、違いはわずかです。存在する場合。


16
この主張を除いて、非常に良い答えです。「しかし、確かに、C ++全体は、Cができるほど効率的ではありません」は、単に間違っています。C ++はCのように、効率的なようにすることができ、十分に高いレベルのコードとすることができるより多くの同等のCコードよりも効率的。はい、C ++には例外を処理する必要があるため、ある程度のオーバーヘッドがありますが、最新のコンパイラでは、コストのかからない抽象化によるパフォーマンスの向上に比べて、そのオーバーヘッドは無視できます。
Konrad Rudolph

私が正しく理解した場合、std::cout例外もスローしますか?
Saher

6
@Saher:はい、いいえ、多分。std::coutはでありstd::basic_ostream、その1つスローすることができ、そうするように構成されている場合は、例外を再スローするか、例外飲み込むことができます。物事失敗する可能性があり、C ++およびC ++標準ライブラリは(ほとんど)ビルドされているため、失敗が簡単に気付かれることはありません。これは煩わしさ祝福です(しかし、煩わしさより祝福です)。一方、Cは中指だけを表示します。戻りコードをチェックせず、何が起こったかはわかりません。
デイモン

1
@KonradRudolph:確かに、これは私が指摘しようとしていたことです。「何らかの理由で、C ++のパフォーマンスが向上することはめったにありません。より好ましい最適化に向いているようです。特に理由を尋ねないでください」。その理由はすぐにはわかりませんが、最適化が向上することはまれではありません。理由はともかく。オプティマイザにとってはすべて同じだと思うかもしれませんが、そうではありません。
デイモン

22

あなたは間違いの代償を払っています。80年代には、コンパイラーがフォーマット文字列をチェックするのに十分ではない場合、演算子のオーバーロードは、io中に型の安全性の類似性を強制するための良い方法と見なされていました。ただし、そのバナー機能はすべて、最初からうまく実装されていないか、概念的に破産しています。

<iomanip>

C ++ストリームio apiの最も不快な部分は、このフォーマットヘッダーライブラリの存在です。ステートフルで醜く、エラーが発生しやすいだけでなく、フォーマットをストリームに結合します。

8桁のゼロで埋められた16進数の符号なし整数、その​​後にスペース、小数点以下3桁の2桁が続く行を出力するとします。を使用する<cstdio>と、簡潔なフォーマット文字列を読み取ることができます。を使用して<ostream>、古い状態を保存し、配置を右に設定し、文字を埋め、幅を設定し、底を16進数に設定し、整数を出力し、保存した状態を復元します(それ以外の場合、整数の形式は浮動小数点形式を汚染します)、スペースを出力します、表記を固定に設定し、精度を設定し、倍精度浮動小数点数と改行を出力してから、古い書式を復元します。

// <cstdio>
std::printf( "%08x %.3lf\n", ival, fval );

// <ostream> & <iomanip>
std::ios old_fmt {nullptr};
old_fmt.copyfmt (std::cout);
std::cout << std::right << std::setfill('0') << std::setw(8) << std::hex << ival;
std::cout.copyfmt (old_fmt);
std::cout << " " << std::fixed << std::setprecision(3) << fval << "\n";
std::cout.copyfmt (old_fmt);

オペレーターのオーバーロード

<iostream> 演算子のオーバーロードを使用しない方法のポスターの子です。

std::cout << 2 << 3 && 0 << 5;

パフォーマンス

std::cout数倍遅いprintf()です。蔓延する特発性炎と仮想派遣はその代償を払う。

スレッドセーフティ

両方とも<cstdio><iostream>すべての関数呼び出しがアトミックであるため、スレッドセーフです。しかし、printf()1回の呼び出しでより多くのことが行われます。オプションを指定して次のプログラムを実行<cstdio>すると、の行のみが表示されますf<iostream>マルチコアマシンで使用する場合、他のものが表示される可能性があります。

// g++ -Wall -Wextra -Wpedantic -pthread -std=c++17 cout.test.cpp

#define USE_STREAM 1
#define REPS 50
#define THREADS 10

#include <thread>
#include <vector>

#if USE_STREAM
    #include <iostream>
#else
    #include <cstdio>
#endif

void task()
{
    for ( int i = 0; i < REPS; ++i )
#if USE_STREAM
        std::cout << std::hex << 15 << std::dec;
#else
        std::printf ( "%x", 15);
#endif

}

int main()
{
    auto threads = std::vector<std::thread> {};
    for ( int i = 0; i < THREADS; ++i )
        threads.emplace_back(task);

    for ( auto & t : threads )
        t.join();

#if USE_STREAM
        std::cout << "\n<iostream>\n";
#else
        std::printf ( "\n<cstdio>\n" );
#endif
}

この例のレトルトは、ほとんどの人がとにかく複数のスレッドから単一のファイル記述子に決して書き込みを行わないという規律を行使することです。まあ、その場合、あなたはそれを観察する必要があります<iostream>親切にすべての上のロックつかむだろう<<、すべてを>>。とは対照的に<cstdio>、頻繁にロックすることはなく、ロックしないというオプションさえあります。

<iostream> より多くのロックを消費して、一貫性の低い結果を達成します。


2
printfのほとんどの実装には、ローカライズに非常に役立つ機能があります。番号付きパラメーターです。2つの異なる言語(英語とフランス語など)で出力を生成する必要があり、語順が異なる場合は、同じprintfを異なるフォーマット文字列で使用でき、パラメーターを異なる順序で印刷できます。
gnasher729 2018

2
ストリームのそのステートフルなフォーマットは、私が何を言っていいのかわからないほど多くの見つけにくいバグを与えているに違いありません。すばらしい答えです。できれば、複数回賛成票を投じます。
mathreadler 2018

6
std::coutは数倍遅いprintf()」—この主張はネット全体で繰り返されていますが、これは長い間当てはまりませんでした。最新のIOstream実装はと同等のパフォーマンスを発揮しprintfます。後者は、バッファリングされたストリームとローカライズされたIO(オペレーティングシステムによって実行されますが、それでも行われます)を処理するために内部で仮想ディスパッチを実行します。
Konrad Rudolph

3
@KevinZそれは素晴らしいことですが、fmt(単一の文字列内のさまざまなフォーマットのロット)の特定の長所を示す1つの特定の呼び出しのベンチマークです。より典型的な使用法での差printfcout収縮します。ちなみに、このサイトにはこのようなベンチマークがたくさんあります。
Konrad Rudolph

3
@KonradRudolphどちらでもない。マイクロベンチマークは、実際のプログラムが実行する特定の限られたリソース(レジスター、icache、メモリー、分岐予測子)を使い果たしないため、膨張と間接化のコストを過小予測することがよくあります。「より典型的な使用法」をほのめかしているとき、それは基本的に他の場所でかなり膨満があることを言っています。私の意見では、パフォーマンス要件がない場合は、C ++でプログラミングする必要はありません。
KevinZ 2018

18

他のすべての答えが言っ
たことに加えて、と同じでstd::endlないという事実もあり'\n'ます。

これは、残念ながらよくある誤解です。std::endl「新しい行」を意味するのではなく、「新しい行を出力してからストリームをフラッシュ
する」という意味です。フラッシングは安くはありません!

printfとの違いを完全に無視するとstd::cout、Cの例と機能的に同等になるため、C ++の例は次のようになります。

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    return 0;
}

そして、ここに、フラッシュを含めた場合の例を示します。

C

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    fflush(stdout);
    return 0;
}

C ++

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    std::cout << std::flush;
    return 0;
}

コードを比較するときは、常にlike for like比較し、コードが何をしているかの意味を理解するように常に注意する必要があります。最も単純な例でさえ、一部の人が理解するよりも複雑な場合があります。


実際には、using std::endl 、改行を行バッファー付きのstdioストリームに書き込むのと機能的に同等です。stdout特に、インタラクティブデバイスに接続する場合は、ラインバッファリングまたはバッファリング解除する必要があります。Linuxは、ラインバッファリングされたオプションを主張しています。

実際、iostreamライブラリにラインバッファリングモードありません ...ラインバッファリングの効果を達成する方法は、std::endl改行を出力するために使用することです。

@Hurkyl Insist?次に、何の使用setvbuf(3)ですか?それとも、デフォルトは行バッファリングされていると言う意味ですか?参考:通常、すべてのファイルはブロックバッファリングされます。ストリームが端末を参照する場合(標準出力が通常そうするように)、ストリームはラインバッファリングされます。標準エラーストリームstderrは、デフォルトでは常にバッファリングされていません。
プリフタン共和国2018年

printf改行文字に遭遇したときに自動的にフラッシュしませんか?
bool3max

1
@ bool3maxそれは私の環境が何をしているかを教えてくれるだけで、他の環境では違うかもしれません。最も一般的なすべての実装で同じように動作する場合でも、どこかにエッジケースがあるということではありません。これが標準が非常に重要である理由です-標準は何かがすべての実装で同じでなければならないか、それが実装間で変化することが許されるかどうかを決定します。
Pharap、

16

既存の技術的な答えは正しいですが、問題は最終的にこの誤解から生じていると思います。

C ++では、あなたがあなたが食べるものに対して支払うことは有名です。

これは、C ++コミュニティのマーケティングトークです。(公平を期すために、すべての言語コミュニティでマーケティングトークがあります。)それはあなたが真剣に信頼できる具体的なことを意味するものではありません。

「使用した分だけ支払う」とは、C ++機能を使用している場合にのみ、C ++機能にオーバーヘッドがあることを意味します。しかし、「機能」の定義は無限に細かいわけではありません。多くの場合、複数の側面を持つ機能をアクティブ化することになります。これらの側面のサブセットのみが必要な場合でも、実装が機能を部分的に導入することは実際的または不可能です。

一般に、多くの言語(間違いなくすべてではありませんが)は、さまざまな程度の成功で、効率的になるように努力しています。C ++はスケールのどこかにありますが、この目標を完全に成功させるための特別な、または魔法のようなデザインはありません。


1
使用しないものに対して支払う場所については、例外とRTTIの2つしか考えられません。そしてそれはマーケティングの話ではないと思います。C ++は、基本的にはより強力なCであり、「使用した分だけ支払う必要はありません」。
Rakete1111

2
@ Rakete1111例外がスローされない場合、コストがかからないことは長い間確立されています。プログラムが一貫してスローしている場合は、再設計する必要があります。失敗の状態が制御不能な場合は、ブール値を返す健全性チェックで状態を確認してから、条件のfalseに依存するメソッドを呼び出す必要があります。
schulmaster 2018

1
@schulmaster:C ++で記述されたコードが他の言語で記述されたコードと相互作用する必要がある場合、例外は設計上の制約を課す可能性があります。非ローカル制御の転送は、モジュールが互いに調整する方法を知っている場合にのみ、モジュール間でスムーズに機能するためです。
スーパーキャット2018

1
(間違いなくすべてではありませんが)言語は効率的になるよう努めています。間違いなくすべてではない:難解なプログラミング言語は、斬新で面白く、効率的ではないように努めています。 esolangs.org。それらのいくつかは、BrainFuckのように、非常に非効率的です。または、たとえば、すべての整数を出力するためのシェイクスピアプログラミング言語、最小サイズ227バイト(コードゴルフ)。プロダクション用の言語のうち、ほとんどは効率を目的としていますが、一部の(bashなど)は主に利便性を目的としており、速度が遅いことが知られています。
Peter Cordes

2
まあ、それはマーケティングですが、それはほぼ完全に本当です。でコンパイルする方法と同じよう<cstdio><iostream>、に固執することも含めないこともできます-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
KevinZ 2018

11

C ++の入出力関数はエレガントに記述され、使いやすいように設計されています。多くの点で、これらはC ++のオブジェクト指向機能のショーケースです。

ただし、見返りに少しパフォーマンスをあきらめることは確かですが、オペレーティングシステムがより低いレベルで機能を処理するのにかかる時間と比較すると、それはごくわずかです。

C ++標準の一部であるため、いつでもCスタイルの関数にフォールバックできます。または、完全に移植性を放棄して、オペレーティングシステムへの直接呼び出しを使用することもできます。


23
「C ++の入力/出力関数は、実用性の薄いベニアの背後にあるクトゥルス語の性質を隠すのに苦労している恐ろしい怪物です。多くの点で、これらは現代のC ++コードを設計しない方法のショーケースです。」おそらくもっと正確でしょう。
user673679 '21

3
@ user673679:とてもそうです。C ++ I / Oストリームの大きな問題は、その根底にあるものです。実際に非常 に多くの複雑さがあり、それらに対処したことのある人(私はstd::basic_*stream下向きと呼んでいます)は、入ってくるeadachesを知っています。それらは広く一般的で、継承によって拡張されるように設計されました。しかし、その複雑さ(文字通りiostreamで書かれた本がある)のため、結局誰もそれをしなかったため、そのために新しいライブラリが生まれました(例:boost、ICUなど)。私たちはこの間違いの代金を払うのをやめるとは思えません。
edmz 2018

1

他の回答で見たように、一般的なライブラリにリンクして複雑なコンストラクタを呼び出すと、料金が発生します。ここでは特に問題はありません。いくつかの実世界の側面を指摘します。

  1. Barneには、効率がC ++ではなくCにとどまる理由にならないようにするためのコアデザイン原則がありました。そうは言っても、これらの効率を得るには注意する必要があり、Cスペック内では常に機能しているが「技術的に」ではなかった効率が時々あります。たとえば、ビットフィールドのレイアウトは実際には指定されていません。

  2. ostreamを調べてみてください。ああ、なんてこった!そこにフライトシミュレータを見つけても驚かないでしょう。stdlibのprintf()でも、通常は約50K実行されます。これらは怠惰なプログラマーではありません。printfサイズの半分は、ほとんどの人が使用することのない間接精度の引数に関係しています。ほとんどすべての本当に制約のあるプロセッサのライブラリは、printfの代わりに独自の出力コードを作成します。

  3. サイズの増加は、通常、より柔軟で柔軟なエクスペリエンスを提供します。類推として、自動販売機はコーヒーのような物質のカップを数枚のコインで販売し、トランザクション全体は1分以内に完了します。良いレストランに立ち寄るには、テーブルセッティング、座っている、注文する、待っている、素敵なカップを手に入れる、請求書を受け取る、選択したフォームで支払う、チップを追加する、そして外出先での良い一日を願うことが含まれます。それは別の経験であり、複雑な食事のために友達と一緒に立ち寄る場合により便利です。

  4. 人々はまだANSI Cを書いていますが、K&R Cはめったにありません。私の経験では、C ++コンパイラーでコンパイルして、ドラッグするものを制限するためにいくつかの構成を微調整します。他の言語には良い議論があります。 ; よりスマートなフィールドパッキングとメモリレイアウトについては、いくつかの良い議論があります。私見すべての言語設計は、Zen of Pythonのように、目標のリストから始める必要があると思います。

楽しい議論でした。なぜ魔法のように小さく、シンプルで、エレガントで、完全で、柔軟なライブラリを作成できないのでしょうか。

答えはありません。答えはありません。それが答えです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.