Em C ++, estou pagando pelo que não estou comendo?

170

Vamos considerar os seguintes exemplos de olá mundo em C e C ++:

main.c

#include <stdio.h>

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

main.cpp

#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?

Saher
fonte
3
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Samuel Liew
26
Nunca ouvi o termo eatassociado ao C ++. Eu acredito que você quer dizer: "Você paga apenas pelo que usa "?
Giacomo Alzetta 24/09
7
@GiacomoAlzetta, ... é um coloquialismo, aplicando o conceito de um buffet à discrição. Usar o termo mais preciso é certamente preferível para uma audiência global, mas como um falante nativo de inglês americano, o título faz sentido para mim.
Charles Duffy
5
@ trolley813 Os vazamentos de memória não têm nada a ver com a cotação e a pergunta do OP. O ponto "Você paga apenas pelo que usa" / "Você não paga pelo que não usa" é dizer que nenhuma ocorrência de desempenho será tomada se você não usar um recurso / abstração específico. Vazamentos de memória não têm nada a ver com isso, e isso mostra apenas que o termo eaté mais ambíguo e deve ser evitado.
Giacomo Alzetta

Respostas:

60

O que você está pagando é chamar uma biblioteca pesada (não tão pesada quanto imprimir no console). Você inicializa um ostreamobjeto. Há algum armazenamento oculto. Então, você chama o std::endlque não é sinônimo \n. A iostreambiblioteca 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:

.LC0:
        .string "Hello world"
main:

Inicializando um objeto 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)

Ligando coutnovamente para imprimir uma nova linha e liberar

    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

Inicialização estática de armazenamento:

_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

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.

Arash
fonte
5
Como observação adicional, um teste completo mostrará que é anexado um programa C ++ com "ios_base :: sync_with_stdio (false);" e "cin.tie (NULL);" tornará o cout mais rápido que o printf (Printf possui sobrecarga de seqüência de caracteres de formato). O primeiro elimina a sobrecarga de garantir que as cout; printf; coutgravações estejam em ordem (uma vez que eles têm seus próprios buffers). O segundo será dessincronizado coute cin, cout; cinpotencialmente, solicitando informações ao usuário primeiro. A descarga forçará a sincronização apenas quando você realmente precisar.
Nicholas Pipitone
Olá Nicholas, muito obrigado por adicionar essas notas úteis.
Arash
"é essencial distinguir entre o idioma e a biblioteca": Bem, sim, mas a biblioteca padrão que acompanha um idioma é a única disponível em todos os lugares, portanto é a que está sendo usada em todos os lugares (e sim, a biblioteca padrão C faz parte da especificação C ++, para que possa ser usado quando desejado). Quanto a "Você não sabe o que está escrito nas funções que está chamando": você pode vincular estaticamente se realmente quiser saber e, de fato, o código de chamada que você examina provavelmente é irrelevante.
Peter - Restabelece Monica
211

Então, neste caso, pelo que estou pagando?

std::couté mais poderoso e complicado do que printf. Ele suporta coisas como localidades, sinalizadores de formatação com estado e muito mais.

Se você não precisar deles, use std::printfou std::puts- eles estão disponíveis no <cstdio>.


É famoso que em C ++ você paga pelo que come.

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).

Vittorio Romeo
fonte
4
+1 por dizer que 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. Muitos parecem usar alegremente os componentes da STL sem considerar as implicações de desempenho em comparação com as suas.
Craig Estey
7
@Craig OTOH muitas partes da biblioteca padrão geralmente são mais rápidas e corretas do que o que normalmente se poderia produzir.
Peter - Restabelece Monica
2
@ PeterA.Schneider OTOH, quando a versão STL é 20x-30x mais lenta, rodar o seu próprio é uma coisa boa. Veja minha resposta aqui: codereview.stackexchange.com/questions/191747/… Nele, outros também sugeriram [pelo menos uma parcial] a sua.
Craig Estey
1
@CraigEstey Um vetor é (além da alocação dinâmica inicial que pode ser significativa, dependendo de quanto trabalho será feito eventualmente com uma determinada instância) não menos eficiente que um array C; Ele foi projetado para não ser. Deve-se tomar cuidado para não copiá-lo, reservar espaço suficiente inicialmente etc, mas tudo o que deve ser feito com uma matriz também e com menos segurança. Com relação ao seu exemplo vinculado: Sim, um vetor de vetores (a menos que seja otimizado) incorreria em um indireto extra comparado a uma matriz 2D, mas presumo que a eficiência 20x não esteja enraizada ali, mas no algoritmo.
Peter - Restabelece Monica
174

