In C ++, sto pagando per quello che non sto mangiando?


170

Consideriamo i seguenti esempi di ciao mondo in 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 li compilo in godbolt in assembly, la dimensione del codice C è solo 9 righe ( gcc -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret

Ma la dimensione del codice C ++ è di 22 righe ( 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

... che è molto più grande.

È famoso che in C ++ paghi per quello che mangi. Quindi, in questo caso, per cosa sto pagando?


3
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew


26
Non ho mai sentito il termine eatassociato a C ++. Credo che intendi: "Paghi solo per quello che usi "?
Giacomo Alzetta,

7
@GiacomoAlzetta, ... è un colloquialismo, che applica il concetto di buffet a volontà. L'uso del termine più preciso è certamente preferibile con un pubblico globale, ma come madrelingua inglese americano, il titolo ha senso per me.
Charles Duffy,

5
@ trolley813 Le perdite di memoria non hanno nulla a che fare con la domanda di preventivo e OP. Il punto "Paghi solo per ciò che usi" / "Non paghi per ciò che non usi" è quello di dire che non viene preso alcun colpo di prestazione se non usi una caratteristica / astrazione specifica. Le perdite di memoria non hanno nulla a che fare con questo, e questo dimostra solo che il termine eatè più ambiguo e dovrebbe essere evitato.
Giacomo Alzetta,

Risposte:


60

Quello che stai pagando è chiamare una libreria pesante (non pesante come stampare sulla console). Si inizializza un ostreamoggetto. Ci sono alcuni archivi nascosti. Quindi, chiami per il std::endlquale non è sinonimo \n. La iostreamlibreria ti aiuta a regolare molte impostazioni e a mettere il carico sul processore anziché sul programmatore. Questo è ciò per cui stai pagando.

Rivediamo il codice:

.LC0:
        .string "Hello world"
main:

Inizializzazione di un oggetto 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)

Richiamo di coutnuovo per stampare una nuova linea e svuotare

    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

Inizializzazione archiviazione statica:

_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

Inoltre, è essenziale distinguere tra la lingua e la biblioteca.

A proposito, questa è solo una parte della storia. Non sai cosa è scritto nelle funzioni che stai chiamando.


5
Come nota aggiuntiva, test approfonditi mostreranno che anteporre un programma C ++ con "ios_base :: sync_with_stdio (false);" e "cin.tie (NULL);" renderà il cout più veloce di printf (Printf ha un overhead di stringa di formato). Il primo elimina le spese generali assicurandosi che le cout; printf; coutscritture siano in ordine (poiché hanno i propri buffer). Il secondo verrà desincronizzato coute cin, causando cout; cinpotenzialmente chiedere all'utente prima informazioni. Il lavaggio forzerà la sincronizzazione solo quando è effettivamente necessario.
Nicholas Pipitone,

Ciao Nicholas, grazie mille per aver aggiunto queste utili note.
Arash,

"è essenziale distinguere tra la lingua e la libreria": beh sì, ma la libreria standard fornita con una lingua è l'unica disponibile ovunque, quindi è quella utilizzata ovunque (e sì, la libreria standard C fa parte della specifica C ++, quindi può essere utilizzato quando lo si desidera). Quanto a "Non sai cosa è scritto nelle funzioni che stai chiamando": puoi collegarti staticamente se vuoi davvero saperlo, e in effetti il ​​codice chiamante che esaminerai è probabilmente irrilevante.
Peter - Ripristina Monica il

211

Quindi, in questo caso, per cosa sto pagando?

std::coutè più potente e complicato di printf. Supporta cose come locali, flag di formattazione con stato e altro.

Se non ti servono, usa std::printfo std::puts- sono disponibili in <cstdio>.


È famoso che in C ++ paghi per quello che mangi.

Voglio anche chiarire che C ++ ! = La libreria standard C ++. La libreria standard dovrebbe essere di uso generale e "abbastanza veloce", ma spesso sarà più lenta di un'implementazione specializzata di ciò di cui hai bisogno.

D'altra parte, il linguaggio C ++ si sforza di rendere possibile la scrittura di codice senza pagare costi nascosti extra non necessari (ad esempio opt-in virtual, nessuna raccolta dei rifiuti).


4
+1 per dire che la Libreria standard dovrebbe essere di uso generale e "abbastanza veloce", ma spesso sarà più lenta di un'implementazione specializzata di ciò di cui hai bisogno. Molti sembrano usare allegramente i componenti STL senza considerare le implicazioni delle prestazioni rispetto al rotolamento.
Craig Estey,

7
@Craig OTOH molte parti della libreria standard sono generalmente più veloci e più corrette di quelle che si potrebbero produrre invece.
Peter - Ripristina Monica il

2
@ PeterA.Schneider OTOH, quando la versione STL è 20x-30x più lenta, far rotolare la tua è una buona cosa. Vedi la mia risposta qui: codereview.stackexchange.com/questions/191747/… In questo, altri hanno anche suggerito [almeno un parziale] di rollare il tuo.
Craig Estey,

1
@CraigEstey Un vettore è (a parte l'allocazione dinamica iniziale che può essere significativa, a seconda di quanto lavoro verrà eventualmente svolto con una determinata istanza) non meno efficiente di un array C; è progettato per non essere. Bisogna fare attenzione a non copiarlo, riservare inizialmente abbastanza spazio, ecc., Ma tutto ciò deve essere fatto anche con un array e in modo meno sicuro. Per quanto riguarda il tuo esempio collegato: Sì, un vettore di vettori (a meno che non sia ottimizzato via) incorrerà in un'ulteriore indiretta rispetto a un array 2D, ma suppongo che l'efficienza 20x non sia radicata lì ma nell'algoritmo.
Peter - Ripristina Monica il

174

Non stai confrontando C e C ++. Stai confrontando printfe std::cout, che sono in grado di cose diverse (locali, formattazione con stato, ecc.).

Prova a utilizzare il seguente codice per il confronto. Godbolt genera lo stesso assembly per entrambi i file (testato con 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);
    }
}


