In C++, am I paying for what I am not eating?
Your listings are indeed comparing apples and oranges, but not for the reason implied in most other answers.
Let’s check what your code actually does:
C:
- print a single string,
"Hello world\n"
C++:
- stream the string
"Hello world"
intostd::cout
- stream the
std::endl
manipulator intostd::cout
Apparently your C++ code is doing twice as much work. For a fair comparison we should combine this:
#include <iostream>
int main()
{
std::cout<<"Hello world\n";
return 0;
}
… and suddenly your assembly code for main
looks very similar to C’s:
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
In fact, we can compare the C and C++ code line by line, and there are very few differences:
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
The only real difference is that in C++ we call operator <<
with two arguments (std::cout
and the string). We could remove even that slight difference by using a closer C eqivalent: fprintf
, which also has a first argument specifying the stream.
This leaves the assembly code for _GLOBAL__sub_I_main
, which is generated for C++ but not C. This is the only true overhead that’s visible in this assembly listing (there’s more, invisible overhead for both languages, of course). This code performs a one-time setup of some C++ standard library functions at the start of the C++ program.
But, as explained in other answers, the relevant difference between these two programs won’t be found in the assembly output of the main
function since all the heavy lifting happens behind the scenes.
You are not comparing C and C++. You are comparing printf
and std::cout
, which are capable of different things (locales, stateful formatting, etc).
Try to use the following code for comparison. Godbolt generates the same assembly for both files (tested with 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);
}
}
So, in this case, what am I paying for?
std::cout
is more powerful and complicated than printf
. It supports things like locales, stateful formatting flags, and more.
If you don't need those, use std::printf
or std::puts
- they're available in <cstdio>
.
It is famous that in C++ you pay for what you eat.
I also want to make it clear that C++ != The C++ Standard Library. The Standard Library is supposed to be general-purpose and "fast enough", but it will often be slower than a specialized implementation of what you need.
On the other hand, the C++ language strives to make it possible to write code without paying unnecessary extra hidden costs (e.g. opt-in virtual
, no garbage collection).
What you are paying for is to call a heavy library (not as heavy as printing into console). You initialize an ostream
object. There are some hidden storage. Then, you call std::endl
which is not a synonym for \n
. The iostream
library helps you adjusting many settings and putting the burden on the processor rather than the programmer. This is what you are paying for.
Let's review the code:
.LC0:
.string "Hello world"
main:
Initializing an ostream object + 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)
Calling cout
again to print a new line and flush
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
Static storage initialization:
_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
Also, it is essential to distinguish between the language and the library.
BTW, this is just a part of the story. You do not know what is written in the functions you are calling.