Você não está comparando C e C ++. Você está comparando printfe std::coutcapaz 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:

#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);
    }
}
pschill
fonte
Felicidades por mostrar código equivalente e explicar o motivo.
HackSlash
134

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:

  • imprima uma única sequência, "Hello world\n"

C ++:

  • transmitir a string "Hello world"parastd::cout
  • transmitir o std::endlmanipulador parastd::cout

Aparentemente, seu código C ++ está fazendo o dobro do trabalho. Para uma comparação justa, devemos combinar isso:

#include <iostream>

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

… E de repente seu código de montagem mainparece muito semelhante ao 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

De fato, podemos comparar o código C e C ++ linha por linha, e há muito poucas diferenças :

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

A única diferença real é que, em C ++, chamamos operator <<com dois argumentos ( std::coute a string). Poderíamos remover até mesmo essa pequena diferença usando um C eqivalent: mais próximo fprintf, 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 mainfunção, pois todo o trabalho pesado acontece nos bastidores.

Konrad Rudolph
fonte
21
Aliás, o tempo de execução C também precisa ser configurado, e isso acontece em uma função chamada, _startmas seu código faz parte da biblioteca de tempo de execução C. De qualquer forma, isso acontece para C e C ++.
21418 Konrad Rudolph
2
@Deduplicator: Na verdade, por padrão a biblioteca iostream não faz qualquer buffer de std::coute 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 em std::cout. É necessário desabilitar explicitamente a sincronização com o stdio se desejar que a biblioteca iostream use seus próprios mecanismos de buffer std::cout.
6
@ KonradRudolph: Na verdade, printfnão precisa liberar os fluxos aqui. De fato, em um caso de uso comum (saída redirecionada para arquivo), geralmente você encontrará essa printfinstrução não liberada. Somente quando a saída é com buffer de linha ou sem buffer, o printfgatilho é liberado.
2
@ PeterCordes: Certo, você não pode bloquear com buffers de saída não liberados, mas pode se surpreender com o fato de o programa ter aceitado sua entrada e ter prosseguido sem exibir a saída esperada. Eu sei disso porque tive a oportunidade de depurar uma "Ajuda, meu programa está travando durante a entrada, mas não consigo descobrir o porquê!" que deu outro desenvolvedor se encaixa por alguns dias.
2
@ PeterCordes: o argumento que faço é "escreva o que você quer dizer" - as novas linhas são apropriadas quando você quer que a saída esteja disponível, e endl é apropriado quando você quer que a saída fique disponível imediatamente.
53

É famoso que em C ++ você paga pelo que come. Então, neste caso, pelo que estou pagando?

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 que std::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 usa printf, usa std::cout, então paga pelo uso std::cout. Você não paga pelo uso printf.

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

  1. 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.

  2. 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?

el.pescado
fonte
6
Você não recebe funções virtuais de graça. Você ainda precisa pagar o custo de escrevê-los primeiro e depois depurar a transformação do código do compilador quando ele não corresponder à sua ideia do que deveria fazer.
alephzero
2
@alephzero Não tenho certeza se é particularmente relevante comparar custos de desenvolvimento com custos de desempenho.
Uma oportunidade tão grande para um trocadilho desperdiçado ... Você poderia ter usado a palavra 'calorias' em vez de 'preço'. A partir disso, você poderia dizer que o C ++ é mais gordo que o C. Ou pelo menos ... o código específico em questão (sou influenciado pelo C ++ em favor do C, portanto não posso ir além). Alas. @Bilkokuya Pode não ser relevante em todos os casos, mas certamente é algo que não se deve desconsiderar. Portanto, é relevante em geral.
Pryftan
46

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!

