Давайте рассмотрим следующие примеры hello world на C и C ++:
#include <stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
#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 ++ вы платите за то, что едите. В таком случае, за что я плачу?
eat
связанный с C ++. Я полагаю, вы имеете в виду: «Вы платите только за то, что вы используете »?eat
более двусмысленный и его следует избегать.Ответы:
Вы платите за то, чтобы вызывать тяжелую библиотеку (не такую тяжелую, как печать в консоли). Вы инициализируете
ostream
объект. Есть несколько скрытых хранилищ. Затем вы звоните,std::endl
который не является синонимом для\n
.iostream
Библиотека позволяет регулировать множество параметров и положить нагрузку на процессор , а не программиста. Это то, за что вы платите.Давайте рассмотрим код:
Инициализация объекта ostream + cout
Повторный вызов
cout
для печати новой строки и сбросаИнициализация статического хранилища:
Кроме того, важно различать язык и библиотеку.
Кстати, это только часть истории. Вы не знаете, что написано в функциях, которые вы вызываете.
источник
cout; printf; cout
правильности записи (поскольку они имеют свои собственные буферы). Второй будет рассинхронизированcout
иcin
, в результате,cout; cin
потенциально будет запрашивать у пользователя информацию в первую очередь. Промывка заставит синхронизироваться только тогда, когда вам это действительно нужно.std::cout
более мощный и сложный, чемprintf
. Он поддерживает такие вещи, как локали, флаги форматирования с сохранением состояния и многое другое.Если они вам не нужны, используйте
std::printf
илиstd::puts
- они доступны в<cstdio>
.Я также хочу прояснить, что C ++ ! = Стандартная библиотека C ++. Стандартная библиотека должна быть универсальной и «достаточно быстрой», но часто она будет медленнее, чем специализированная реализация того, что вам нужно.
С другой стороны, язык C ++ стремится сделать возможным написание кода без дополнительных ненужных скрытых затрат (например, подписка
virtual
, сбор мусора).источник
Вы не сравниваете C и C ++. Вы сравниваете
printf
иstd::cout
, на которые способны разные вещи (локали, форматирование с учетом состояния и т. Д.).Попробуйте использовать следующий код для сравнения. Godbolt генерирует одинаковую сборку для обоих файлов (протестировано с gcc 8.2, -O3).
main.c:
main.cpp:
источник
Ваши списки действительно сравнивают яблоки и апельсины, но не по той причине, которая указана в большинстве других ответов.
Давайте проверим, что на самом деле делает ваш код:
C:
"Hello world\n"
C ++:
"Hello world"
вstd::cout
std::endl
манипулятора вstd::cout
Очевидно, ваш код C ++ выполняет вдвое больше работы. Для честного сравнения мы должны объединить это:
… И вдруг ваш ассемблерный код
main
выглядит очень похожим на C:Фактически, мы можем сравнивать код C и C ++ построчно, и различий очень мало :
Единственная реальная разница в том, что в C ++ мы вызываем
operator <<
с двумя аргументами (std::cout
и строкой). Мы могли бы устранить даже эту небольшую разницу, используя более близкий C eqivalent:,fprintf
который также имеет первый аргумент, указывающий поток.Это оставляет код сборки для
_GLOBAL__sub_I_main
, который генерируется для C ++, но не для C. Это единственные истинные издержки, которые видны в этом листинге сборки (есть больше, невидимые накладные расходы для обоих конечно, языков). Этот код выполняет однократную настройку некоторых функций стандартной библиотеки C ++ при запуске программы C ++.Но, как объясняется в других ответах, соответствующая разница между этими двумя программами не будет обнаружена в результатах сборки
main
функции, поскольку все тяжелые работы происходят за кулисами.источник
_start
но ее код является частью библиотеки времени выполнения C. В любом случае это происходит как для C, так и для C ++.std::cout
и вместо проходит I / O в STDIO реализации (который использует свои собственные механизмы буферизации). В частности, при подключении к (как известно, к) интерактивному терминалу, по умолчанию вы никогда не увидите полностью буферизованный вывод при записиstd::cout
. Вы должны явно отключить синхронизацию с stdio, если хотите, чтобы библиотека iostream использовала свои собственные механизмы буферизацииstd::cout
.printf
необходимости очищать потоки здесь. Фактически, в обычном случае использования (вывод перенаправлен в файл), вы обычно обнаружите, чтоprintf
оператор не сбрасывается. Только когда выход буферизован или небуферизован,printf
триггер будет сброшен.Это просто Вы платите за
std::cout
. «Вы платите только за то, что едите», не означает «вы всегда получаете лучшие цены». Конечно,printf
дешевле. Можно утверждать, чтоstd::cout
это безопаснее и универсальнее, поэтому его большая стоимость оправдана (она стоит дороже, но обеспечивает большую ценность), но это упускает из виду. Вы не используетеprintf
, вы используетеstd::cout
, поэтому вы платите за использованиеstd::cout
. Вы не платите за использованиеprintf
.Хороший пример - виртуальные функции. Виртуальные функции требуют определенных затрат времени и места, но только если вы их действительно используете. Если вы не используете виртуальные функции, вы ничего не платите.
Несколько замечаний
Даже если код C ++ оценивается в большее количество инструкций по сборке, он все равно является небольшим количеством инструкций, и любые накладные расходы по производительности, вероятно, будут уменьшены фактическими операциями ввода-вывода.
На самом деле, иногда это даже лучше, чем «в C ++ вы платите за то, что едите». Например, компилятор может сделать вывод, что вызов виртуальной функции не требуется в некоторых обстоятельствах, и преобразовать его в не виртуальный вызов. Это означает, что вы можете получить виртуальные функции бесплатно . Разве это не здорово?
источник
«Список сборок для printf» НЕ для printf, а для put (тип оптимизации компилятора?); printf гораздо сложнее, чем puts ... не забывайте!
источник
std::cout
внутренностях, которые не видны в списке сборки.puts
, который выглядит идентично вызову,printf
если вы передаете только одну строку формата и ноль лишних аргументов. (за исключением того, что также будетxor %eax,%eax
причина, потому что мы передаем нулевые аргументы FP в регистрах в функцию с переменным числом.) Ни одна из них не является реализацией, просто передачей указателя на строку в библиотечную функцию. Но да, оптимизацияprintf
дляputs
- это то, что gcc делает для форматов, которые имеют только"%s"
или когда нет преобразований, и строка заканчивается новой строкой.Я вижу некоторые правильные ответы здесь, но я собираюсь немного подробнее рассказать о них.
Перейдите к приведенной ниже сводке, чтобы найти ответ на свой главный вопрос, если вы не хотите проходить через всю эту стену текста.
абстракция
Вы платите за абстракцию . Возможность писать более простой и понятный для человека код обходится дорого. В C ++, который является объектно-ориентированным языком, почти все является объектом. Когда вы используете какой-либо предмет, под капотом всегда будут происходить три главных вещи:
init()
метода). Обычно выделение памяти происходит под капотом, как первое на этом этапе.Вы не видите этого в коде, но каждый раз, когда вы используете объект, все три вышеперечисленных вещи должны как-то происходить. Если бы вы делали все вручную, код, очевидно, был бы намного длиннее.
Теперь абстракция может быть сделана эффективно, без добавления накладных расходов: компиляция методов и другие методы могут использоваться как компиляторами, так и программистами для устранения издержек абстракции, но это не ваш случай.
Что на самом деле происходит в C ++?
Вот оно, сломано:
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 == ничего не платит?
В коде C происходит очень мало шагов:
puts
черезedi
реестр.puts
вызывается.Нет объектов нигде, следовательно, не нужно ничего инициализировать / уничтожать.
Это, однако, не означает, что вы ничего не «платите» в Си . Вы по-прежнему платите за абстракцию, а также за инициализацию стандартной библиотеки C и динамическое разрешение, при котором
printf
функция (или, собственноputs
, оптимизированная компилятором, поскольку вам не нужна строка формата) все еще происходит под капотом.Если бы вы написали эту программу в чистом виде, она бы выглядела примерно так:
Что в основном приводит только к
write
вызову системного вызова, за которым следуетexit
системный вызов. Теперь это был бы минимум для достижения того же.Подвести итоги
C гораздо более простой и выполняет только необходимый минимум, оставляя полный контроль над пользователем, который способен полностью оптимизировать и настраивать практически все, что он хочет. Вы говорите процессору загрузить строку в регистр, а затем вызываете библиотечную функцию для использования этой строки. С ++, с другой стороны, более сложный и абстрактный . Это имеет огромное преимущество при написании сложного кода и позволяет легче писать и более дружественный к человеку код, но это, очевидно, обходится дорого. В C ++ всегда будет недостаток производительности по сравнению с C в подобных случаях, поскольку C ++ предлагает больше, чем необходимо для выполнения таких базовых задач, и, следовательно, добавляет больше накладных расходов .
Отвечая на ваш главный вопрос :
В данном конкретном случае да . Вы не пользуетесь преимуществами того, что C ++ может предложить больше, чем C, но это только потому, что в этом простом куске кода нет ничего, что могло бы помочь вам в C ++: это настолько просто, что вам действительно не нужен C ++ вообще.
Ох, и еще одна вещь!
На первый взгляд преимущества C ++ могут показаться неочевидными, поскольку вы написали очень простую и небольшую программу, но посмотрите на более сложный пример и увидите разницу (обе программы делают одно и то же):
C :
C ++ :
Надеюсь, вы можете ясно увидеть, что я имею в виду здесь. Также обратите внимание на то, как в C вы должны управлять памятью на более низком уровне с помощью
malloc
иfree
как вам нужно быть более осторожным с индексированием и размерами, и как вы должны быть очень точными при вводе и печати.источник
Есть несколько заблуждений для начала. Во-первых, программа C ++ не приводит к 22 инструкциям, это больше похоже на 22 000 из них (я вытащил это число из своей шляпы, но оно примерно в поле). Кроме того, код C не дает 9 инструкций. Это только те, которые вы видите.
Что делает код C, так это то, что после выполнения многих вещей, которые вы не видите, он вызывает функцию из CRT (которая обычно, но не обязательно присутствует в качестве разделяемой библиотеки), а затем не проверяет возвращаемое значение или дескриптор. ошибки и выручает. В зависимости от компилятора и настроек оптимизации он даже не вызывается,
printf
ноputs
или что-то еще более примитивное.Вы могли бы написать более или менее ту же самую программу (за исключением некоторых невидимых функций инициализации) также на C ++, если бы только вы вызывали ту же самую функцию таким же образом. Или, если вы хотите быть супер-корректным, используйте ту же функцию с префиксом
std::
.Соответствующий код C ++ на самом деле совсем не одно и то же. В то время как все
<iostream>
это хорошо известно как толстый уродливый поросенок, который добавляет огромные накладные расходы для небольших программ (в «настоящей» программе вы не особо замечаете этого), несколько более справедливая интерпретация заключается в том, что это ужасно много вещей, которые вы не видите и которые просто работают . Включая, но не ограничиваясь этим, магическое форматирование практически любого случайного материала, включая различные числовые форматы и локали и тому подобное, буферизацию и надлежащую обработку ошибок. Обработка ошибок? Ну да, угадайте, что, вывод строки может действительно потерпеть неудачу, и в отличие от программы C, программа C ++ не будет игнорировать это молча. Учитывая чтоstd::ostream
делает под капотом, и никто не узнает, это на самом деле довольно легкий. Не то, чтобы я использовал это, потому что я ненавижу потоковый синтаксис со страстью. Но все же, это довольно круто, если учесть, что он делает.Но конечно, C ++ в целом не так эффективен, как C. Это не может быть столь же эффективным , поскольку это не то же самое , и это не делает то же самое. Если ничего другого, C ++ генерирует исключения (и код для их генерации, обработки или сбоя) и дает некоторые гарантии, которые C не дает. Так что, конечно, программа на C ++ должна быть немного больше. В целом, однако, это не имеет никакого значения. Напротив, для реальных программ я не редко обнаруживал, что C ++ работает лучше, потому что, по той или иной причине, он кажется более благоприятным для оптимизации. Не спрашивайте меня, почему, в частности, я бы не знал.
Если вместо того, чтобы «запустить и забыть надежду на лучшее», вы хотите написать правильный код C (т. Е. Вы на самом деле проверяете на наличие ошибок, и программа ведет себя правильно при наличии ошибок), тогда разница незначительна, если есть.
источник
std::cout
выкидывает исключения?std::cout
это то,std::basic_ostream
что можно выбросить, и он может перебрасывать возникающие в противном случае исключения, если настроен на это, или он может проглотить исключения. Дело в том, что вещи могут потерпеть неудачу, и C ++, а также стандартная библиотека C ++ (в основном) созданы, поэтому сбои не могут остаться незамеченными. Это раздражение и благословение (но скорее благословение, чем раздражение). С другой стороны, просто показывает средний палец. Вы не проверяете код возврата, вы никогда не знаете, что случилось.Вы платите за ошибку. В 80-х годах, когда компиляторы не были достаточно хороши для проверки строк форматирования, перегрузка операторов рассматривалась как хороший способ обеспечить некоторое подобие безопасности типов во время ввода-вывода. Тем не менее, каждая из его функций баннера с самого начала либо реализована плохо, либо концептуально банкротом:
<Iomanip>
Наиболее отвратительной частью потока C ++ io api является наличие этой библиотеки заголовков форматирования. Помимо сохранения состояния, уродства и ошибок, он объединяет форматирование с потоком.
Предположим, что вы хотите распечатать строку с 8-значным нулем, заполненным шестнадцатеричным int без знака, за которым следует пробел, за которым следует двойное число с 3 десятичными знаками. С помощью
<cstdio>
вы получите краткую строку формата. С помощью<ostream>
, вы должны сохранить старое состояние, установить выравнивание вправо, установить символ заливки, установить ширину заливки, установить основание на шестнадцатеричное, вывести целое число, восстановить сохраненное состояние (в противном случае ваше целочисленное форматирование будет загрязнять форматирование с плавающей запятой), вывести пробел установите фиксированную запись, задайте точность, выведите двойную и новую строки, а затем восстановите старое форматирование.Перегрузка оператора
<iostream>
Является ли плакат потомком того, как не использовать перегрузку операторов:Производительность
std::cout
в несколько раз медленнееprintf()
. Безудержный фурит и виртуальная отправка делают свое дело.Поток безопасности
Оба
<cstdio>
и<iostream>
потокобезопасны в том смысле, что каждый вызов функции является атомарным. Но,printf()
гораздо больше сделано за звонок. Если вы запустите следующую программу с<cstdio>
опцией, вы увидите только строкуf
. Если вы используете<iostream>
многоядерный компьютер, вы, скорее всего, увидите что-то еще.Ответ на этот пример заключается в том, что большинство людей применяют дисциплину, чтобы в любом случае никогда не записывать один дескриптор файла из нескольких потоков. Ну, в этом случае, вы должны будете заметить, что это
<iostream>
поможет захватить блокировку на всех<<
и каждого>>
. Принимая во внимание<cstdio>
, что вы не будете блокировать так часто, и у вас даже есть возможность не блокировать.<iostream>
расходует больше блокировок для достижения менее последовательного результата.источник
std::cout
несколько раз медленнееprintf()
» - это утверждение повторяется по всей сети, но оно не было верным в течение веков. Современные реализации IOstream работают наравне сprintf
. Последний также выполняет виртуальную диспетчеризацию для внутренних операций с буферизованными потоками и локализованным вводом-выводом (выполняется операционной системой, но тем не менее).printf
иcout
уменьшается. Кстати, на этом сайте есть множество таких тестов.В дополнение к тому, что сказали все остальные ответы,
есть еще тот факт, что
std::endl
это не то же самое, что'\n'
.Это, к сожалению, распространенное заблуждение.
std::endl
не означает «новая строка»,это означает «напечатать новую строку и затем очистить поток ». Промывка не дешевая!
Полностью игнорируя различия между
printf
иstd::cout
на мгновение, чтобы быть функционально эквивалентным вашему примеру C, ваш пример C ++ должен выглядеть следующим образом:А вот пример того, какими должны быть ваши примеры, если вы включите сброс.
С
C ++
Когда вы сравниваете код, вы всегда должны быть осторожны, чтобы сравнивать «лайки» и понимать смысл того, что делает ваш код. Иногда даже самые простые примеры сложнее, чем думают некоторые люди.
источник
std::endl
- это функциональный эквивалент записи новой строки в поток stdio с буферизацией строки.stdout
в частности, требуется, чтобы он был линейно-буферизован или не буферизован при подключении к интерактивному устройству. Я полагаю, что Linux настаивает на использовании линейного буфера.std::endl
вывод строки.setvbuf(3)
? Или вы хотите сказать, что по умолчанию используется линейная буферизация? К вашему сведению: обычно все файлы имеют блочную буферизацию. Если поток ссылается на терминал (как обычно делает stdout), он буферизуется в строке. Стандартный поток ошибок stderr по умолчанию всегда небуферизован.printf
мигает автоматически при встрече с символом новой строки?Хотя существующие технические ответы верны, я думаю, что вопрос в конечном счете вытекает из этого заблуждения:
Это просто маркетинговый разговор сообщества C ++. (Справедливости ради, в каждом языковом сообществе говорят о маркетинге.) Это не означает ничего конкретного, от чего можно серьезно зависеть.
«Вы платите за то, что используете», как предполагается, означает, что функция C ++ имеет накладные расходы, только если вы используете эту функцию. Но определение «особенность» не является бесконечно гранулированным. Часто вы заканчиваете тем, что активируете функции, которые имеют несколько аспектов, и даже если вам требуется только подмножество этих аспектов, реализация часто не позволяет реализовать функцию частично.
В целом, многие (хотя, возможно, не все) языки стремятся быть эффективными с разной степенью успеха. C ++ где-то в масштабе, но нет ничего особенного или волшебного в его дизайне, который позволил бы ему быть совершенно успешным в этой цели.
источник
<cstdio>
и не включать<iostream>
, так же, как вы можете скомпилировать-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
.Функции ввода / вывода в C ++ написаны элегантно и разработаны так, чтобы они были просты в использовании. Во многих отношениях они являются витриной для объектно-ориентированных функций в C ++.
Но вы действительно отдаете немного производительности взамен, но это ничтожно мало по сравнению со временем, затрачиваемым вашей операционной системой на обработку функций на более низком уровне.
Вы всегда можете прибегнуть к функциям стиля C, поскольку они являются частью стандарта C ++, или, возможно, вообще отказаться от переносимости и использовать прямые вызовы вашей операционной системы.
источник
std::basic_*stream
нисходящему), знает входящие eadaches. Они были разработаны, чтобы быть широко общими и распространяться через наследование; но никто в конце концов не сделал этого из-за их сложности (буквально на iostreams написаны книги), настолько, что новые библиотеки были созданы именно для этого (например, boost, ICU и т. д.). Я сомневаюсь, что мы когда-нибудь перестанем платить за эту ошибку.Как вы видели в других ответах, вы платите, когда ссылаетесь в общие библиотеки и вызываете сложные конструкторы. Здесь нет никакого особого вопроса, больше жалобы. Я укажу некоторые реальные аспекты:
У Барна был основной принцип проектирования, который никогда не позволял эффективности быть причиной оставаться в C, а не в C ++. Тем не менее, нужно быть осторожным, чтобы получить эти эффективности, и есть случайные эффективности, которые всегда работали, но не были «технически» в спецификации C. Например, расположение битовых полей на самом деле не было указано.
Попробуйте посмотреть через ostream. Боже мой, это раздутый! Я не удивлюсь, если найду симулятор полета там. Даже stdlib printf () обычно работает около 50K. Это не ленивые программисты: половина размера printf была связана с косвенными аргументами точности, которые большинство людей никогда не используют. Почти каждая действительно ограниченная библиотека процессора создает свой собственный выходной код вместо printf.
Увеличение размера обычно обеспечивает более сдержанный и гибкий опыт. По аналогии, торговый автомат продаст чашку кофеоподобного вещества за несколько монет, и вся транзакция займет меньше минуты. Заходить в хороший ресторан - это накрывать на стол, сидеть, заказывать, ждать, получать хорошую чашку, получать счет, оплачивать выбор форм, добавлять чаевые и желать хорошего дня на выходе. Это другой опыт, и более удобный, если вы заходите с друзьями на сложную трапезу.
Люди все еще пишут ANSI C, хотя редко K & R C. Мой опыт показывает, что мы всегда компилируем его с помощью компилятора C ++, используя несколько настроек, чтобы ограничить то, что перетаскивается. Для других языков есть хорошие аргументы: Go удаляет полиморфные издержки и сумасшедший препроцессор ; было несколько хороших аргументов в пользу более разумной упаковки полей и размещения памяти. ИМХО Я думаю, что любой языковой дизайн должен начинаться с перечисления целей, во многом как Zen of Python .
Это была веселая дискуссия. Вы спрашиваете, почему у вас не может быть волшебно маленьких, простых, элегантных, полных и гибких библиотек?
Там нет ответа. Там не будет ответа. Это ответ.
источник