Saluti per aver mostrato codice equivalente e spiegare il motivo.
HackSlash,

134

Le tue inserzioni stanno effettivamente confrontando mele e arance, ma non per il motivo implicito nella maggior parte delle altre risposte.

Controlliamo cosa fa effettivamente il tuo codice:

C:

  • stampare una singola stringa, "Hello world\n"

C ++:

  • eseguire lo streaming della stringa "Hello world"instd::cout
  • flusso del std::endlmanipolatore instd::cout

Apparentemente il tuo codice C ++ sta facendo il doppio del lavoro. Per un confronto equo dovremmo combinare questo:

#include <iostream>

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

... e all'improvviso il tuo codice assembly mainsembra molto simile a quello di 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

In effetti, possiamo confrontare il codice C e C ++ riga per riga e ci sono pochissime differenze :

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

L'unica vera differenza è che in C ++ chiamiamo operator <<con due argomenti ( std::coute la stringa). Potremmo rimuovere anche quella leggera differenza usando un C eqivalent più vicino: fprintfche ha anche un primo argomento che specifica il flusso.

Questo lascia il codice assembly per _GLOBAL__sub_I_main , che viene generato per C ++ ma non C. Questo è l'unico vero overhead visibile in questo elenco di assembly (c'è altro overhead invisibile per entrambi ovviamente lingue). Questo codice esegue un'installazione singola di alcune funzioni di libreria standard C ++ all'avvio del programma C ++.

Ma, come spiegato in altre risposte, la differenza rilevante tra questi due programmi non si troverà nell'output di assemblaggio della mainfunzione poiché tutto il sollevamento pesante avviene dietro le quinte.


21
Per inciso il runtime C inoltre deve essere istituito, e questo avviene in una funzione chiamata _start, ma il suo codice è parte della libreria runtime C. Ad ogni modo ciò accade sia per C che per C ++.
Konrad Rudolph,

2
@Deduplicator: In realtà, per impostazione predefinita la libreria iostream non esegue alcun buffering std::coute passa invece l'I / O all'implementazione stdio (che utilizza i propri meccanismi di buffering). In particolare, quando si è collegati a (ciò che è noto) un terminale interattivo, per impostazione predefinita non si vedrà mai un output completamente bufferizzato durante la scrittura std::cout. È necessario disabilitare esplicitamente la sincronizzazione con stdio se si desidera che la libreria iostream utilizzi i propri meccanismi di buffering std::cout.

6
@KonradRudolph: In realtà, printfnon è necessario scaricare i flussi qui. In effetti, in un caso d'uso comune (output reindirizzato al file), di solito troverete che l' printfistruzione non scarica. Solo quando l'output è bufferizzato o non bufferizzato, si printfattiva un flush.