Álvaro Gustavo López
fonte
13
Esta é até agora a melhor resposta, já que todos os outros ficam pendurados em um arenque vermelho sobre std::coutas partes internas da casa, que não são visíveis na lista de montagem.
21418 Konrad Rudolph
12
A listagem de montagem é para uma chamada para puts , que parece idêntica a uma chamada printfse você passar apenas uma sequência de formato único e zero args extras. (exceto que também haverá um xor %eax,%eaxporque 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, otimizando printfa putsé 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.
22618 Peter Cordes
45

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

Então, neste caso, pelo que estou pagando?

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ô:

  1. Criação de objeto, basicamente alocação de memória para o próprio objeto e seus dados.
  2. Inicialização de objeto (geralmente através de algum init()método). Normalmente, a alocação de memória acontece sob o capô como a primeira coisa nesta etapa.
  3. Destruição de objetos (nem sempre).

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:

  1. A std::ios_baseclasse é inicializada, que é a classe base para tudo relacionado a E / S.
  2. O std::coutobjeto é inicializado.
  3. Sua string é carregada e passada para std::__ostream_insert, que (como você já descobriu pelo nome) é um método std::cout(basicamente o <<operador) que adiciona uma string ao fluxo.
  4. cout::endltambém é passado para std::__ostream_insert.
  5. __std_dso_handleé passado para __cxa_atexit, que é uma função global responsável pela "limpeza" antes de sair do programa. __std_dso_handleele 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:

  1. Sua string é carregada e passada putsatravés do ediregistro.
  2. 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 printffunção (ou, na verdade puts, 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:

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

O que basicamente resulta apenas em invocar o write syscall seguido pelo exitsyscall. 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 :

Estou pagando pelo que não estou comendo?

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 :

#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;
}

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;
}

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 malloce freecomo precisa ser mais cuidadoso com a indexação e os tamanhos e como precisa ser muito específico ao receber entradas e impressões.

Marco Bonelli
fonte
27

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 , printfmas é putsalgo 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::ostreamsob 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.

Damon
fonte
16
Resposta muito boa, exceto que esta afirmação: “Mas, em geral, C ++ não é tão eficiente quanto C pode ser” está simplesmente errado. C ++ pode ser tão eficiente quanto C e código de nível suficientemente alto pode ser mais eficiente que código C equivalente. Sim, o C ++ possui alguma sobrecarga por ter que lidar com exceções, mas nos compiladores modernos a sobrecarga é insignificante em comparação com os ganhos de desempenho de melhores abstrações sem custo.
21418 Konrad Rudolph
Se entendi corretamente, também std::coutlança exceções?
Saher 21/09/18
6
@ Saher: Sim, não, talvez. std::couté um std::basic_ostreame 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.
Damon
1
@KonradRudolph: É verdade, é isso que eu estava tentando apontar com "Eu 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" . Não é imediatamente óbvio o motivo, mas não raramente, apenas otimiza melhor. Por qualquer motivo. Você pensaria que é o mesmo para o otimizador, mas não é.
Damon
22

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.

// <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);

Sobrecarga do operador

<iostream> é o filho do pôster de como não usar sobrecarga de operador:

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

atuação

std::couté várias vezes mais lento printf(). 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 de f. Se você usar <iostream>em uma máquina multicore, provavelmente verá outra coisa.

// 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
}

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.

KevinZ
fonte
2
A maioria das implementações do printf possui um recurso extremamente útil para localização: parâmetros numerados. Se você precisar produzir alguma saída em dois idiomas diferentes (como inglês e francês) e a ordem das palavras for diferente, poderá usar o mesmo printf com uma sequência de formatação diferente e imprimir parâmetros em ordem diferente.
gnasher729
2
Essa formatação estável de fluxos deve ter causado tantos erros difíceis de encontrar que não sei o que dizer. Ótima resposta. Voto a favor mais de uma vez, se eu pudesse.
mathreadler
6
" std::coutÉ várias vezes mais lento printf()" - essa afirmação é repetida em toda a rede, mas não é verdade há séculos. As implementações modernas do IOstream têm desempenho semelhante printf. 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).
21918 Konrad Rudolph
3
@KevinZ E isso é ótimo, mas compara uma única chamada específica, que mostra os pontos fortes específicos do fmt (vários formatos diferentes em uma única string). No uso mais típico, a diferença entre printfe coutdiminui. Aliás, existem muitos desses benchmarks neste site.
21418 Konrad Rudolph
3
@KonradRudolph Isso também não é verdade. As microbenchmarks muitas vezes subestimam o custo do inchaço e da indireção, porque não esgotam certos recursos limitados (seja registradores, icache, memória, preditores de ramificação) onde um programa real o fará. Quando você alude ao "uso mais típico", está basicamente dizendo que você tem muito mais inchaço em outro lugar, o que é bom, mas fora de tópico. Na minha opinião, se você não possui requisitos de desempenho, não precisa programar em C ++.
KevinZ
18

