在C ++中,我付出的代价是我不吃东西吗?

让我们考虑C和C ++中的以下hello world示例:

main.c

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

main.cpp

 #include  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 >& std::__ostream_insert<char, std::char_traits >(std::basic_ostream<char, std::char_traits >&, char const*, long) mov edi, OFFSET FLAT:_ZSt4cout call std::basic_ostream<char, std::char_traits >& std::endl<char, std::char_traits >(std::basic_ostream<char, std::char_traits >&) 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 >& std::__ostream_insert >(std::basic_ostream >&, char const*, long) 

再次调用cout打印新行并刷新

  mov edi, OFFSET FLAT:_ZSt4cout call std::basic_ostream >& std::endl >(std::basic_ostream >&) 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::coutprintf更强大,更复杂。 它支持区域设置,有状态格式标记等内容。

如果你不需要那些,请使用std::printfstd::puts – 它们在可用。


众所周知,在C ++中,你需要为你所吃的东西买单。

我还要说清楚C ++ != C ++标准库。 标准库应该是通用的并且“足够快”,但它通常比您需要的专门实现慢。

另一方面,C ++语言努力使编写代码成为可能,而无需支付不必要的额外隐藏成本(例如,选择加入virtual ,不进行垃圾收集)。

你不是在比较C和C ++。 您正在比较printfstd::cout ,它们具有不同的function(区域设置,有状态格式等)。

尝试使用以下代码进行比较。 Godbolt为两个文件生成相同的程序集(使用gcc 8.2,-O3进行测试)。

main.c中:

 #include  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  #include  int main() { std::array arr {1, 2, 3, 4, 5, 6}; for (auto x : arr) { std::printf("%d\n", x); } } 

您的列表确实在比较苹果和橙子,但不是出于大多数其他答案暗示的原因。

我们来看看你的代码实际上做了什么:

C:

  • 打印一个字符串, "Hello world\n"

C ++:

  • 将字符串"Hello world"流式传输到std::cout
  • std::endl操纵器流式传输到std::cout

显然你的C ++代码做了两倍的工作。 为了公平比较,我们应该结合这个:

 #include  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 >& std::operator<<  >(std::basic_ostream >&, char const*) xor eax, eax add rsp, 8 ret 

实际上,我们可以逐行比较C和C ++代码,并且差别很小

 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 >& std::operator<<  >(std::basic_ostream >&, char const*) xor eax, eax xor eax, eax add rsp, 8 add rsp, 8 ret ret 

唯一真正的区别是在C ++中我们用两个参数( std::cout和字符串)调用operator << 。 我们可以通过使用更接近的C eqivalent: fprintf来删除那个微小的差异, fprintf也有一个指定流的第一个参数。

这留下了_GLOBAL__sub_I_main的汇编代码,它是为C ++但不是C生成的。这是在这个汇编列表中可见的唯一真正的开销(当然, 这两种语言的开销更多,不可见)。 此代码在C ++程序的开头执行一些C ++标准库函数的一次性设置。

但是,正如其他答案所解释的那样,这两个程序之间的相关差异将不会出现在mainfunction的assembly输出中,因为所有繁重的工作都发生在幕后。

众所周知,在C ++中,你需要为你所吃的东西买单。 所以,在这种情况下,我付的是什么?

这很简单。 你支付std::cout 。 “你只为你吃的东西买单”并不意味着“你总是得到最优惠的价格”。 当然, printf更便宜。 人们可以争辩说std::cout更安全,更通用,因此它的成本更高是合理的(它花费更多,但提供了更多的价值),但是忽略了这一点。 你不使用printf ,你使用std::cout ,所以你需要付费使用std::cout 。 您不需要支付使用printf

一个很好的例子是虚函数。 虚函数具有一些运行时成本和空间要求 – 但前提是您实际使用它们。 如果您不使用虚拟function,则不需要支付任何费用。

一些评论

  1. 即使C ++代码评估更多的汇编指令,它仍然是一些指令,任何性能开销仍然可能与实际的I / O操作相比相形见绌。

  2. 实际上,有时它甚至比“在C ++中为你所吃的东西买单”更好。 例如,编译器可以推断在某些情况下不需要虚函数调用,并将其转换为非虚拟调用。 这意味着您可以免费获得虚拟function。 那不是很好吗?

“printf的汇编列表”不适用于printf,但适用于puts(编译器优化的种类?); printf比put更复杂…别忘了!

我在这里看到一些有效的答案,但我会更详细地了解细节。

如果您不想浏览整个文本墙,请跳至下面的摘要以获取主要问题的答案。


抽象化

所以,在这种情况下,我付的是什么?

你正在为抽象买单 。 能够编写更简单,更人性化的代码需要付出代价。 在C ++中,这是一种面向对象的语言,几乎所有东西都是一个对象。 当您使用任何对象时,总有三件事情会发生在幕后:

  1. 对象创建,基本上是对象本身及其数据的内存分配。
  2. 对象初始化(通常通过一些init()方法)。 通常,内存分配在这个步骤中首先发生在引擎盖下。
  3. 对象破坏(并非总是如此)。