2
@PeterCordes: Giusto, non puoi bloccare con buffer di output non scaricati, ma puoi imbatterti nella sorpresa in cui il programma ha accettato il tuo input e ha continuato a marciare senza visualizzare l'output previsto. Lo so perché ho avuto occasione di eseguire il debug di un "Aiuto, il mio programma si blocca durante l'input ma non riesco a capire perché!" che aveva fornito un altro sviluppatore adatto per alcuni giorni.

2
@PeterCordes: l'argomento che faccio è "scrivi cosa intendi" - le nuove righe sono appropriate quando intendi che l'output sia finalmente disponibile, ed endl è appropriato quando intendi che l'output sia immediatamente disponibile.

53

È famoso che in C ++ paghi per quello che mangi. Quindi, in questo caso, per cosa sto pagando?

È semplice. Paghi per std::cout. "Paghi solo ciò che mangi" non significa "ottieni sempre i prezzi migliori". Certo, printfè più economico. Si può affermare che std::coutè più sicuro e più versatile, quindi il suo maggior costo è giustificato (costa di più, ma fornisce più valore), ma ciò non vale. Non usi printf, usi std::cout, quindi paghi per l'utilizzo std::cout. Non paghi per l'utilizzo printf.

Un buon esempio sono le funzioni virtuali. Le funzioni virtuali hanno alcuni costi di runtime e requisiti di spazio, ma solo se effettivamente utilizzati. Se non usi le funzioni virtuali, non paghi nulla.

Alcune osservazioni

  1. Anche se il codice C ++ restituisce ulteriori istruzioni di assemblaggio, è ancora una manciata di istruzioni e qualsiasi sovraccarico di prestazioni è comunque ridotto dalle effettive operazioni di I / O.

  2. In realtà, a volte è persino meglio di "in C ++ paghi per quello che mangi". Ad esempio, il compilatore può dedurre che la chiamata di funzione virtuale non è necessaria in alcune circostanze e trasformarla in chiamata non virtuale. Ciò significa che potresti ottenere funzioni virtuali gratuitamente . Non è fantastico?


6
Non ottieni funzioni virtuali gratis. Devi ancora pagare il costo prima di scriverli e poi eseguire il debug della trasformazione del codice del compilatore quando non corrisponde alla tua idea di cosa avrebbe dovuto fare.
alephzero,

2
@alephzero Non sono sicuro che sia particolarmente rilevante confrontare i costi di sviluppo con i costi delle prestazioni.

Una grande opportunità per un gioco di parole sprecato ... Avresti potuto usare la parola "calorie" anziché "prezzo". Da ciò si potrebbe dire che C ++ è più grasso di C. O almeno ... il codice specifico in questione (sono di parte contro C ++ a favore di C, quindi non posso andare oltre). Ahimè. @Bilkokuya Potrebbe non essere rilevante in tutti i casi, ma è certamente qualcosa che non si dovrebbe ignorare. Quindi è rilevante nel complesso.
Pryftan,

46

La "lista assembly per printf" NON è per printf, ma per put (tipo di ottimizzazione del compilatore?); printf è molto più complesso di quanto metta ... non dimenticare!


13
Questa è finora la risposta migliore, dal momento che tutti gli altri vengono appesi su un'aringa rossa per std::coutgli interni, che non sono visibili nella lista dell'assemblea.
Konrad Rudolph,

12
L'elenco di assembly è per una chiamata a puts , che sembra identico a una chiamata printfse si passa solo una stringa di formato singolo e zero argomenti aggiuntivi. (tranne che ci sarà anche un xor %eax,%eaxperché stiamo passando zero argomenti FP nei registri a una funzione variadica.) Nessuna di queste due sono l'implementazione, semplicemente passando un puntatore a una stringa alla funzione libreria. Ma sì, ottimizzazione printfper putsqualcosa gcc fa per i formati che hanno solo "%s", o quando non ci sono le conversioni e le estremità di stringa con un ritorno a capo.
Peter Cordes,

45

Vedo alcune risposte valide qui, ma ho intenzione di approfondire un po 'di più i dettagli.

Vai al riepilogo di seguito per la risposta alla tua domanda principale se non vuoi passare attraverso questa intera parete di testo.


Astrazione

Quindi, in questo caso, per cosa sto pagando?