Além do que todas as outras respostas disseram,
também há o fato de que nãostd::endl é o mesmo .'\n'

Infelizmente, este é um equívoco comum. std::endlnão significa "nova linha",
significa "imprimir nova linha e liberar o fluxo ". Flushing não é barato!

Ignorando completamente as diferenças entre printfe std::coutpor um momento, para ser funcionalmente equivalente ao seu exemplo de C, seu exemplo de C ++ deve ficar assim:

#include <iostream>

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

E aqui está um exemplo de como devem ser seus exemplos se você incluir a descarga.

C

#include <stdio.h>

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

C ++

#include <iostream>

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

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.

Pharap
fonte
Na verdade, usar 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.
De fato, a biblioteca iostream não possui um modo de buffer de linha ... a maneira de obter o efeito de buffer de linha é precisamente usar std::endlpara gerar novas linhas.
@Hurkyl Insist? Então, para que serve 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.
Pryftan #
Não é printfliberado automaticamente ao encontrar um caractere de nova linha?
bool3max
1
@ bool3max Isso só me diz o que meu ambiente faz, pode ser diferente em outros ambientes. Mesmo que se comporte da mesma forma em todas as implementações mais populares, isso não significa que exista um caso de ponta em algum lugar. É por isso que o padrão é tão importante - o padrão determina se algo deve ser o mesmo para todas as implementações ou se é permitido variar entre implementações.
Pharap
16

Embora as respostas técnicas existentes estejam corretas, acho que a pergunta deriva finalmente desse equívoco:

É famoso que em C ++ você paga pelo que come.

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.

Theodoros Chatzigiannakis
fonte
1
Há apenas duas coisas em que posso pensar onde você paga por algo que não usa: exceções e RTTI. E não acho que seja conversa de marketing; C ++ é basicamente um C mais poderoso, que também é "não paga pelo que você usa".
precisa saber é o seguinte
2
@ Rakete1111 Há muito tempo é estabelecido que, se as exceções não derem, elas não custam. Se seu programa estiver sendo lançado de forma consistente, ele deverá ser redesenhado. Se a condição de falha estiver fora de seu controle, verifique a condição com uma verificação de sanidade de retorno booleano, antes de chamar o método que se baseia na condição de que a condição é falsa.
schulmaster
1
@schulmaster: Exceções podem impor restrições de design quando o código escrito em C ++ precisa interagir com o código escrito em outras linguagens, já que transferências de controle não locais só podem funcionar sem problemas entre os módulos se os módulos souberem coordenar-se.
Supercat 22/09
1
(embora indiscutivelmente nem todos) os idiomas se esforçam para ser eficientes . Definitivamente nem todos: as linguagens de programação esotéricas se esforçam para ser novas / interessantes, não eficientes. esolangs.org . Alguns deles, como o BrainFuck, são notoriamente ineficientes. Ou, por exemplo, a linguagem de programação de Shakespeare, tamanho mínimo de 227 bytes (codegolf) para imprimir todos os números inteiros . Fora dos idiomas destinados ao uso em produção, a maioria busca eficiência, mas alguns (como o bash) buscam principalmente a conveniência e são conhecidos por serem lentos.
Peter Cordes
2
Bem, é marketing, mas é quase completamente verdade. Você pode manter <cstdio>e não incluir <iostream>, assim como pode compilar -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables.
KevinZ
11

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.

Bathsheba
fonte
23
"As funções de Entrada / Saída no C ++ são monstros hediondos que lutam para esconder sua natureza cthuliana por trás de uma fina camada de utilidade. Em muitos aspectos, são uma demonstração de como não projetar o código C ++ moderno". Provavelmente seria mais preciso.
user673679
3
@ user673679: Muito verdade. O grande problema com os fluxos de E / S C ++ é o que está por baixo: há realmente muita complexidade acontecendo, e qualquer pessoa que já tenha lidado com eles (estou me referindo a std::basic_*streambaixo) 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.
Edmz 22/09/19
1

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Charles Merriam
fonte