让我们考虑C和C++中的以下hello world示例:
#include <stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
Run Code Online (Sandbox Code Playgroud)
#include <iostream>
int main()
{
std::cout<<"Hello world"<<std::endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
当我在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
Run Code Online (Sandbox Code Playgroud)
但是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
Run Code Online (Sandbox Code Playgroud)
......哪个更大.
众所周知,在C++中,你需要为你所吃的东西买单.所以,在这种情况下,我付的是什么?
Vit*_*meo 208
所以,在这种情况下,我付的是什么?
std::cout比...更强大,更复杂printf.它支持区域设置,有状态格式标记等内容.
如果您不需要,请使用std::printf或std::puts- 它们可用<cstdio>.
众所周知,在C++中,你需要为你所吃的东西买单.
我还要说清楚C++ != C++标准库.标准库应该是通用的并且"足够快",但它通常比您需要的专门实现慢.
另一方面,C++语言努力使编写代码成为可能,而无需支付不必要的额外隐藏成本(例如,选择加入virtual,不进行垃圾回收).
psc*_*ill 173
您不是在比较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;
}
Run Code Online (Sandbox Code Playgroud)
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);
}
}
Run Code Online (Sandbox Code Playgroud)
Kon*_*lph 134
您的列表确实在比较苹果和橙子,但不是因为大多数其他答案暗示的原因.
我们来看看你的代码实际上做了什么:
"Hello world\n""Hello world"流入std::coutstd::endl操纵器流入std::cout显然你的C++代码做了两倍的工作.为了公平比较,我们应该结合这个:
#include <iostream>
int main()
{
std::cout<<"Hello world\n";
return 0;
}
Run Code Online (Sandbox Code Playgroud)
...突然你的汇编代码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
Run Code Online (Sandbox Code Playgroud)
实际上,我们可以逐行比较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<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
Run Code Online (Sandbox Code Playgroud)
唯一真正的区别是在C++中我们调用operator <<两个参数(std::cout和字符串).我们可以通过使用更接近的C eqivalent:来删除那个微小的差异fprintf,它也有一个指定流的第一个参数.
这留下了汇编代码_GLOBAL__sub_I_main,它是为C++而不是C生成的.这是在这个汇编列表中可见的唯一真正的开销(当然,这两种语言的开销更多,不可见).此代码在C++程序的开头执行一些C++标准库函数的一次性设置.
但是,正如其他答案中所解释的那样,这两个程序之间的相关差异将不会在main函数的汇编输出中找到,因为所有繁重的工作都发生在幕后.
Ara*_*ash 59
你要付出的代价就是调用一个沉重的库(不像打印到控制台那么重).您初始化一个ostream对象.有一些隐藏的存储空间.然后,你调用std::endl哪个不是同义词\n.该iostream库可帮助您调整许多设置并将负担放在处理器而不是程序员身上.这就是你要付出的代价.
我们来看看代码:
.LC0:
.string "Hello world"
main:
Run Code Online (Sandbox Code Playgroud)
初始化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)
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
静态存储初始化:
_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
Run Code Online (Sandbox Code Playgroud)
此外,必须区分语言和库.
顺便说一下,这只是故事的一部分.你不知道你正在调用的函数中写了什么.
el.*_*ado 53
众所周知,在C++中,你需要为你所吃的东西买单.所以,在这种情况下,我付的是什么?
这很简单.你买单std::cout."你只为你所吃的东西买单"并不意味着"你总是得到最优惠的价格".当然,printf更便宜.人们可以说这std::cout是更安全和更通用的,因此其更高的成本是合理的(它花费更多,但提供更多的价值),但这忽略了这一点.你不使用printf,你使用std::cout,所以你付费使用std::cout.你不用付钱printf.
一个很好的例子是虚函数.虚函数具有一些运行时成本和空间要求 - 但前提是您实际使用它们.如果您不使用虚拟功能,则不需要支付任何费用.
一些评论
即使C++代码评估更多的汇编指令,它仍然是一些指令,并且任何性能开销仍然可能与实际的I/O操作相比相形见绌.
实际上,有时它甚至比"在C++中为你所吃的东西买单"更好.例如,编译器可以推断在某些情况下不需要虚函数调用,并将其转换为非虚拟调用.这意味着您可以免费获得虚拟功能.那不是很好吗?
小智 47
"printf的汇编列表"不适用于printf,但适用于puts(编译器优化的种类?); printf比put更复杂...不要忘记!
Mar*_*lli 44
我在这里看到了一些有效的答案,但我将更详细地了解细节.
如果您不想浏览整个文本墙,请跳至下面的摘要以获取主要问题的答案.
所以,在这种情况下,我付的是什么?
你是在为抽象买单.能够编写更简单,更人性化的代码需要付出代价.在C++中,它是一种面向对象的语言,几乎所有东西都是一个对象.当您使用任何对象时,总有三件事情会发生在幕后:
init()方法).通常,内存分配在这个步骤中首先发生在引擎盖下.你没有在代码中看到它,但每次使用一个对象时,上述三个方面都需要以某种方式发生.如果您手动完成所有操作,代码显然会更长.
现在,可以在不增加开销的情况下有效地进行抽象:编译器和程序员可以使用方法内联和其他技术来消除抽象开销,但这不是您的情况.
在这里,细分:
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
Run Code Online (Sandbox Code Playgroud)
这基本上只会导致调用write syscall后跟exitsyscall.现在,这将是完成同样事情的最低限度.
C更加简单,只需要最低限度,只需对用户完全控制,即可完全优化和自定义他们想要的任何东西.您告诉处理器在寄存器中加载一个字符串,然后调用库函数来使用该字符串.另一方面,C++更复杂,更抽象.这在编写复杂代码时具有巨大的优势,并且允许更容易编写和更人性化的代码,但显然需要付出代价.在这种情况下,如果与C相比,C++的性能总会出现问题,因为C++提供的不仅仅是完成这些基本任务所需要的,因此会增加更多的开销.
回答你的主要问题:
我付的是我不吃的东西吗?
在这个具体案例中,是的.你没有利用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;
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
希望你能清楚地看到我的意思.另请注意在C中如何使用较低级别管理内存malloc以及free如何在索引和大小方面需要更加小心,以及在进行输入和打印时如何非常具体.
Dam*_*mon 27
开始时有一些误解.首先,C++程序不会产生22条指令,它更像22,000条指令(我从我的帽子中提取了这个数字,但它大致在球场上).此外,C代码也不会产生9条指令.那些只是你看到的.
C代码做的是,在做了很多你看不到的东西后,它从CRT中调用一个函数(通常但不一定作为共享库存在),然后不检查返回值或句柄错误,并纾困.根据编译器和优化设置,它甚至不真的叫printf,但是puts,甚至更原始的东西.
你可以在C++中编写或多或少相同的程序(除了一些不可见的init函数),如果你只是以相同的方式调用相同的函数.或者,如果你想要超正确,那同样的功能前缀std::.
相应的C++代码实际上并非完全相同.虽然整个<iostream>它都是一个肥胖丑陋的猪,为小型程序增加了巨大的开销(在"真正的"程序中你并没有真正注意到那么多),但是更为公平的解释是它做得太糟糕了很多东西,你没有看到,哪些只是工作.包括但不限于几乎任何杂乱的东西的魔法格式,包括不同的数字格式和区域设置和诸如此类,缓冲和正确的错误处理.错误处理?是的,猜猜看,输出字符串实际上可能会失败,而且与C程序不同,C++程序不会默默地忽略它.考虑std::ostream到引擎盖下的内容,没有人知道,它实际上非常轻巧.不像我正在使用它,因为我讨厌激情的流语法.但是,如果你考虑一下它的作用,那就太棒了.
但肯定的是,C++总体是不一样高效为C即可.它不是那么有效,因为它不是同一个东西而且它没有做同样的事情.如果不出意外,C++会生成异常(以及生成,处理或失败的代码),并且它提供了C不提供的一些保证.所以,当然,C++程序有点需要更大一点.然而,从大局来看,这无论如何都无关紧要.相反,对于真正的程序,我并不是很少发现C++表现更好,因为出于某种原因,它似乎有助于更有利的优化.不要问我为什么特别,我不会知道.
如果你不是为了最好的方法而不是为了最好的方法而是你想要编写正确的 C代码(即你实际检查错误,并且程序在出现错误时表现正确),那么差异是微不足道的,如果存在的话.
Kev*_*inZ 22
你正在为错误买单.在80年代,当编译器不足以检查格式字符串时,运算符重载被认为是在io期间强制执行某些类型安全性的好方法.但是,它的每一个横幅功能都从一开始就严重执行或从概念上破产:
C++流io api中最令人厌恶的部分是这个格式化头库的存在.除了有状态,丑陋和容易出错之外,它还将格式与流相结合.
假设您要打印出一行,其中包含8位零填充十六进制无符号整数,后跟一个空格,后跟一个带有3位小数的双精度数.有了<cstdio>,您可以阅读简洁的格式字符串.使用时<ostream>,您必须保存旧状态,将对齐设置为右,设置填充字符,设置填充宽度,将基数设置为十六进制,输出整数,恢复保存状态(否则您的整数格式将污染您的浮动格式),输出空间,将表示法设置为固定,设置精度,输出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);
Run Code Online (Sandbox Code Playgroud)
<iostream> 是如何不使用运算符重载的典型代表:
std::cout << 2 << 3 && 0 << 5;
Run Code Online (Sandbox Code Playgroud)
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
}
Run Code Online (Sandbox Code Playgroud)
对这个例子的反驳是,大多数人都会遵守规则,从而永远不会从多个线程写入单个文件描述符.那么,在这种情况下,你必须观察到,<iostream>会帮忙,抓住每一个锁<<和每一个>>.而在<cstdio>,你不会经常锁定,你甚至可以选择不锁定.
<iostream> 花费更多的锁来实现不太一致的结果.
Pha*_*rap 18
除了所有其他答案所说的,
还有一个事实std::endl是不一样的'\n'.
这是一个不幸的常见误解.std::endl并不意味着"新行",
它意味着"打印新行然后刷新流 ".法拉盛并不便宜!
完全无视之间的差异printf和std::cout了一会儿,在功能上eqvuialent到C例如,你的C++例子应该是这样的:
#include <iostream>
int main()
{
std::cout << "Hello world\n";
return 0;
}
Run Code Online (Sandbox Code Playgroud)
这里有一个示例,如果您包含刷新,您的示例应该是什么样的.
C
#include <stdio.h>
int main()
{
printf("Hello world\n");
fflush(stdout);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
C++
#include <iostream>
int main()
{
std::cout << "Hello world\n";
std::cout << std::flush;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
在比较代码时,您应该始终小心,您要比较喜欢,并了解您的代码正在做什么的含义.有时即使是最简单的例子也比有些人意识到的要复杂得多.
The*_*kis 16
虽然现有的技术答案是正确的,但我认为这个问题最终源于这种误解:
众所周知,在C++中,你需要为你所吃的东西买单.
这只是来自C++社区的营销演讲.(公平地说,每个语言社区都有营销演讲.)这并不意味着你可以认真依赖的任何具体内容.
"你支付你使用的费用"应该意味着如果你使用该功能,C++功能只会有开销.但是"特征"的定义并非无限细节.通常,您最终会激活具有多个方面的功能,即使您只需要这些方面的子集,但实现部分地使用该功能通常也不实际或不可能.
一般来说,许多(尽管可能不是全部)语言都努力提高效率,并取得了不同程度的成功.C++在某种程度上是一个规模,但它的设计没有任何特殊或神奇的东西可以让它在这个目标中取得完美的成功.
Bat*_*eba 12
C++中的输入/输出函数编写得很优雅,设计使它们易于使用.在许多方面,它们是C++中面向对象特性的展示.
但是你确实放弃了一些性能,但与操作系统处理较低级别的功能所花费的时间相比,这可以忽略不计.
您可以随时回退到C风格函数,因为它们是C++标准的一部分,或者可能完全放弃可移植性并使用对操作系统的直接调用.
| 归档时间: |
|
| 查看次数: |
24290 次 |
| 最近记录: |