你没有在代码中看到它,但每次使用一个对象时,上述三个方面都需要以某种方式发生。 如果你手动完成所有操作,代码显然会更长。

现在,可以在不增加开销的情况下有效地进行抽象:编译器和程序员都可以使用方法内联和其他技术来消除抽象开销,但这不是您的情况。

C ++中真正发生了什么?

在这里,细分:

  1. 初始化了std::ios_base类,它是I / O相关所有内容的基类。
  2. std::cout对象已初始化。
  3. 您的字符串被加载并传递给std::__ostream_insert ,它(正如您已经通过名称std::__ostream_insert )是std::cout (基本上是<<运算符)的方法,它将一个字符串添加到流中。
  4. cout::endl也传递给std::__ostream_insert
  5. __std_dso_handle传递给__cxa_atexit ,这是一个全局函数,负责在退出程序之前“清理”。 __std_dso_handle本身由此函数调用以释放和销毁剩余的全局对象。

那么使用C ==不支付任何费用?

在C代码中,发生的步骤非常少:

  1. 您的字符串被加载并通过edi寄存器传递给puts
  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,后跟exit syscall。 现在, 将是完成同样事情的最低限度。


总结一下

C更加简单 ,只需要最低限度,只需对用户完全控制,即可完全优化和自定义他们想要的任何东西。 您告诉处理器在寄存器中加载一个字符串,然后调用库函数来使用该字符串。 另一方面,C ++更复杂和抽象 。 这在编写复杂的代码时具有巨大的优势,并且允许更容易编写和更人性化的代码,但显然需要付出代价。 在这种情况下,与C相比,C ++的性能总会出现问题,因为C ++提供的不仅仅是完成这些基本任务所需要的,因此增加了更多的开销

回答你的主要问题

我付钱给我不吃的东西吗?

在这个特定情况下, 是的 。 你没有利用C ++提供的任何东西而不是C,但这仅仅是因为C ++可以帮助你的那段简单的代码中没有任何东西:它非常简单,你真的根本不需要C ++。


哦,还有一件事!

乍一看,C ++的优点可能看起来并不明显,因为你编写了一个非常简单的小程序,但是看一些更复杂的例子并看到差异(两个程序完全相同):

C

 #include  #include  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  #include  #include  using namespace std; int main(void) { int n; cout << "How many integers do you want to input? "; cin >> n; vector 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管理较低级别的malloc以及如何在索引和大小时更加小心,以及在进行输入和打印时需要非常具体。

一开始有一些误解。 首先,C ++程序不会产生22条指令,它更像22,000条指令(我从我的帽子中提取了这个数字,但它大致在球场上)。 此外,C代码也不会产生9条指令。 那些只是你看到的那些。

C代码做的是,在做了很多你看不到的东西之后,它从CRT中调用一个函数(通常但不一定作为共享库存在),然后检查返回值或句柄错误,并纾困。 根据编译器和优化设置,它甚至不会真正调用printf而是puts ,或者甚至更原始的东西。
你可以在C ++中编写或多或少相同的程序(除了一些不可见的init函数),如果你只是以相同的方式调用相同的函数。 或者,如果你想要超正确,那就是以std::为前缀的相同函数。

相应的C ++代码实际上并非完全相同。 虽然整个众所周知,它是一个肥胖的丑陋猪,为小型程序增加了巨大的开销(在“真正的”程序中你并没有真正注意到那么多),一个更公平的解释是它做了很多你看不到的东西,哪些只是有效 。 包括但不限于几乎任何随意的东西的魔法格式,包括不同的数字格式和区域设置等等,缓冲和正确的error handling。 error handling? 是的,猜猜看,输出字符串实际上可能会失败,而且与C程序不同,C ++程序不会默默地忽略它。 考虑到std::ostreamstd::ostream做了什么,没有人知道,它实际上非常轻量级。 不像我正在使用它,因为我厌恶流语法。 但是,如果你考虑一下它的作用,那就太棒了。

但可以肯定的是,C ++整体效率不如C高。 它不是那么有效,因为它不是同一个东西而且它没有同样的事情。 如果不出意外,C ++会生成exception(以及生成,处理或失败的代码),并且它提供了C不提供的一些保证。 所以,当然,C ++程序有点需要更大一点。 然而,从大局来看,这无论如何都无关紧要。 相反,对于真正的程序,我并不是很少发现C ++表现更好,因为出于某种原因,它似乎有助于更有利的优化。 不要问我为什么特别,我不会知道。

如果你不是为了最好的方法,而是为了最好的方法而编写正确的 C代码(即你实际检查错误,并且程序在出现错误时表现正确),那么差异是微不足道的,如果存在的话。

你正在为错误买单。 在80年代,当编译器不足以检查格式字符串时,操作符重载被视为在io期间强制执行某些类型安全性的好方法。 但是,它的每一个横幅function都从一开始就严重执行或从概念上破产:

<了iomanip>

C ++流io api中最令人厌恶的部分是这个格式化头库的存在。 除了有状态,丑陋和容易出错之外,它还将格式与流相结合。