Stai pagando per l' astrazione . Essere in grado di scrivere un codice più semplice e più umano ha un costo. In C ++, che è un linguaggio orientato agli oggetti, quasi tutto è un oggetto. Quando usi qualsiasi oggetto, tre cose principali succederanno sempre sotto il cofano:

  1. Creazione di oggetti, sostanzialmente allocazione di memoria per l'oggetto stesso e i suoi dati.
  2. Inizializzazione dell'oggetto (di solito tramite un init()metodo). Di solito l'allocazione della memoria avviene sotto il cofano come prima cosa in questo passaggio.
  3. Distruzione di oggetti (non sempre).

Non lo vedi nel codice, ma ogni volta che usi un oggetto tutte e tre le cose sopra devono accadere in qualche modo. Se dovessi fare tutto manualmente, il codice sarebbe ovviamente molto più lungo.

Ora, l'astrazione può essere realizzata in modo efficiente senza aggiungere spese generali: il metodo inline e altre tecniche possono essere utilizzate sia dai compilatori che dai programmatori per rimuovere le spese generali di astrazione, ma non è questo il caso.

Cosa sta realmente succedendo in C ++?

Eccolo, suddiviso:

  1. La std::ios_baseclasse è inizializzata, che è la classe base per tutto ciò che riguarda l'I / O.
  2. L' std::coutoggetto è inizializzato.
  3. La tua stringa viene caricata e passata a std::__ostream_insert, che (come hai già capito con il nome) è un metodo di std::cout(sostanzialmente l' <<operatore) che aggiunge una stringa al flusso.
  4. cout::endlviene anche passato a std::__ostream_insert.
  5. __std_dso_handleviene passato a __cxa_atexit, che è una funzione globale responsabile della "pulizia" prima di uscire dal programma. __std_dso_handlestesso viene chiamato da questa funzione per deallocare e distruggere gli oggetti globali rimanenti.

Quindi usando C == non pagando nulla?

Nel codice C, stanno accadendo pochissimi passaggi:

  1. La tua stringa viene caricata e passata putstramite il ediregistro.
  2. puts viene chiamato.

Nessun oggetto da nessuna parte, quindi non è necessario inizializzare / distruggere nulla.

Questo però non significa che non stai "pagare" per nulla C . Stai ancora pagando per l'astrazione, e anche l'inizializzazione della libreria standard C e la risoluzione dinamica della printffunzione (o, in realtà puts, che è ottimizzata dal compilatore poiché non hai bisogno di alcuna stringa di formato) avvengono ancora sotto il cofano.

Se dovessi scrivere questo programma in puro assemblaggio sarebbe simile a questo:

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

Il che in pratica si traduce solo in invocazione della write syscall seguita dalla exitsyscall. Ora questo sarebbe il minimo indispensabile per realizzare la stessa cosa.


Riassumere

C è molto più semplice e fa solo il minimo indispensabile, lasciando il pieno controllo all'utente, che è in grado di ottimizzare completamente e personalizzare praticamente tutto ciò che vogliono. Dite al processore di caricare una stringa in un registro e quindi chiamate una funzione di libreria per usare quella stringa. Il C ++ invece è molto più complesso e astratto . Questo ha un enorme vantaggio quando si scrive un codice complicato e consente una scrittura più semplice e un codice più umano, ma ovviamente ha un costo. Ci sarà sempre uno svantaggio in termini di prestazioni in C ++ se confrontato con C in casi come questo, poiché C ++ offre più di quanto è necessario per svolgere tali compiti di base e quindi aggiunge più sovraccarico .

Rispondere alla domanda principale :

Sto pagando per quello che non sto mangiando?

In questo caso specifico, . Non stai sfruttando nulla di ciò che C ++ ha da offrire oltre a C, ma è solo perché non c'è niente in quel semplice pezzo di codice con cui C ++ potrebbe aiutarti: è così semplice che non hai davvero bisogno del C ++.


Oh, e solo un'altra cosa!

I vantaggi del C ++ potrebbero non sembrare ovvi a prima vista, poiché hai scritto un programma molto semplice e piccolo, ma guarda un esempio un po 'più complesso e vedi la differenza (entrambi i programmi fanno esattamente la stessa cosa):

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

Spero che tu possa vedere chiaramente cosa intendo qui. Nota anche come in C devi gestire la memoria a un livello inferiore usando malloce freecome devi essere più attento riguardo all'indicizzazione e alle dimensioni, e come devi essere molto specifico quando prendi input e stampa.


27

Ci sono alcune idee sbagliate per cominciare. Innanzitutto, il programma C ++ non porta a 22 istruzioni, è più simile a 22.000 (ho estratto quel numero dal mio cappello, ma è approssimativamente nel campo di baseball). Inoltre, il codice C non risulta in 9 istruzioni. Quelli sono solo quelli che vedi.

Ciò che fa il codice C è, dopo aver fatto molte cose che non vedi, chiama una funzione dal CRT (che di solito è ma non necessariamente presente come lib condivisa), quindi non controlla il valore restituito o gestisce errori e salvataggi. A seconda del compilatore e delle impostazioni di ottimizzazione, non si chiama nemmeno davvero , printfma puts, o qualcosa di ancora più primitivo.
Avresti potuto scrivere più o meno lo stesso programma (ad eccezione di alcune funzioni invisibili di init) anche in C ++, se solo avessi chiamato la stessa funzione nello stesso modo. Oppure, se si desidera essere super corretti, è stata aggiunta la stessa funzione con il prefisso std::.

Il corrispondente codice C ++ non è in realtà la stessa cosa. Mentre tutto <iostream>è ben noto per essere un brutto maiale grasso che aggiunge un immenso sovraccarico per piccoli programmi (in un programma "reale" non si nota molto), un'interpretazione un po 'più giusta è che fa un terribile molte cose che non vedi e che funzionano . Incluso ma non limitato alla formattazione magica di praticamente qualsiasi cosa casuale, compresi diversi formati numerici e locali e quant'altro, buffering e corretta gestione degli errori. Gestione degli errori? Bene sì, indovina un po ', l'output di una stringa può effettivamente fallire e, diversamente dal programma C, il programma C ++ non ignorerebbe silenziosamente. Considerando cosastd::ostreamfa sotto il cofano, e senza che nessuno se ne accorga, è in realtà piuttosto leggero. Non come se lo usassi perché odio la sintassi del flusso con passione. Tuttavia, è davvero fantastico se si considera ciò che fa.

Ma certo, C ++ nel complesso non è efficiente quanto C può essere. Non può essere così efficiente poiché non è la stessa cosa e non sta facendo la stessa cosa. Se non altro, C ++ genera eccezioni (e codice per generare, gestire o fallire su di esse) e offre alcune garanzie che C non dà. Quindi, certo, un programma C ++ deve necessariamente essere un po 'più grande. Nel quadro generale, tuttavia, questo non ha importanza in alcun modo. Al contrario, per i programmi reali , raramente non ho trovato le prestazioni del C ++ migliori perché, per un motivo o per l'altro, sembra prestare ottimizzazioni più favorevoli. Non chiedermi perché, in particolare, non lo saprei.

Se, invece di sparare e dimenticare la speranza per il meglio, ti interessa scrivere il codice C che è corretto (cioè controlli effettivamente gli errori e il programma si comporta correttamente in presenza di errori), allora la differenza è marginale, se esistente.


16
Ottima risposta, tranne per il fatto che questa affermazione: "Ma certo, C ++ nel complesso non è efficiente come C può essere" è semplicemente sbagliato. Il C ++ può essere efficiente come il C e un codice di livello sufficientemente alto può essere più efficiente del codice C equivalente. Sì, il C ++ ha un certo overhead dovuto alla necessità di gestire le eccezioni, ma sui compilatori moderni l'overhead di ciò è trascurabile rispetto ai guadagni in termini di prestazioni derivanti da migliori astrazioni gratuite.
Konrad Rudolph,

Se ho capito bene, std::coutgenera anche delle eccezioni?
Saher,

6
@Saher: Sì, no, forse. std::coutè un std::basic_ostreame che uno può lanciare e può riproporre eccezioni che si verificano altrimenti se configurato per farlo o può ingoiare eccezioni. Il fatto è che le cose possono fallire e il C ++ e la lib standard C ++ sono (principalmente) costruiti in modo che i fallimenti non passino facilmente inosservati. Questo è un fastidio e una benedizione (ma, più benedizione che fastidio). C d'altra parte ti mostra solo il dito medio. Non controlli un codice di ritorno, non sai mai cosa è successo.
Damon,

1
@KonradRudolph: Vero, questo è quello che stavo cercando di sottolineare con "Non ho trovato raramente il C ++ con prestazioni migliori perché, per un motivo o per l'altro, sembra prestarsi a ottimizzazioni più favorevoli. Non chiedermi perché in particolare" . Non è immediatamente ovvio il perché, ma non di rado si ottimizza meglio. Per qualsiasi ragione. Penseresti che sia lo stesso per l'ottimizzatore, ma non lo è.
Damon,

22

Stai pagando per un errore. Negli anni '80, quando i compilatori non erano abbastanza buoni per controllare le stringhe di formato, il sovraccarico dell'operatore era visto come un buon modo per applicare una parvenza di sicurezza del tipo durante io. Tuttavia, ognuna delle sue funzionalità di banner è implementata male o concettualmente fallita dall'inizio:

<Iomanip>

La parte più ripugnante del flusso C ++ io api è l'esistenza di questa libreria di intestazioni di formattazione. Oltre ad essere stato brutto e soggetto a errori, accoppia la formattazione allo stream.

Supponiamo di voler stampare una riga con int esadecimale senza segno a 8 cifre senza segno seguita da uno spazio seguito da un doppio con 3 cifre decimali. Con <cstdio>, puoi leggere una stringa di formato concisa. Con <ostream>, devi salvare il vecchio stato, impostare l'allineamento a destra, impostare il carattere di riempimento, impostare la larghezza di riempimento, impostare la base su esadecimale, generare l'intero, ripristinare lo stato salvato (altrimenti la formattazione dei numeri interi inquinerà la formattazione float), genera lo spazio , imposta la notazione su fixed, imposta la precisione, stampa il double e il newline, quindi ripristina la vecchia formattazione.

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

Sovraccarico dell'operatore

<iostream> è il bambino poster di come non utilizzare il sovraccarico dell'operatore:

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

Prestazione

std::coutè più volte più lento printf(). La featuritis dilagante e l'invio virtuale fanno la differenza.

Sicurezza del filo

Entrambi <cstdio>e <iostream>sono thread-safe in quanto ogni chiamata di funzione è atomica. Ma printf()ottiene molto di più per chiamata. Se esegui il seguente programma con l' <cstdio>opzione, vedrai solo una riga di f. Se lo usi <iostream>su una macchina multicore, probabilmente vedrai qualcos'altro.

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

La risposta a questo esempio è che la maggior parte delle persone esercita la disciplina per non scrivere mai su un singolo descrittore di file da più thread in ogni caso. Beh, in questo caso, dovrete osservare che <iostream>saranno utilmente prendere un blocco su ogni <<singolo >>. Considerando <cstdio>che, non si bloccherà più spesso e si ha anche la possibilità di non bloccare.

<iostream> spende più blocchi per ottenere un risultato meno coerente.


2
La maggior parte delle implementazioni di printf ha una funzionalità estremamente utile per la localizzazione: parametri numerati. Se è necessario produrre un output in due lingue diverse (come inglese e francese) e l'ordine delle parole è diverso, è possibile utilizzare lo stesso printf con una stringa di formattazione diversa e stampare i parametri in ordine diverso.
gnasher729,

2
Quella formattazione con stato dei flussi deve aver dato così tanti bug difficili da trovare che non so cosa dire. Bella risposta. Voterei più di una volta se potessi.
matematico

6
" std::coutÈ più volte più lento printf()" - Questa affermazione si ripete su tutta la rete ma non è stata vera per secoli. Le moderne implementazioni IOstream funzionano alla pari con printf. Quest'ultimo esegue anche l'invio virtuale internamente per gestire flussi bufferizzati e IO locale (fatto dal sistema operativo ma fatto comunque).
Konrad Rudolph,

3
@KevinZ Ed è fantastico, ma sta confrontando una singola chiamata specifica, che mostra i punti di forza specifici di fmt (molti formati diversi in una singola stringa). Nell'uso più tipico la differenza tra printfe coutsi riduce. Per inciso, ci sono tonnellate di tali parametri di riferimento proprio su questo sito.
Konrad Rudolph,

3
@KonradRudolph Neanche questo è vero. I microbenchmark spesso sottostimano il costo di gonfiaggio e di indiretta perché non esauriscono determinate risorse limitate (che si tratti di registri, icache, memoria, predittori di succursali) dove sarà un vero programma. Quando alludi a "un uso più tipico", in pratica si dice che hai molto più gonfia altrove, il che va bene, ma fuori tema. Secondo me, se non si hanno requisiti prestazionali, non è necessario programmare in C ++.
KevinZ,

18

Oltre a ciò che hanno detto tutte le altre risposte,
c'è anche il fatto che nonstd::endl è la stessa .'\n'

Questo è purtroppo un malinteso comune. std::endlnon significa "nuova riga",
significa "stampa una nuova riga e quindi svuota il flusso ". Il lavaggio non è economico!

Ignorando completamente le differenze tra printfe std::coutper un momento, per essere funzionalmente eqvuialent al tuo esempio C, il tuo esempio C ++ dovrebbe assomigliare a questo:

#include <iostream>

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

Ed ecco un esempio di come dovrebbero essere i tuoi esempi se includi il flushing.

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

Quando si confronta il codice, si dovrebbe sempre fare attenzione a confrontare like per like e comprendere le implicazioni di ciò che il codice sta facendo. A volte anche gli esempi più semplici sono più complicati di quanto alcuni credano.


In realtà, l'utilizzo std::endl è l'equivalente funzionale della scrittura di una nuova riga in un flusso stdio con buffer di linea. stdout, in particolare, deve essere bufferizzato in linea o senza buffer quando è collegato a un dispositivo interattivo. Linux, credo, insiste sull'opzione buffer di linea.

In effetti, la libreria iostream non ha una modalità buffer di linea ... il modo per ottenere l'effetto del buffering di linea è proprio quello di utilizzare std::endlper generare nuove righe.

@Hurkyl Insist? Allora a che serve setvbuf(3)? O intendi dire che l'impostazione predefinita è buffer di riga? Cordiali saluti: normalmente tutti i file sono bufferizzati. Se uno stream fa riferimento a un terminale (come normalmente fa stdout), è bufferizzato in linea. Il flusso di errori standard stderr è sempre senza buffer per impostazione predefinita.
Pryftan,

Non si printfscarica automaticamente quando si incontra un personaggio di nuova riga?
bool3max,

1
@ bool3max Questo mi direbbe solo cosa fa il mio ambiente, potrebbe essere diverso in altri ambienti. Anche se si comporta allo stesso modo in tutte le implementazioni più popolari, ciò non significa che ci sia un caso limite da qualche parte. Ecco perché lo stanard è così importante: lo standard stabilisce se qualcosa deve essere uguale per tutte le implementazioni o se è consentito variare tra le implementazioni.
Pharap,

16

Mentre le risposte tecniche esistenti sono corrette, penso che la domanda alla fine derivi da questo malinteso:

È famoso che in C ++ paghi per quello che mangi.

Questo è solo un discorso di marketing della comunità C ++. (Per essere onesti, ci sono discorsi di marketing in ogni comunità linguistica.) Non significa nulla di concreto da cui poter seriamente dipendere.

"Paghi per quello che usi" dovrebbe significare che una funzione C ++ ha un sovraccarico solo se la usi. Ma la definizione di "caratteristica" non è infinitamente granulare. Spesso finirai per attivare funzionalità con molteplici aspetti e anche se hai solo bisogno di un sottoinsieme di tali aspetti, spesso non è pratico o possibile per l'implementazione includere parzialmente la funzionalità.

In generale, molte lingue (sebbene probabilmente non tutte) si sforzano di essere efficienti, con vari gradi di successo. Il C ++ è da qualche parte nella scala, ma non c'è nulla di speciale o magico nel suo design che gli permetta di avere perfettamente successo in questo obiettivo.


1
Ci sono solo due cose a cui posso pensare dove pagare per qualcosa che non usi: eccezioni e RTTI. E non penso che sia un discorso di marketing; Il C ++ è fondamentalmente un C più potente, che è anche "non pagare per quello che usi".
Rakete1111,

2
@ Rakete1111 È ormai noto che se le eccezioni non generano, non costano. Se il tuo programma viene lanciato in modo coerente, dovrebbe essere riprogettato. Se la condizione di errore è fuori dal tuo controllo, dovresti verificare la condizione con un controllo di integrità che restituisce bool, prima di chiamare il metodo che si basa sulla condizione di essere falsa.
schulmaster,

1
@schulmaster: le eccezioni possono imporre vincoli di progettazione quando il codice scritto in C ++ deve interagire con il codice scritto in altre lingue, poiché i trasferimenti di controllo non locali possono funzionare senza problemi tra i moduli solo se i moduli sanno come coordinarsi tra loro.
supercat,

1
(anche se probabilmente non tutte) le lingue si sforzano di essere efficienti . Sicuramente non tutti: i linguaggi di programmazione esoterici si sforzano di essere nuovi / interessanti, non efficienti. esolangs.org . Alcuni di loro, come BrainFuck, sono notoriamente inefficienti. O ad esempio, il linguaggio di programmazione Shakespeare, dimensione minima 227 byte (codegolf) per stampare tutti i numeri interi . Al di fuori delle lingue destinate alla produzione, la maggior parte punta all'efficienza, ma alcune (come la bash) mirano principalmente alla praticità e sono note per essere lente.
Peter Cordes,

2
Bene, è marketing ma è quasi del tutto vero. È possibile attenersi <cstdio>e non includere <iostream>, proprio come è possibile compilare -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables.
KevinZ,

11

Le funzioni di Input / Output in C ++ sono elegantemente scritte e progettate in modo da essere semplici da usare. Per molti aspetti sono una vetrina per le funzionalità orientate agli oggetti in C ++.

Ma in effetti si rinuncia a un po 'di prestazioni in cambio, ma questo è trascurabile rispetto al tempo impiegato dal sistema operativo per gestire le funzioni a un livello inferiore.

Puoi sempre ricorrere alle funzioni di stile C poiché fanno parte dello standard C ++, o forse rinunciare del tutto alla portabilità e utilizzare le chiamate dirette al tuo sistema operativo.


23
"Le funzioni di Input / Output in C ++ sono mostri orribili che lottano per nascondere la loro natura cthuliana dietro una sottile patina di utilità. Per molti aspetti sono una vetrina per come non progettare il moderno codice C ++". Probabilmente sarebbe più preciso.
user673679,

3
@ user673679: molto vero. Il grande problema con i flussi di I / O C ++ è quello che c'è sotto: c'è davvero molta complessità in corso, e chiunque abbia mai affrontato (mi riferisco al std::basic_*streamverso del basso) conosce le eadach in arrivo. Sono stati progettati per essere ampiamente generali ed estesi attraverso l'eredità; ma alla fine nessuno lo fece, a causa della loro complessità (ci sono letteralmente libri scritti su iostreams), così tanto che nacquero nuove biblioteche proprio per quello (es. boost, ICU ecc.). Dubito che smetteremo mai di pagare per questo errore.
edmz,

1

Come hai visto in altre risposte, paghi quando ti colleghi a librerie generali e chiami costruttori complessi. Non c'è una domanda particolare qui, più una lamentela. Indico alcuni aspetti del mondo reale:

  1. Barne aveva un principio progettuale fondamentale per non lasciare che l'efficienza fosse un motivo per rimanere in C piuttosto che in C ++. Detto questo, bisogna fare attenzione a ottenere queste efficienze e ci sono efficienze occasionali che hanno sempre funzionato ma non erano "tecnicamente" all'interno delle specifiche C. Ad esempio, il layout dei campi di bit non è stato specificato.

  2. Prova a guardare attraverso ostream. Oh mio Dio, è gonfio! Non sarei sorpreso di trovare un simulatore di volo lì dentro. Anche stdlib's printf () di solito esegue circa 50K. Questi non sono programmatori pigri: metà della dimensione di printf aveva a che fare con argomenti di precisione indiretti che la maggior parte delle persone non usa mai. Quasi tutte le librerie del processore veramente vincolate creano il proprio codice di output anziché printf.

  3. L'aumento delle dimensioni di solito fornisce un'esperienza più contenuta e flessibile. Per analogia, un distributore automatico venderà una tazza di sostanza simile al caffè per poche monete e l'intera transazione richiederà meno di un minuto. Entrare in un buon ristorante implica un tavolo, sedersi, ordinare, aspettare, prendere una bella tazza, ottenere un conto, pagare nella scelta dei moduli, aggiungere una mancia e farsi augurare una buona giornata all'uscita. È un'esperienza diversa e più conveniente se fai un salto con gli amici per un pasto complesso.

  4. Le persone scrivono ancora ANSI C, sebbene raramente K&R C. La mia esperienza è che la compiliamo sempre con un compilatore C ++ usando alcune modifiche di configurazione per limitare ciò che viene trascinato. Vi sono buoni argomenti per altre lingue: Go rimuove l'overhead polimorfico e il preprocessore pazzo ; ci sono stati alcuni buoni argomenti per un impacchettamento dei campi e un layout di memoria più intelligenti. Secondo me, qualsiasi progetto di linguaggio dovrebbe iniziare con un elenco di obiettivi, proprio come lo Zen di Python .

È stata una discussione divertente. Ti chiedi perché non puoi avere librerie magicamente piccole, semplici, eleganti, complete e flessibili?

Non c'è risposta. Non ci sarà una risposta. Questa è la risposta

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.