Vamos considerar os seguintes exemplos de olá mundo em C e C ++:
#include <stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
#include <iostream>
int main()
{
std::cout<<"Hello world"<<std::endl;
return 0;
}
Quando eu os compilo em godbolt para montagem, o tamanho do código C é de apenas 9 linhas ( gcc -O3
):
.LC0:
.string "Hello world"
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
xor eax, eax
add rsp, 8
ret
Mas o tamanho do código C ++ é 22 linhas ( 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
... que é muito maior.
É famoso que em C ++ você paga pelo que come. Então, neste caso, pelo que estou pagando?
eat
associado ao C ++. Eu acredito que você quer dizer: "Você paga apenas pelo que usa "?eat
é mais ambíguo e deve ser evitado.Respostas:
O que você está pagando é chamar uma biblioteca pesada (não tão pesada quanto imprimir no console). Você inicializa um
ostream
objeto. Há algum armazenamento oculto. Então, você chama ostd::endl
que não é sinônimo\n
. Aiostream
biblioteca ajuda a ajustar muitas configurações e sobrecarregar o processador, e não o programador. É por isso que você está pagando.Vamos revisar o código:
Inicializando um objeto ostream + cout
Ligando
cout
novamente para imprimir uma nova linha e liberarInicialização estática de armazenamento:
Além disso, é essencial distinguir entre o idioma e a biblioteca.
BTW, isso é apenas uma parte da história. Você não sabe o que está escrito nas funções que está chamando.
fonte
cout; printf; cout
gravações estejam em ordem (uma vez que eles têm seus próprios buffers). O segundo será dessincronizadocout
ecin
,cout; cin
potencialmente, solicitando informações ao usuário primeiro. A descarga forçará a sincronização apenas quando você realmente precisar.std::cout
é mais poderoso e complicado do queprintf
. Ele suporta coisas como localidades, sinalizadores de formatação com estado e muito mais.Se você não precisar deles, use
std::printf
oustd::puts
- eles estão disponíveis no<cstdio>
.Também quero deixar claro que C ++ ! = The C ++ Standard Library. A Biblioteca Padrão deve ser de uso geral e "rápida o suficiente", mas geralmente será mais lenta que uma implementação especializada do que você precisa.
Por outro lado, a linguagem C ++ se esforça para tornar possível escrever código sem pagar custos ocultos extras desnecessários (por exemplo, opt-in
virtual
, sem coleta de lixo).fonte
Você não está comparando C e C ++. Você está comparando
printf
estd::cout
capaz de diferentes coisas (localidades, formatação com estado, etc.).Tente usar o seguinte código para comparação. Godbolt gera o mesmo conjunto para os dois arquivos (testado com gcc 8.2, -O3).
main.c:
main.cpp:
fonte
Suas listagens estão realmente comparando maçãs e laranjas, mas não pelo motivo implícito na maioria das outras respostas.
Vamos verificar o que seu código realmente faz:
C:
"Hello world\n"
C ++:
"Hello world"
parastd::cout
std::endl
manipulador parastd::cout
Aparentemente, seu código C ++ está fazendo o dobro do trabalho. Para uma comparação justa, devemos combinar isso:
… E de repente seu código de montagem
main
parece muito semelhante ao C:De fato, podemos comparar o código C e C ++ linha por linha, e há muito poucas diferenças :
A única diferença real é que, em C ++, chamamos
operator <<
com dois argumentos (std::cout
e a string). Poderíamos remover até mesmo essa pequena diferença usando um C eqivalent: mais próximofprintf
, que também possui um primeiro argumento especificando o fluxo.Isso deixa o código de montagem para
_GLOBAL__sub_I_main
, que é gerado para C ++, mas não para C. Essa é a única sobrecarga verdadeira visível nesta lista de montagens (há mais, sobrecarga invisível para ambos idiomas, é claro). Esse código executa uma instalação única de algumas funções da biblioteca padrão do C ++ no início do programa C ++.Mas, como explicado em outras respostas, a diferença relevante entre esses dois programas não será encontrada na saída de montagem da
main
função, pois todo o trabalho pesado acontece nos bastidores.fonte
_start
mas seu código faz parte da biblioteca de tempo de execução C. De qualquer forma, isso acontece para C e C ++.std::cout
e em vez disso passa I / O para a implementação stdio (que utiliza os seus próprios mecanismos de tamponamento). Em particular, quando conectado a (o que se sabe ser) um terminal interativo, por padrão, você nunca verá uma saída totalmente em buffer ao gravar emstd::cout
. É necessário desabilitar explicitamente a sincronização com o stdio se desejar que a biblioteca iostream use seus próprios mecanismos de bufferstd::cout
.printf
não precisa liberar os fluxos aqui. De fato, em um caso de uso comum (saída redirecionada para arquivo), geralmente você encontrará essaprintf
instrução não liberada. Somente quando a saída é com buffer de linha ou sem buffer, oprintf
gatilho é liberado.Isso é simples. Você paga
std::cout
. "Você paga apenas pelo que come" não significa "você sempre obtém os melhores preços". Claro,printf
é mais barato. Pode-se argumentar questd::cout
é mais seguro e versátil, portanto, seu maior custo é justificado (custa mais, mas agrega mais valor), mas isso não leva em consideração. Você não usaprintf
, usastd::cout
, então paga pelo usostd::cout
. Você não paga pelo usoprintf
.Um bom exemplo são as funções virtuais. As funções virtuais têm alguns requisitos de custo e espaço em tempo de execução - mas apenas se você realmente as usar. Se você não usa funções virtuais, não paga nada.
Algumas observações
Mesmo se o código C ++ for avaliado para obter mais instruções de montagem, ainda há algumas instruções e qualquer sobrecarga de desempenho provavelmente ainda será diminuída pelas operações de E / S reais.
Na verdade, às vezes é ainda melhor do que "em C ++ você paga pelo que come". Por exemplo, o compilador pode deduzir que a chamada de função virtual não é necessária em algumas circunstâncias e transformá-la em chamada não virtual. Isso significa que você pode obter funções virtuais gratuitamente . Isso não é ótimo?
fonte
A "lista de montagem para printf" NÃO é para printf, mas para puts (tipo de otimização do compilador?); printf é muito mais complexo do que coloca ... não se esqueça!
fonte
std::cout
as partes internas da casa, que não são visíveis na lista de montagem.puts
, que parece idêntica a uma chamadaprintf
se você passar apenas uma sequência de formato único e zero args extras. (exceto que também haverá umxor %eax,%eax
porque estamos passando zero argumentos de FP nos registros para uma função variável). Nenhuma dessas implementações é a implementação, apenas passando um ponteiro para uma string para a função de biblioteca. Mas sim, otimizandoprintf
aputs
é algo gcc faz para formatos que só têm"%s"
, ou quando não há conversões, e as extremidades da corda com uma nova linha.Vejo algumas respostas válidas aqui, mas vou detalhar um pouco mais.
Vá para o resumo abaixo para obter a resposta para sua pergunta principal, se você não quiser passar por toda essa parede de texto.
Abstração
Você está pagando pela abstração . Ser capaz de escrever códigos mais simples e mais amigáveis ao homem tem um custo. No C ++, que é uma linguagem orientada a objetos, quase tudo é um objeto. Quando você usa qualquer objeto, três coisas principais sempre acontecem sob o capô:
init()
método). Normalmente, a alocação de memória acontece sob o capô como a primeira coisa nesta etapa.Você não o vê no código, mas toda vez que você usa um objeto, todas as três coisas acima precisam acontecer de alguma forma. Se você fizesse tudo manualmente, o código seria obviamente muito mais longo.
Agora, a abstração pode ser feita de maneira eficiente, sem acrescentar sobrecarga: o inlining de método e outras técnicas podem ser usadas pelos compiladores e programadores para remover as sobrecargas da abstração, mas esse não é o seu caso.
O que realmente está acontecendo em C ++?
Aqui está, dividido:
std::ios_base
classe é inicializada, que é a classe base para tudo relacionado a E / S.std::cout
objeto é inicializado.std::__ostream_insert
, que (como você já descobriu pelo nome) é um métodostd::cout
(basicamente o<<
operador) que adiciona uma string ao fluxo.cout::endl
também é passado parastd::__ostream_insert
.__std_dso_handle
é passado para__cxa_atexit
, que é uma função global responsável pela "limpeza" antes de sair do programa.__std_dso_handle
ele próprio é chamado por essa função para desalocar e destruir os objetos globais restantes.Então, usando C == não está pagando por nada?
No código C, muito poucas etapas estão acontecendo:
puts
através doedi
registro.puts
é chamado.Nenhum objeto em qualquer lugar, portanto, não há necessidade de inicializar / destruir nada.
Este, porém, não significa que você não está "pagando" para qualquer coisa em C . Você ainda está pagando pela abstração, e também pela inicialização da biblioteca padrão C e da resolução dinâmica. A
printf
função (ou, na verdadeputs
, que é otimizada pelo compilador, pois você não precisa de nenhuma sequência de formatação) ainda acontece sob o capô.Se você escrevesse este programa em assembly puro, seria algo como isto:
O que basicamente resulta apenas em invocar o
write
syscall seguido peloexit
syscall. Agora, esse seria o mínimo necessário para realizar a mesma coisa.Resumir
C é bem mais simples , e apenas o mínimo necessário, deixando total controle ao usuário, que é capaz de otimizar e personalizar totalmente basicamente o que quiser. Você diz ao processador para carregar uma string em um registro e, em seguida, chama uma função de biblioteca para usar essa string. C ++, por outro lado, é muito mais complexo e abstrato . Isso tem uma enorme vantagem ao escrever códigos complicados e permite a criação de códigos mais fáceis e mais amigáveis ao ser humano, mas obviamente tem um custo. Sempre haverá uma desvantagem no desempenho do C ++ se comparado ao C em casos como esse, pois o C ++ oferece mais do que o necessário para realizar essas tarefas básicas e, portanto, acrescenta mais sobrecarga .
Respondendo à sua pergunta principal :
Nesse caso específico, sim . Você não está tirando proveito de nada que o C ++ tenha a oferecer mais que o C ++, mas isso é apenas porque não há nada nesse trecho de código simples com o qual o C ++ possa ajudá-lo: é tão simples que você realmente não precisa do C ++.
Ah, e só mais uma coisa!
As vantagens do C ++ podem não parecer óbvias à primeira vista, pois você escreveu um programa muito simples e pequeno, mas observe um exemplo um pouco mais complexo e veja a diferença (os dois programas fazem exatamente a mesma coisa):
C :
C ++ :
Espero que você possa ver claramente o que quero dizer aqui. Observe também como em C você precisa gerenciar a memória em um nível mais baixo usando
malloc
efree
como precisa ser mais cuidadoso com a indexação e os tamanhos e como precisa ser muito específico ao receber entradas e impressões.fonte
Existem alguns conceitos errados para começar. Primeiro, o programa C ++ não resulta em 22 instruções, é mais como 22.000 delas (tirei esse número do meu chapéu, mas está aproximadamente no estádio). Além disso, o código C também não resulta em 9 instruções. Esses são apenas os que você vê.
O que o código C faz é que, depois de fazer muitas coisas que você não vê, ele chama uma função do CRT (que geralmente está presente, mas não necessariamente está presente como lib compartilhada), depois não verifica o valor de retorno ou o manipulador erros e falhas. Dependendo das configurações do compilador e da otimização, ele nem chama ,
printf
mas éputs
algo ainda mais primitivo.Você poderia ter escrito mais ou menos o mesmo programa (exceto algumas funções invis invisíveis) em C ++, se você chamasse a mesma função da mesma maneira. Ou, se você deseja ser super-correto, essa mesma função é prefixada com
std::
.O código C ++ correspondente, na realidade, não é a mesma coisa. Embora tudo
<iostream>
isso seja conhecido por ser um porco feio e gordo que acrescenta uma imensa sobrecarga para pequenos programas (em um programa "real" você nem percebe muito)), uma interpretação um pouco mais justa é que ele faz uma terrível muitas coisas que você não vê e que simplesmente funcionam . Incluindo, mas não limitado a, formatação mágica de praticamente qualquer material aleatório, incluindo diferentes formatos e localizações de números e outros enfeites, armazenamento em buffer e tratamento de erros adequado. Manipulação de erros? Bem, sim, adivinhe, a saída de uma string pode realmente falhar e, ao contrário do programa C, o programa C ++ não ignoraria isso silenciosamente. Considerando o questd::ostream
sob o capô, e sem que ninguém perceba, é realmente muito leve. Não é como se estivesse usando, porque odeio a sintaxe do fluxo com paixão. Mas ainda assim, é incrível se você considerar o que faz.Mas, com certeza, o C ++ em geral não é tão eficiente quanto C pode ser. Não pode ser tão eficiente, pois não é a mesma coisa e não está fazendo a mesma coisa. Se nada mais, o C ++ gera exceções (e código para gerar, manipular ou falhar nelas) e fornece algumas garantias que o C não fornece. Então, com certeza, um programa em C ++ precisa necessariamente ser um pouco maior. No geral, no entanto, isso não importa de forma alguma. Pelo contrário, para programas reais , raramente achei o C ++ com melhor desempenho porque, por um motivo ou outro, parece emprestar otimizações mais favoráveis. Não me pergunte por que, em particular, eu não saberia.
Se, em vez de despertar e esquecer a esperança de obter o melhor, você deseja escrever o código C correto (ou seja, na verdade, você verifica erros e o programa se comporta corretamente na presença de erros), a diferença é marginal, se existir.
fonte
std::cout
lança exceções?std::cout
é umstd::basic_ostream
e que se pode lançar e pode repetir exceções que ocorreram de outra forma, se configurado para fazer isso, ou pode engolir exceções. O problema é que as coisas podem falhar, e o C ++, assim como a biblioteca padrão do C ++, é (principalmente) criada para que as falhas não passem facilmente despercebidas. Isso é um aborrecimento e uma bênção (mas, mais bênção do que aborrecimento). C, por outro lado, apenas mostra o dedo do meio. Você não verifica um código de retorno, nunca sabe o que aconteceu.Você está pagando por um erro. Nos anos 80, quando os compiladores não eram bons o suficiente para verificar as seqüências de formato, a sobrecarga do operador era vista como uma boa maneira de impor alguma aparência de segurança de tipo durante o io. No entanto, todos os seus recursos de banner são implementados mal ou conceitualmente falidos desde o início:
<iomanip>
A parte mais repugnante da API do fluxo C ++ io é a existência dessa biblioteca de cabeçalhos de formatação. Além de ser stateful, feio e propenso a erros, ele formata o fluxo.
Suponha que você queira imprimir uma linha com 8 dígitos int não assinados, preenchidos com zero, seguidos de um espaço seguido de um duplo com 3 casas decimais. Com
<cstdio>
, você começa a ler uma string de formato conciso. Com<ostream>
, você deve salvar o estado antigo, definir o alinhamento para a direita, definir o caractere de preenchimento, definir a largura de preenchimento, definir a base como hexadecimal, gerar o número inteiro, restaurar o estado salvo (caso contrário, sua formatação inteira poluirá a formatação do flutuador), produzirá o espaço , defina a notação como fixa, defina a precisão, produza a linha dupla e a nova linha e restaure a formatação antiga.Sobrecarga do operador
<iostream>
é o filho do pôster de como não usar sobrecarga de operador:atuação
std::cout
é várias vezes mais lentoprintf()
. A façanha desenfreada e a expedição virtual cobram seu preço.Segurança da linha
Ambos
<cstdio>
e<iostream>
são thread-safe, pois cada chamada de função é atômica. Mas,printf()
faz muito mais por chamada. Se você executar o seguinte programa com a<cstdio>
opção, verá apenas uma linha def
. Se você usar<iostream>
em uma máquina multicore, provavelmente verá outra coisa.A réplica deste exemplo é que a maioria das pessoas exerce disciplina para nunca gravar em um único descritor de arquivo a partir de vários encadeamentos. Bem, nesse caso, você terá que observar que
<iostream>
será útil trancar uma fechadura em todos<<
e cada um>>
. Considerando que<cstdio>
, você não estará bloqueando com tanta frequência e você ainda tem a opção de não bloquear.<iostream>
gasta mais bloqueios para obter um resultado menos consistente.fonte
std::cout
É várias vezes mais lentoprintf()
" - essa afirmação é repetida em toda a rede, mas não é verdade há séculos. As implementações modernas do IOstream têm desempenho semelhanteprintf
. O último também realiza o despacho virtual internamente para lidar com fluxos em buffer e E / S localizadas (executadas pelo sistema operacional, mas, no entanto).printf
ecout
diminui. Aliás, existem muitos desses benchmarks neste site.Além do que todas as outras respostas disseram,
também há o fato de que não
std::endl
é o mesmo .'\n'
Infelizmente, este é um equívoco comum.
std::endl
não significa "nova linha",significa "imprimir nova linha e liberar o fluxo ". Flushing não é barato!
Ignorando completamente as diferenças entre
printf
estd::cout
por um momento, para ser funcionalmente equivalente ao seu exemplo de C, seu exemplo de C ++ deve ficar assim:E aqui está um exemplo de como devem ser seus exemplos se você incluir a descarga.
C
C ++
Ao comparar código, você deve sempre ter cuidado para comparar comparações semelhantes e entender as implicações do que seu código está fazendo. Às vezes, mesmo os exemplos mais simples são mais complicados do que algumas pessoas imaginam.
fonte
std::endl
é o equivalente funcional para escrever uma nova linha em um fluxo stdio com buffer de linha.stdout
, em particular, é necessário que seja buffer de linha ou sem buffer quando conectado a um dispositivo interativo. Acredito que o Linux insiste na opção com buffer de linha.std::endl
para gerar novas linhas.setvbuf(3)
? Ou você quer dizer que o padrão é buffer de linha? FYI: Normalmente, todos os arquivos são armazenados em bloco. Se um fluxo se refere a um terminal (como stdout normalmente faz), ele é buffer de linha. O fluxo de erros padrão stderr é sempre sem buffer por padrão.printf
liberado automaticamente ao encontrar um caractere de nova linha?Embora as respostas técnicas existentes estejam corretas, acho que a pergunta deriva finalmente desse equívoco:
Esta é apenas uma palestra de marketing da comunidade C ++. (Para ser justo, há discussões sobre marketing em todas as comunidades de idiomas.) Isso não significa nada concreto do qual você possa confiar seriamente.
"Você paga pelo que usa" deve significar que um recurso C ++ só terá sobrecarga se você estiver usando esse recurso. Mas a definição de "um recurso" não é infinitamente granular. Freqüentemente, você acaba ativando recursos que possuem vários aspectos e, embora precise apenas de um subconjunto desses aspectos, geralmente não é prático ou possível para a implementação trazer o recurso parcialmente.
Em geral, muitos idiomas (embora provavelmente não todos) se esforçam para serem eficientes, com graus variados de sucesso. O C ++ está em algum lugar na escala, mas não há nada de especial ou mágico em seu design que permita que ele seja perfeitamente bem-sucedido nesse objetivo.
fonte
<cstdio>
e não incluir<iostream>
, assim como pode compilar-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
.As funções de entrada / saída em C ++ são escritas com elegância e são projetadas para serem simples de usar. Em muitos aspectos, eles são uma vitrine para os recursos orientados a objetos em C ++.
Mas, na verdade, você renuncia um pouco do desempenho, mas isso é insignificante em comparação com o tempo gasto pelo seu sistema operacional para lidar com as funções em um nível mais baixo.
Você sempre pode voltar para as funções no estilo C, pois elas fazem parte do padrão C ++, ou talvez renuncie completamente à portabilidade e use chamadas diretas para o seu sistema operacional.
fonte
std::basic_*stream
baixo) conhece os problemas de entrada. Eles foram projetados para serem amplamente gerais e estendidos por herança; mas ninguém acabou fazendo isso, por causa de sua complexidade (há literalmente livros escritos em iostreams), tanto que novas bibliotecas nasceram justamente para isso (por exemplo, boost, ICU etc.). Duvido que um dia paremos de pagar por esse erro.Como você viu em outras respostas, você paga quando vincula bibliotecas gerais e chama construtores complexos. Não há nenhuma pergunta em particular aqui, mais uma queixa. Vou apontar alguns aspectos do mundo real:
Barne tinha um princípio básico de design para nunca deixar a eficiência ser um motivo para permanecer em C, e não em C ++. Dito isto, é preciso ter cuidado para obter essas eficiências, e há eficiências ocasionais que sempre funcionaram, mas não foram 'tecnicamente' dentro da especificação C. Por exemplo, o layout dos campos de bits não foi realmente especificado.
Tente olhar através do ostream. Oh meu deus está inchado! Eu não ficaria surpreso ao encontrar um simulador de vôo lá. Até o printf () do stdlib geralmente roda cerca de 50K. Esses não são programadores preguiçosos: metade do tamanho da impressão estava relacionada a argumentos de precisão indiretos que a maioria das pessoas nunca usa. Quase todas as bibliotecas de processadores realmente restritos criam seu próprio código de saída em vez de printf.
O aumento no tamanho geralmente fornece uma experiência mais contida e flexível. Como analogia, uma máquina de venda automática venderá uma xícara de café como substância por algumas moedas e toda a transação leva menos de um minuto. Entrar em um bom restaurante envolve a colocação de uma mesa, sentar, pedir, esperar, tomar uma boa xícara, receber uma fatura, pagar em suas formas, adicionar uma dica e desejar um bom dia ao sair. É uma experiência diferente e mais conveniente se você estiver acompanhando os amigos para uma refeição complexa.
As pessoas ainda escrevem ANSI C, embora raramente K&R C. Minha experiência é que sempre o compilamos com um compilador C ++ usando alguns ajustes na configuração para limitar o que é arrastado. Existem bons argumentos para outros idiomas: Go remove a sobrecarga polimórfica e o pré-processador maluco ; existem alguns bons argumentos para empacotamento de campo e layout de memória mais inteligentes. IMHO Eu acho que qualquer design de linguagem deve começar com uma lista de objetivos, como o Zen do Python .
Tem sido uma discussão divertida. Você pergunta por que não pode ter bibliotecas magicamente pequenas, simples, elegantes, completas e flexíveis?
Não há resposta. Não haverá resposta. Essa é a resposta.
fonte