CとC++における次のようなHello Worldの例を考えてみましょう。
#include <stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
#include <iostream>
int main()
{
std::cout<<"Hello world"<<std::endl;
return 0;
}
アセンブリにゴッドボルトでコンパイルすると、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++であなたが食べるものの代金を支払うことは有名です。だから、この場合、私は何を払っているのですか?
あなたが払っているのは、(ライブラリに印刷するほど重くない)重いライブラリを呼び出すことです。 ostream
オブジェクトを初期化します。いくつかの隠しストレージがあります。次に、std::endl
の同義語ではない\n
を呼び出します。 iostream
ライブラリはあなたが多くの設定を調整し、プログラマーよりもむしろプロセッサに負担をかけるのを助けます。これはあなたが払っているものです。
コードを見てみましょう。
.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
また、言語と図書館を区別することも不可欠です。
ところで、これは物語のほんの一部です。呼び出している関数に何が書かれているのかわかりません。
だから、この場合、私は何を払っているのですか?
std::cout
はprintf
よりも強力で複雑です。ロケール、ステートフルフォーマットフラグなどをサポートしています。
必要ない場合は、std::printf
またはstd::puts
を使用してください - それらは<cstdio>
で利用可能です。
C++であなたが食べるものの代金を支払うことは有名です。
また、C++ != C++標準ライブラリであることを明確にしたいと思います。標準ライブラリは汎用で "十分に速い"ものとされていますが、多くの場合、必要とするものを特殊に実装したものよりも遅くなります。
一方、C++言語は、不要な余分な隠れたコストを支払うことなくコードを記述できるようにすることを目指しています(例:opt-in virtual
、ガベージコレクションなし)。
あなたは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);
}
}
あなたのリストは確かにりんごとオレンジを比較しています、しかしほとんどの他の答えで暗示されている理由のためではありません。
あなたのコードが実際に何をするのかチェックしましょう。
"Hello world\n"
という単一の文字列を出力します"Hello world"
をstd::cout
にストリームするstd::endl
マニピュレータをstd::cout
にストリームする明らかにあなたのC++コードは2倍の仕事をしています。公正な比較のために、これを組み合わせる必要があります。
#include <iostream>
int main()
{
std::cout<<"Hello world\n";
return 0;
}
…そして突然、main
のアセンブリコードはCのコードと非常によく似たものになります。
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++では、2つの引数(operator <<
と文字列)を付けてstd::cout
を呼び出すことです。そのわずかな違いでさえも、C言語の同等のfprintf
を使用することで除去できます。これには、ストリームを指定する最初の引数もあります。
これは、C++ではなくC++に対して生成される_GLOBAL__sub_I_main
のアセンブリコードをそのままにします。これは、このアセンブリリストに表示される唯一の真のオーバーヘッドです(もちろん、 both 言語には見えないオーバーヘッドがあります)。このコードは、C++プログラムの開始時に、一部のC++標準ライブラリ関数の一度限りのセットアップを実行します。
しかし、他の答えで説明されているように、これら2つのプログラムの間の関連する違いは、すべての重い作業が舞台裏で行われるため、main
関数のAssembly出力には見られません。
C++では、食べるものにお金を払うことで有名です。したがって、この場合、私は何を払っていますか?
簡単です。 std::cout
の料金を支払います。 「あなたが食べるものだけにお金を払う」というのは、「常に最高の価格を手に入れる」という意味ではありません。確かに、printf
は安価です。 std::cout
の方が安全で汎用性が高いと主張することができます。したがって、コストの増加は正当化されます(コストは高くなりますが、より多くの価値があります)。 printf
を使用せず、std::cout
を使用するため、std::cout
を使用して料金を支払います。 printf
を使用しても料金はかかりません。
良い例は仮想関数です。仮想関数には実行時のコストとスペースの要件がありますが、実際にを使用する場合のみです。仮想機能を使用しない場合、料金は発生しません。
いくつかの発言
C++コードがより多くのAssembly命令を評価したとしても、それはまだ少数の命令であり、パフォーマンスオーバーヘッドは実際のI/O操作によって依然としてd小化されている可能性があります。
実際、「C++では食事にお金を払う」よりも優れている場合もあります。たとえば、コンパイラは、特定の状況で仮想関数呼び出しが不要であると推測し、それを非仮想呼び出しに変換できます。つまり、freeの仮想関数を取得できます。それは素晴らしいことではありませんか?
"printfのアセンブリリスト"はprintf用ではなくputs用です(一種のコンパイラ最適化?)。 printfはputよりはるかに複雑です...忘れないでください!
ここにはいくつか有効な答えがありますが、もう少し詳細を説明します。
このテキスト全体を読みたくない場合は、以下の概要に移動して、主な質問への回答をご覧ください。
したがって、この場合、私は何を払っていますか?
abstractionの料金を支払っています。よりシンプルで人間に優しいコードを記述できるようになるには、コストがかかります。オブジェクト指向言語であるC++では、ほとんどすべてがオブジェクトです。オブジェクトを使用すると、3つの主なことが常に内部で発生します。
init()
メソッドを使用)。通常、メモリの割り当ては、このステップの最初のものとして内部で行われます。コードには表示されませんが、オブジェクトを使用するたびに、上記の3つのすべてが何らかの形で発生する必要があります。すべてを手動で行う場合、コードは明らかにずっと長くなります。
現在、オーバーヘッドを追加せずに抽象化を効率的に行うことができます。抽象化のオーバーヘッドを削除するために、コンパイラーとプログラマーの両方がメソッドのインライン化およびその他の手法を使用できますが、これはあなたの場合ではありません。
ここに、分解されます:
std::ios_base
クラスが初期化されます。これは、I/O関連のすべての基本クラスです。std::cout
オブジェクトが初期化されます。std::__ostream_insert
に渡されます。これは(名前で既にわかっているように)ストリームに文字列を追加するstd::cout
(基本的に<<
演算子)のメソッドです。cout::endl
もstd::__ostream_insert
に渡されます。__std_dso_handle
は__cxa_atexit
に渡されます。これは、プログラムを終了する前に「クリーニング」を行うグローバル関数です。 __std_dso_handle
自体は、残りのグローバルオブジェクトの割り当てを解除して破棄するために、この関数によって呼び出されます。Cコードでは、非常に少ないステップが発生しています。
puts
レジスタを介してedi
に渡されます。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 に続いてexit
syscallが呼び出されるだけです。これで、thisが同じことを達成するための最低限になります。
Cはより素朴な方法であり、必要な最低限の機能のみを行い、ユーザーが完全に制御できるようにします。これにより、基本的に必要なものを完全に最適化およびカスタマイズできます。プロセッサにレジスタに文字列をロードし、ライブラリ関数を呼び出してその文字列を使用するように指示します。 C++はより複雑で抽象的です。これは、複雑なコードを作成する場合に大きな利点があり、より簡単で人間に優しいコードを作成できますが、明らかにコストがかかります。 C++は、このような基本的なタスクを実行するために必要なものより多くを提供するため、オーバーヘッドが追加されるであるため、このような場合のCと比較すると、C++のパフォーマンスには常に欠点があります。
主な質問に答える:
食べていないものにお金を払っていますか?
この特定の場合、yes。 C++がC以上のものを提供しなければならないことを利用していませんが、それはC++があなたを助けることができるその単純なコード部分に何もないからです:それはあなたが本当にC++をまったく必要としないほど単純です。
ああ、もう一つだけ!
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
を使用して下位レベルでメモリを管理する方法、およびインデックス作成とサイズに注意する必要がある方法、および入力と印刷。
最初にいくつかの誤解があります。最初に、C++プログラムしないは22命令になり、22,000命令のようになります(その数字を帽子から引き出しましたが、ほぼ球場にあります)。また、Cコードdoes n'tは、9命令にもなります。それらはあなたが見るものだけです。
Cコードが行うことは、見えない多くのことを行った後、CRTから関数を呼び出します(通常は共有ライブラリとして存在しますが、必ずしも共有ライブラリとして存在するわけではありません)does戻り値を確認するか、エラーを処理して、解決します。コンパイラと最適化の設定によっては、printf
を実際に呼び出すことはありませんが、puts
を呼び出すことも、よりプリミティブなものを呼び出すこともあります。
同じ関数を同じ方法で呼び出した場合に限り、C++でも同じプログラム(いくつかの非表示のinit関数を除く)を作成できます。または、非常に正確にしたい場合は、同じ関数にstd::
というプレフィックスを付けます。
対応するC++コードは実際にはまったく同じではありません。 <iostream>
全体は、小さなプログラム(「実際の」プログラムではそれほど気づかない)に莫大なオーバーヘッドを追加する太いい豚であることでよく知られていますが、やや公平な解釈はそれはあなたが見ないもの、そしてちょうどうまくいくという非常に多くのものをします。さまざまな数字の形式とロケール、その他、バッファリング、適切なエラー処理など、ほぼすべての偶発的なものの魔法の書式設定が含まれますが、これに限定されません。エラー処理?そうですね、文字列の出力は実際に失敗する可能性があります。Cプログラムとは異なり、C++プログラムはnotを黙って無視します。 std::ostream
が内部で何をするかを考えると、誰も気付かないうちに、それは実際にはかなり軽量です。私は情熱を持ってストリーム構文を嫌っているので、私はそれを使用しているようではありません。しかし、それでも、それが何をするのかを考えれば、それはかなり素晴らしいです。
しかし、確かに、C++全体はnot Cができる限り効率的です。同じものではなく、doing同じものではないため、効率的ではありません。それ以外の場合、C++は例外(およびそれらを生成、処理、または失敗するコード)を生成し、Cが与えない保証を提供します。そのため、C++プログラムは必ず少し大きくする必要があります。ただし、全体像では、これはどうでも構いません。それどころか、realプログラムの場合、何らかの理由でより好ましい最適化に役立つように見えるため、C++のパフォーマンスが向上することはめったにありません。なぜ、特にわからないのかと聞かないでください。
Fire-and-forget-hope-for-the-bestの代わりにcorrect(つまり、実際にエラーをチェックし、エラーの存在下でプログラムが正しく動作する)であるCコードを作成する場合)存在する場合、差はわずかです。
あなたは間違いを払っています。 80年代、コンパイラがフォーマット文字列をチェックするのに十分ではなかったとき、演算子オーバーロードは、ioの間に型の安全性をいくらか強制するための良い方法と見られていました。ただし、バナー機能はすべて、最初からひどく実装されているか、概念的に破綻しています。
C++ストリームio apiの最も気になる部分は、このフォーマットヘッダライブラリの存在です。ステートフルで醜く、エラーが発生しやすいだけでなく、フォーマットとストリームを結合します。
8桁のゼロで埋められた16進数のunsigned int、その後にスペース、小数点以下3桁のdoubleが続く行を印刷したいとします。 <cstdio>
を使えば、簡潔なフォーマット文字列を読むことができます。 <ostream>
では、古い状態を保存し、右寄せを設定し、塗りつぶし文字を設定し、塗りつぶし幅を設定し、基数を16進数に設定し、整数を出力し、 space、表記法をfixedに設定し、精度を設定し、doubleと改行を出力してから、古いフォーマットを復元します。
// <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()
は呼び出しごとにより多くのことが行われます。 <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>
は、一貫性の低い結果を得るために、より多くのロックを消費します。
他のすべての答えが言っていることに加えて、std::endl
が'\n'
と同じ{notであるという事実もあります。
これは残念ながら一般的な誤解です。 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 のように比較していることと、コードが実行していることの意味を理解していることを常に注意してください。時には最も簡単な例でさえ、何人かの人々が理解するよりも複雑です。
既存の技術的な答えは正しいのですが、問題は結局この誤解から生じると思います。
C++であなたが食べるものの代金を支払うことは有名です。
これはC++コミュニティからの単なるマーケティングの話です。 (公平を期すために、あらゆる言語コミュニティでマーケティングの話があります。)それはあなたが真剣に頼ることができるという具体的な何かを意味するのではありません。
「あなたが使うものの代金を払う」とは、C++機能がその機能を使っている場合にのみオーバーヘッドがあるということを意味しています。しかし 「機能」の定義は無限に細かいわけではありません。 多くの場合、複数の側面を持つ機能をアクティブ化することになります。それらの側面のサブセットしか必要ない場合でも、実装で部分的に機能を取り込むことは実用的ではないか不可能です。
一般に、多くの(おそらく間違いなく全部ではないが)言語は効率の向上を目指しており、成功の程度はさまざまです。 C++は規模のどこかにありますが、その設計に関してこの目標を完全に成功させることを可能にするような特別なものや魔法のようなものは何もありません。
C++の入出力関数は上品に書かれており、使いやすいように設計されています。多くの点で、それらはC++のオブジェクト指向機能のショーケースです。
しかし、あなたは実際に少しパフォーマンスをあきらめますが、それはあなたのオペレーティングシステムがより低いレベルで機能を処理するのにかかる時間と比較してごくわずかです。
Cスタイル関数はC++標準の一部であるため、いつでもCスタイル関数にフォールバックすることも、おそらく移植性を完全に断念してオペレーティングシステムへの直接呼び出しを使用することもできます。
あなたが他の答えで見たように、あなたが一般的な図書館でリンクして、そして複雑な構成者を呼ぶとき、あなたは支払います。ここでは特に疑問はありませんが、もっと不満です。実世界の側面をいくつか指摘します。
Barneは、効率性がC++ではなくCに留まる理由にならないようにするというコア設計原則を持っていました。とは言っても、これらの効率性を得るためには注意が必要です。また、C仕様の範囲内では常に機能するが「技術的」ではない効率性が時折あります。たとえば、ビットフィールドのレイアウトは実際には指定されていません。
Ostreamを通して見てみてください。ああ私の神はその肥大化した!そこにフライトシミュレータがあっても驚きません。 stdlibのprintf()でさえも、通常は50Kくらいです。これらは怠惰なプログラマーではありません:printfのサイズの半分は、ほとんどの人が決して使わない間接的な精度の引数を扱うためのものでした。ほとんどすべての本当に制限されたプロセッサのライブラリはprintfの代わりにそれ自身の出力コードを作成します。
サイズの増加は通常、より包括的で柔軟な体験を提供しています。例えとして、自動販売機は一杯のコーヒーのような物質を数枚のコインで売るでしょう、そして取引全体は1分もかかりません。良いレストランに立ち寄るには、テーブルセッティング、着席中、注文中、待っている、素敵なカップをもらう、請求書を受け取る、フォームの選択で支払う、チップを追加する、そして外出先で楽しい一日を過ごすことが必要です。それは違う経験です、そしてあなたが複雑な食事のために友達と一緒に立ち寄っているならもっと便利です。
K&R Cはめったにありませんが、人々はまだANSI Cを書いています。私の経験では、引き込まれるものを制限するためにいくつかの設定の調整を使用してC++コンパイラでそれをコンパイルします。 ;よりスマートなフィールドパッキングとメモリレイアウトについては、いくつかの良い議論があります。私見 Zen of Python のように、どんな言語設計でも目標のリストから始めるべきだと思います。
それは楽しい議論でした。あなたはなぜあなたが魔法のように小さく、シンプルで、エレガントで、完全で、そして柔軟なライブラリを手に入れることができないのですか?
答えはありません。答えはありません。それが答えです。