假设您要打印出一行,其中包含8位零填充hex无符号整数,后跟一个空格,后跟一个带有3位小数的双精度数。 使用 ,您可以阅读简洁的格式字符串。 使用 ,您必须保存旧状态,将对齐设置为右,设置填充字符,设置填充宽度,将base设置为hex,输出整数,恢复保存状态(否则您的整数格式将污染您的浮动格式),输出空格,将表示法设置为固定,设置精度,输出double和换行符,然后恢复旧格式。

 //  std::printf( "%08x %.3lf\n", ival, fval ); //  &  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); 

运算符重载

是如何不使用运算符重载的典型代表:

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

性能

std::coutprintf() std::cout几倍。 猖獗的特征性和虚拟调度确实造成了损失。

线程安全

都是线程安全的,因为每个函数调用都是primefaces的。 但是, printf()每次调用都会完成更多工作。 如果使用选项运行以下程序,则只会看到一行f 。 如果您在多核计算机上使用 ,您可能会看到其他内容。

 // g++ -Wall -Wextra -Wpedantic -pthread -std=c++17 cout.test.cpp #define USE_STREAM 1 #define REPS 50 #define THREADS 10 #include  #include  #if USE_STREAM #include  #else #include  #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 {}; for ( int i = 0; i < THREADS; ++i ) threads.emplace_back(task); for ( auto & t : threads ) t.join(); #if USE_STREAM std::cout << "\n\n"; #else std::printf ( "\n\n" ); #endif } 

对这个例子的反驳是,大多数人都会遵守纪律,从而永远不会从多个线程写入单个文件描述符。 那么,在这种情况下,你必须观察将有助于锁定每个<<和每个>> 。 而在 ,您不会经常锁定,甚至可以选择不锁定。

消耗更多锁以实现不太一致的结果。

除了所有其他答案所说的,
还有一个事实是std::endl'\n'

这是一个不幸的常见误解。 std::endl并不意味着“新行”,
它的意思是“打印新行,然后刷新流 ”。 法拉盛并不便宜!

完全忽略了printfstd::cout之间的差异片刻,在function上与你的C示例相同,你的C ++示例应该如下所示:

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

这里有一个例子,说明如果你包括刷新你的例子应该是什么样的。

C

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

C ++

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

在比较代码时, 您应该始终小心,您要比较喜欢并且了解代码所做的事情的含义。 有时甚至最简单的例子也比有些人意识到的要复杂得多。

虽然现有的技术答案是正确的,但我认为这个问题最终源于这种误解:

众所周知,在C ++中,你需要为你所吃的东西买单。

这只是来自C ++社区的营销演讲。 (To be fair, there’s marketing talk in every language community.) It doesn’t mean anything concrete that you can seriously depend on.

“You pay for what you use” is supposed to mean that a C++ feature only has overhead if you’re using that feature. But the definition of “a feature” is not infinitely granular. Often you will end up activating features that have multiple aspects, and even though you only need a subset of those aspects, it’s often not practical or possible for the implementation to bring the feature in partially.

In general, many (though arguably not all) languages strive to be efficient, with varying degrees of success. C++ is somewhere on the scale, but there is nothing special or magical about its design that would allow it to be perfectly successful in this goal.

The Input / Output functions in C++ are elegantly written and are designed so they are simple to use. In many respects they are a showcase for the object-orientated features in C++.

But you do indeed give up a bit of performance in return, but that’s negligible compared to the time taken by your operating system to handle the functions at a lower level.

You can always fall back to the C style functions as they are part of the C++ standard, or perhaps give up portability altogether and use direct calls to your operating system.

As you have seen in other answers, you pay when you link in general libraries and call complex constructors. There is no particular question here, more a gripe. I’ll point out some real-world aspects:

  1. Barne had a core design principle to never let efficiency be a reason for staying in C rather than C++. That said, one needs to be careful to get these efficiencies, and there are occasional efficiencies that always worked but were not ‘technically’ within the C spec. For example, the layout of bit fields was not really specified.

  2. Try looking through ostream. Oh my god its bloated! I wouldn’t be surprised to find a flight simulator in there. Even stdlib’s printf() usally runs about 50K. These aren’t lazy programmers: half of the printf size was to do with indirect precision arguments that most people never use. Almost every really constrained processor’s library creates its own output code instead of printf.

  3. The increase in size is usually providing a more contained and flexible experience. As an analogy, a vending machine will sell a cup of coffee-like-substance for a few coins and the whole transaction takes under a minute. Dropping into a good restaurant involves a table setting, being seated, ordering, waiting, getting a nice cup, getting a bill, paying in your choice of forms, adding a tip, and being wished a good day on your way out. Its a different experience, and more convenient if you are dropping in with friends for a complex meal.

  4. People still write ANSI C, though rarely K&R C. My experience is we always compile it with a C++ compiler using a few configuration tweaks to limit what is dragged in. There are good arguments for other languages: Go removes the polymorphic overhead and crazy preprocessor; there have been some good arguments for smarter field packing and memory layout. IMHO I think any language design should start with a listing of goals, much like the Zen of Python .

It’s been a fun discussion. You ask why can’t you have magically small, simple, elegant, complete, and flexible libraries?

There is no answer. There will not be an answer. 这就是答案。