Bezahle ich in C ++ für das, was ich nicht esse?


170

Betrachten wir die folgenden Hallo-Welt-Beispiele in C und 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;
}

Wenn ich sie in Godbolt to Assembly kompiliere, beträgt die Größe des C-Codes nur 9 Zeilen ( gcc -O3):

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

Die Größe des C ++ - Codes beträgt jedoch 22 Zeilen ( 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

... was viel größer ist.

Es ist berühmt, dass Sie in C ++ für das bezahlen, was Sie essen. Wofür bezahle ich in diesem Fall?


3
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Samuel Liew


26
Ich habe den eatmit C ++ verbundenen Begriff noch nie gehört . Ich glaube du meinst: "Du bezahlst nur für das, was du benutzt "?
Giacomo Alzetta

7
@GiacomoAlzetta, ... es ist eine Umgangssprache, die das Konzept eines All-you-can-eat-Buffets anwendet. Die Verwendung des genaueren Begriffs ist für ein globales Publikum sicherlich vorzuziehen, aber als englischer Muttersprachler macht der Titel für mich Sinn.
Charles Duffy

5
@ trolley813 Speicherlecks haben nichts mit dem Zitat und der OP-Frage zu tun. Der Punkt "Sie zahlen nur für das, was Sie verwenden" / "Sie zahlen nicht für das, was Sie nicht verwenden" bedeutet, dass kein Leistungseinbruch erzielt wird, wenn Sie keine bestimmte Funktion / Abstraktion verwenden. Speicherlecks haben damit überhaupt nichts zu tun, und dies zeigt nur, dass der Begriff eatmehrdeutiger ist und vermieden werden sollte.
Giacomo Alzetta

Antworten:


60

Was Sie bezahlen, ist, eine schwere Bibliothek aufzurufen (nicht so schwer wie das Drucken in die Konsole). Sie initialisieren ein ostreamObjekt. Es gibt einige versteckte Speicher. Dann rufen Sie an, std::endlwas kein Synonym für ist \n. Die iostreamBibliothek hilft Ihnen dabei, viele Einstellungen anzupassen und den Prozessor und nicht den Programmierer zu belasten. Dafür bezahlen Sie.

Lassen Sie uns den Code überprüfen:

.LC0:
        .string "Hello world"
main:

Initialisieren eines Ostream-Objekts + 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)

Rufen Sie couterneut an, um eine neue Zeile zu drucken und zu spülen

    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

Statische Speicherinitialisierung:

_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

Es ist auch wichtig, zwischen der Sprache und der Bibliothek zu unterscheiden.

Übrigens ist dies nur ein Teil der Geschichte. Sie wissen nicht, was in den von Ihnen aufgerufenen Funktionen geschrieben steht.


5
Als zusätzliche Anmerkung zeigen gründliche Tests, dass ein C ++ - Programm mit "ios_base :: sync_with_stdio (false)" vorangestellt wird. und "cin.tie (NULL)"; macht cout schneller als printf (Printf hat Format-String-Overhead). Der erste eliminiert den Overhead, um sicherzustellen, dass die cout; printf; coutSchreibvorgänge in der richtigen Reihenfolge ausgeführt werden (da sie über eigene Puffer verfügen). Der zweite wird desynchronisiert coutund cinführt dazu cout; cin, dass der Benutzer möglicherweise zuerst nach Informationen gefragt wird. Durch das Spülen wird die Synchronisierung nur dann erzwungen, wenn Sie dies tatsächlich benötigen.
Nicholas Pipitone

Hallo Nicholas, vielen Dank, dass Sie diese nützlichen Notizen hinzugefügt haben.
Arash

"Es ist wichtig, zwischen der Sprache und der Bibliothek zu unterscheiden": Nun ja, aber die Standardbibliothek, die mit einer Sprache geliefert wird, ist die einzige, die überall verfügbar ist. Sie wird also überall verwendet (und ja, die C-Standardbibliothek ist Teil davon der C ++ - Spezifikation, kann also bei Bedarf verwendet werden). Zu "Sie wissen nicht, was in den Funktionen geschrieben ist, die Sie aufrufen": Sie können statisch verknüpfen, wenn Sie es wirklich wissen möchten, und tatsächlich ist der aufrufende Code, den Sie untersuchen, wahrscheinlich irrelevant.
Peter - Reinstate Monica

211

Wofür bezahle ich in diesem Fall?

std::coutist mächtiger und komplizierter als printf. Es unterstützt Dinge wie Gebietsschemas, Stateful-Formatierungsflags und mehr.

Wenn Sie diese nicht benötigen, verwenden Sie std::printfoder std::puts- sie sind in verfügbar <cstdio>.


Es ist berühmt, dass Sie in C ++ für das bezahlen, was Sie essen.

Ich möchte auch klarstellen, dass C ++ ! = Die C ++ - Standardbibliothek. Die Standardbibliothek soll universell einsetzbar und "schnell genug" sein, ist jedoch häufig langsamer als eine spezielle Implementierung Ihrer Anforderungen.

Andererseits ist die C ++ - Sprache bestrebt, das Schreiben von Code zu ermöglichen, ohne unnötige zusätzliche versteckte Kosten zu zahlen (z. B. Opt-In virtual, keine Speicherbereinigung).


4
+1 für die Aussage, dass die Standardbibliothek universell einsetzbar und "schnell genug" sein soll, aber oft langsamer als eine spezielle Implementierung dessen, was Sie benötigen. Viele scheinen STL-Komponenten munter zu verwenden, ohne die Auswirkungen auf die Leistung zu berücksichtigen, anstatt Ihre eigenen zu rollen.
Craig Estey

7
@Craig OTOH Viele Teile der Standardbibliothek sind normalerweise schneller und korrekter als das, was man normalerweise produzieren könnte.
Peter - Stellen Sie Monica

2
@ PeterA.Schneider OTOH, wenn die STL-Version 20x-30x langsamer ist, ist es gut, die eigene zu rollen. Siehe meine Antwort hier: codereview.stackexchange.com/questions/191747/… Darin schlugen andere auch vor, [zumindest teilweise] selbst zu rollen.
Craig Estey

1
@CraigEstey Ein Vektor ist (abgesehen von der anfänglichen dynamischen Zuordnung, die erheblich sein kann, abhängig davon, wie viel Arbeit letztendlich mit einer bestimmten Instanz erledigt wird) nicht weniger effizient als ein C-Array. es soll nicht sein. Es muss darauf geachtet werden, dass es nicht kopiert wird, zunächst genügend Speicherplatz reserviert wird usw. Dies alles muss jedoch auch mit einem Array und weniger sicher erfolgen. In Bezug auf Ihr verknüpftes Beispiel: Ja, ein Vektor von Vektoren wird (sofern nicht wegoptimiert) eine zusätzliche Indirektion im Vergleich zu einem 2D-Array verursachen, aber ich gehe davon aus, dass die 20-fache Effizienz nicht dort, sondern im Algorithmus verwurzelt ist.
Peter - Monica

174

Sie vergleichen C und C ++ nicht. Sie vergleichen printfund std::cout, die zu verschiedenen Dingen fähig sind (Gebietsschemas, zustandsbehaftete Formatierung usw.).

Versuchen Sie, den folgenden Code zum Vergleich zu verwenden. Godbolt generiert für beide Dateien dieselbe Assembly (getestet mit gcc 8.2, -O3).

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


Ein Hoch auf den entsprechenden Code und die Erklärung des Grundes.
HackSlash

134

Ihre Angebote vergleichen zwar Äpfel und Orangen, aber nicht aus dem Grund, der in den meisten anderen Antworten impliziert ist.

Lassen Sie uns überprüfen, was Ihr Code tatsächlich tut:

C:

  • eine einzelne Zeichenfolge drucken, "Hello world\n"

C ++:

  • Streame den String "Hello world"instd::cout
  • Streame den std::endlManipulator instd::cout

Anscheinend macht Ihr C ++ - Code doppelt so viel Arbeit. Für einen fairen Vergleich sollten wir dies kombinieren:

#include <iostream>

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

… Und plötzlich mainsieht Ihr Assembler-Code für C sehr ähnlich aus:

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

Tatsächlich können wir den C- und C ++ - Code Zeile für Zeile vergleichen, und es gibt nur sehr wenige Unterschiede :

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

Der einzige wirkliche Unterschied besteht darin, dass wir in C ++ operator <<mit zwei Argumenten ( std::coutund der Zeichenfolge) aufrufen . Wir könnten sogar diesen kleinen Unterschied beseitigen, indem wir ein näheres Äquivalent verwenden:, fprintfdas auch ein erstes Argument enthält, das den Strom spezifiziert.

Dadurch bleibt der Assemblycode für _GLOBAL__sub_I_mainübrig, der für C ++, aber nicht für C generiert wird. Dies ist der einzige echte Overhead, der in dieser Assemblyliste angezeigt wird (es gibt natürlich mehr unsichtbaren Overhead für beide Sprachen). Dieser Code führt zu Beginn des C ++ - Programms eine einmalige Einrichtung einiger C ++ - Standardbibliotheksfunktionen durch.

Wie in anderen Antworten erläutert, wird der relevante Unterschied zwischen diesen beiden Programmen jedoch nicht in der Assembly-Ausgabe der mainFunktion gefunden, da das ganze schwere Heben hinter den Kulissen stattfindet.


21
Übrigens die C - Laufzeit auch muss eingerichtet werden, und dies geschieht in einer Funktion namens _startaber sein Code ist Teil der C - Laufzeitbibliothek. In jedem Fall geschieht dies sowohl für C als auch für C ++.
Konrad Rudolph

2
@Deduplicator: Tatsächlich führt die iostream-Bibliothek standardmäßig keine Pufferung durch std::coutund übergibt stattdessen E / A an die stdio-Implementierung (die ihre eigenen Puffermechanismen verwendet). Insbesondere wenn eine Verbindung zu einem interaktiven Terminal besteht (was als solches bekannt ist), wird beim Schreiben standardmäßig nie eine vollständig gepufferte Ausgabe angezeigt std::cout. Sie müssen die Synchronisation mit stdio explizit deaktivieren, wenn die iostream-Bibliothek ihre eigenen Puffermechanismen für verwenden soll std::cout.

6
@KonradRudolph: Eigentlich printfmüssen die Streams hier nicht gespült werden. In einem allgemeinen Anwendungsfall (Ausgabe in Datei umgeleitet) werden Sie normalerweise feststellen, dass diese printfAnweisung nicht gelöscht wird. Nur wenn der Ausgang zeilengepuffert oder ungepuffert ist, printflöst der Trigger einen Flush aus.

2
@PeterCordes: Richtig, Sie können nicht mit nicht geleerten Ausgabepuffern blockieren, aber Sie können auf die Überraschung stoßen, dass das Programm Ihre Eingabe akzeptiert und fortgesetzt hat, ohne die erwartete Ausgabe anzuzeigen. Ich weiß das, weil ich Gelegenheit hatte, eine "Hilfe, mein Programm hängt während der Eingabe, aber ich kann nicht herausfinden warum!" Zu debuggen. das hatte einem anderen Entwickler für ein paar Tage Passungen gegeben.

2
@PeterCordes: Das Argument, das ich vorbringe, lautet "schreibe, was du meinst" - Zeilenumbrüche sind angemessen, wenn du meinst, dass die Ausgabe irgendwann verfügbar sein soll, und endl ist angemessen, wenn du meinst, dass die Ausgabe sofort verfügbar sein soll.

53

Es ist berühmt, dass Sie in C ++ für das bezahlen, was Sie essen. Wofür bezahle ich in diesem Fall?

Das ist einfach. Sie bezahlen für std::cout. "Sie bezahlen nur für das, was Sie essen" bedeutet nicht "Sie bekommen immer die besten Preise". Klar printfist billiger. Man kann argumentieren, dass dies std::coutsicherer und vielseitiger ist, weshalb die höheren Kosten gerechtfertigt sind (es kostet mehr, bietet aber mehr Wert), aber das geht am eigentlichen Punkt vorbei. Sie verwenden nicht printf, Sie verwenden std::cout, also zahlen Sie für die Verwendung std::cout. Sie zahlen nicht für die Verwendung printf.

Ein gutes Beispiel sind virtuelle Funktionen. Virtuelle Funktionen haben einige Laufzeitkosten und Platzanforderungen - aber nur, wenn Sie sie tatsächlich verwenden. Wenn Sie keine virtuellen Funktionen verwenden, zahlen Sie nichts.

Ein paar Bemerkungen

  1. Selbst wenn C ++ - Code mehr Assembly-Anweisungen ergibt, handelt es sich immer noch um eine Handvoll Anweisungen, und der Leistungsaufwand wird wahrscheinlich durch tatsächliche E / A-Vorgänge in den Schatten gestellt.

  2. Tatsächlich ist es manchmal sogar besser als "In C ++ zahlen Sie für das, was Sie essen". Beispielsweise kann der Compiler ableiten, dass ein virtueller Funktionsaufruf unter bestimmten Umständen nicht erforderlich ist, und diesen in einen nicht virtuellen Aufruf umwandeln. Das bedeutet, dass Sie möglicherweise virtuelle Funktionen kostenlos erhalten . Ist das nicht toll?


6
Sie erhalten keine kostenlosen virtuellen Funktionen. Sie müssen immer noch die Kosten dafür bezahlen, dass Sie sie zuerst schreiben und dann die Transformation Ihres Codes durch den Compiler debuggen, wenn sie nicht Ihrer Vorstellung entspricht, was er tun sollte.
Alephzero

2
@alephzero Ich bin mir nicht sicher, ob es besonders wichtig ist, Entwicklungskosten mit Leistungskosten zu vergleichen.

Eine großartige Gelegenheit für ein verschwendetes Wortspiel ... Sie hätten das Wort "Kalorien" anstelle von "Preis" verwenden können. Daraus könnte man sagen, dass C ++ dicker als C ist. Oder zumindest ... der spezifische Code in Frage (ich bin gegen C ++ zugunsten von C voreingenommen, daher kann ich nicht fair darüber hinausgehen). Ach. @Bilkokuya Es ist vielleicht nicht in allen Fällen relevant, aber es ist sicherlich etwas, das man nicht ignorieren sollte. Somit ist es insgesamt relevant.
Pryftan

46

Die "Assembly-Liste für printf" ist NICHT für printf, sondern für Puts (Art der Compiler-Optimierung?); printf ist viel komplexer als Puts ... nicht vergessen!


13
Dies ist bislang die beste Antwort, da alle anderen an einem roten Hering über std::coutdie Einbauten hängen bleiben, die in der Baugruppenliste nicht sichtbar sind.
Konrad Rudolph

12
Die Assembly-Liste ist für einen Aufruf von gedacht puts , der mit einem Aufruf von identisch ist, printfwenn Sie nur eine einzelne Formatzeichenfolge und null zusätzliche Argumente übergeben. (außer es wird auch eine geben, xor %eax,%eaxweil wir null FP-Argumente in Registern an eine variable Funktion übergeben.) Keine dieser Implementierungen ist nur die Übergabe eines Zeigers auf eine Zeichenfolge an die Bibliotheksfunktion. Aber ja, die Optimierung printfauf putsist etwas, was gcc für Formate tut, die nur haben "%s"oder wenn es keine Konvertierungen gibt, und die Zeichenfolge endet mit einer neuen Zeile.
Peter Cordes

45

Ich sehe hier einige gültige Antworten, aber ich werde ein bisschen mehr ins Detail gehen.

Wechseln Sie zur folgenden Zusammenfassung, um die Antwort auf Ihre Hauptfrage zu erhalten, wenn Sie nicht die gesamte Textwand durchgehen möchten.


Abstraktion

Wofür bezahle ich in diesem Fall?

Sie bezahlen für die Abstraktion . Die Möglichkeit, einfacheren und menschlicheren Code zu schreiben, ist mit Kosten verbunden. In C ++, einer objektorientierten Sprache, ist fast alles ein Objekt. Wenn Sie ein Objekt verwenden, passieren immer drei Dinge unter der Haube:

  1. Objekterstellung, im Grunde Speicherzuordnung für das Objekt selbst und seine Daten.
  2. Objektinitialisierung (normalerweise über eine init()Methode). Normalerweise erfolgt die Speicherzuweisung als erstes in diesem Schritt unter der Haube.
  3. Objektzerstörung (nicht immer).

Sie sehen es nicht im Code, aber jedes Mal, wenn Sie ein Objekt verwenden, müssen alle drei oben genannten Dinge irgendwie passieren. Wenn Sie alles manuell machen würden, wäre der Code offensichtlich viel länger.

Jetzt kann die Abstraktion effizient durchgeführt werden, ohne zusätzlichen Aufwand zu verursachen: Methoden-Inlining und andere Techniken können sowohl von Compilern als auch von Programmierern verwendet werden, um Abstraktionskosten zu entfernen. Dies ist jedoch nicht der Fall.

Was passiert wirklich in C ++?

Hier ist es, aufgeschlüsselt:

  1. Die std::ios_baseKlasse wird initialisiert. Dies ist die Basisklasse für alles, was mit E / A zu tun hat.
  2. Das std::coutObjekt wird initialisiert.
  3. Ihre Zeichenfolge wird geladen und an übergeben. Dies ist std::__ostream_insert(wie Sie bereits anhand des Namens herausgefunden haben) eine Methode std::cout(im Grunde der <<Operator), mit der dem Stream eine Zeichenfolge hinzugefügt wird.
  4. cout::endlwird auch an übergeben std::__ostream_insert.
  5. __std_dso_handlewird an übergeben __cxa_atexit, eine globale Funktion, die für das "Reinigen" vor dem Beenden des Programms verantwortlich ist. __std_dso_handleselbst wird von dieser Funktion aufgerufen, um die Zuordnung und Zerstörung verbleibender globaler Objekte aufzuheben.

Also mit C == nichts bezahlen?

Im C-Code werden nur sehr wenige Schritte ausgeführt:

  1. Ihre Zeichenfolge wird geladen und putsüber das ediRegister weitergeleitet.
  2. puts wird gerufen.

Keine Objekte irgendwo, daher muss nichts initialisiert / zerstört werden.

Dies bedeutet jedoch nicht, dass Sie für nichts in C "bezahlen" . Sie zahlen immer noch für die Abstraktion, und auch die Initialisierung der C-Standardbibliothek und die dynamische Auflösung der printfFunktion (oder tatsächlich puts, die vom Compiler optimiert wird, da Sie keine Formatzeichenfolge benötigen) finden immer noch unter der Haube statt.

Wenn Sie dieses Programm in reiner Assemblierung schreiben würden, würde es ungefähr so ​​aussehen:

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

Dies führt im Grunde nur zum Aufrufen des write Systemaufrufs, gefolgt vom exitSystemaufruf. Nun dies wäre das absolute Minimum , das Gleiche zu erreichen.


Zusammenfassen

C ist weitaus einfacher und stellt nur das Nötigste dar, das benötigt wird, und überlässt dem Benutzer die volle Kontrolle, der in der Lage ist, im Grunde alles zu optimieren und anzupassen, was er will. Sie weisen den Prozessor an, eine Zeichenfolge in ein Register zu laden und dann eine Bibliotheksfunktion aufzurufen, um diese Zeichenfolge zu verwenden. C ++ hingegen ist viel komplexer und abstrakter . Dies hat einen enormen Vorteil beim Schreiben von kompliziertem Code und ermöglicht ein einfacheres Schreiben und einen benutzerfreundlicheren Code, ist jedoch offensichtlich mit Kosten verbunden. In solchen Fällen wird die Leistung in C ++ im Vergleich zu C immer einen Nachteil haben, da C ++ mehr bietet, als für die Ausführung solcher grundlegenden Aufgaben erforderlich ist, und somit mehr Overhead verursacht .

Beantwortung Ihrer Hauptfrage :

Zahle ich für das, was ich nicht esse?

In diesem speziellen Fall ja . Sie nutzen nichts, was C ++ mehr zu bieten hat als C, aber das liegt nur daran, dass dieser einfache Code nichts enthält, bei dem C ++ Ihnen helfen könnte: Es ist so einfach, dass Sie C ++ wirklich überhaupt nicht benötigen.


Oh, und noch eine Sache!

Die Vorteile von C ++ sind auf den ersten Blick vielleicht nicht offensichtlich, da Sie ein sehr einfaches und kleines Programm geschrieben haben. Schauen Sie sich jedoch ein etwas komplexeres Beispiel an und sehen Sie den Unterschied (beide Programme machen genau das Gleiche):

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

Hoffentlich können Sie klar sehen, was ich hier meine. Beachten Sie auch, wie Sie in C den Speicher auf einer niedrigeren Ebene verwalten müssen mallocund freewie Sie bei der Indizierung und Größe vorsichtiger sein müssen und wie Sie bei der Eingabe und beim Drucken sehr spezifisch sein müssen.


27

Zunächst gibt es einige Missverständnisse. Erstens führt das C ++ - Programm nicht zu 22 Anweisungen, sondern eher zu 22.000 (ich habe diese Nummer aus meinem Hut gezogen, aber sie befindet sich ungefähr im Baseballstadion). Auch der C-Code nicht zu 9 Anweisungen. Das sind nur die, die du siehst.

Der C-Code ruft nach vielen Vorgängen, die Sie nicht sehen, eine Funktion aus der CRT auf (die normalerweise, aber nicht unbedingt als gemeinsam genutzte Bibliothek vorhanden ist) und überprüft dann nicht den Rückgabewert oder das Handle Fehler und rettet. Je nach Compiler und Optimierungseinstellungen es wirklich nicht einmal nennen , printfaber puts, oder etwas noch primitiv.
Sie hätten auch mehr oder weniger dasselbe Programm (mit Ausnahme einiger unsichtbarer Init-Funktionen) in C ++ schreiben können, wenn Sie nur dieselbe Funktion auf dieselbe Weise aufgerufen hätten. Oder, wenn Sie sehr korrekt sein möchten, wird dieselbe Funktion vorangestellt std::.

Der entsprechende C ++ - Code ist in Wirklichkeit überhaupt nicht dasselbe. Während das Ganze als <iostream>bekanntes fettes hässliches Schwein bekannt ist, das kleinen Programmen einen immensen Overhead hinzufügt (in einem "echten" Programm merkt man nicht so viel), ist eine etwas gerechtere Interpretation, dass es schrecklich ist viele Sachen, die du nicht siehst und die einfach funktionieren . Einschließlich, aber nicht beschränkt auf die magische Formatierung so ziemlich aller zufälligen Dinge, einschließlich verschiedener Zahlenformate und Gebietsschemas und so weiter, sowie Pufferung und ordnungsgemäße Fehlerbehandlung. Fehlerbehandlung? Nun ja, raten Sie mal, die Ausgabe eines Strings kann tatsächlich fehlschlagen, und im Gegensatz zum C-Programm würde das C ++ - Programm dies nicht stillschweigend ignorieren. Überlegen wasstd::ostreamtut unter der Haube, und ohne dass es jemand merkt, ist es eigentlich ziemlich leicht. Nicht so, als würde ich es benutzen, weil ich die Stream-Syntax aus Leidenschaft hasse. Trotzdem ist es ziemlich großartig, wenn man bedenkt, was es tut.

Aber sicher ist C ++ insgesamt nicht so effizient wie C sein kann. Es kann nicht so effizient sein , da es nicht dasselbe ist , und es ist nicht zu tun , die gleiche Sache. Wenn nichts anderes, generiert C ++ Ausnahmen (und Code zum Generieren, Behandeln oder Fehlschlagen) und gibt einige Garantien, die C nicht gibt. Ein C ++ - Programm muss also unbedingt etwas größer sein. Im Großen und Ganzen spielt dies jedoch keine Rolle. Im Gegenteil, für echte Programme habe ich nicht selten festgestellt, dass C ++ eine bessere Leistung erbringt, da es aus dem einen oder anderen Grund günstigere Optimierungen zu bieten scheint. Fragen Sie mich nicht, warum ich es nicht wissen würde.

Wenn Sie anstelle von Feuer und Vergessen der Hoffnung auf das Beste C-Code schreiben möchten, der korrekt ist (dh Sie suchen tatsächlich nach Fehlern und das Programm verhält sich bei Vorhandensein von Fehlern korrekt), ist der Unterschied gering. falls vorhanden.


16
Sehr gute Antwort, außer dass diese Behauptung: "Aber sicher, C ++ ist insgesamt nicht so effizient wie C sein kann" einfach falsch ist. C ++ kann so effizient wie C sein, und eine ausreichend hohe Stufencode kann mehr effizienter als äquivalente C - Code. Ja, C ++ hat einen gewissen Overhead, da Ausnahmen behandelt werden müssen, aber bei modernen Compilern ist dieser Overhead im Vergleich zu Leistungssteigerungen durch bessere kostenlose Abstraktionen vernachlässigbar.
Konrad Rudolph

Wenn ich es richtig verstanden habe, std::coutwirft es auch Ausnahmen?
Saher

6
@ Saher: Ja, nein, vielleicht. std::coutist ein std::basic_ostreamund das kann man werfen, und es kann ansonsten auftretende Ausnahmen erneut auslösen, wenn es dafür konfiguriert ist , oder es kann Ausnahmen schlucken. Die Sache ist, Dinge können fehlschlagen, und C ++ sowie die C ++ - Standardbibliothek sind (meistens) so aufgebaut, dass Fehler nicht leicht unbemerkt bleiben. Dies ist ein Ärger und ein Segen (aber mehr Segen als Ärger). C hingegen zeigt Ihnen nur den Mittelfinger. Sie überprüfen keinen Rückkehrcode, Sie wissen nie, was passiert ist.
Damon

1
@KonradRudolph: Richtig, das ist es, worauf ich hinweisen wollte: "Ich habe nicht selten festgestellt, dass C ++ eine bessere Leistung erbringt, weil es aus dem einen oder anderen Grund günstigere Optimierungen zu ermöglichen scheint. Fragen Sie mich nicht, warum insbesondere." . Es ist nicht sofort klar warum, aber nicht selten optimiert es einfach besser. Warum auch immer. Sie würden denken, dass es dem Optimierer egal ist, aber es ist nicht so.
Damon

22

Sie bezahlen für einen Fehler. In den 80er Jahren, als Compiler nicht gut genug waren, um Formatzeichenfolgen zu überprüfen, wurde das Überladen von Operatoren als ein guter Weg angesehen, um einen gewissen Anschein von Typensicherheit während io zu erzwingen. Alle Bannerfunktionen sind jedoch von Anfang an entweder schlecht implementiert oder konzeptionell bankrott:

<iomanip>

Der abstoßendste Teil der C ++ - Stream-API ist die Existenz dieser Formatierungsheaderbibliothek. Es ist nicht nur zustandsbehaftet und hässlich und fehleranfällig, sondern koppelt auch die Formatierung an den Stream.

Angenommen, Sie möchten eine Zeile mit einem 8-stelligen, mit Nullen gefüllten hexadezimalen Int ohne Vorzeichen ausdrucken, gefolgt von einem Leerzeichen, gefolgt von einem Doppel mit 3 Dezimalstellen. Mit <cstdio>können Sie eine prägnante Formatzeichenfolge lesen. Mit <ostream>müssen Sie den alten Status speichern, die Ausrichtung auf rechts setzen, das Füllzeichen festlegen, die Füllbreite festlegen, die Basis auf hex setzen, die Ganzzahl ausgeben, den gespeicherten Status wiederherstellen (andernfalls verschmutzt Ihre Ganzzahlformatierung Ihre Gleitkommaformatierung) und das Leerzeichen ausgeben Setzen Sie die Notation auf fest, stellen Sie die Genauigkeit ein, geben Sie die doppelte und die neue Zeile aus und stellen Sie dann die alte Formatierung wieder her.

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

Überlastung des Bedieners

<iostream> ist das Aushängeschild, wie man das Überladen von Operatoren nicht verwendet:

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

Performance

std::coutist um ein Vielfaches langsamer printf(). Die grassierende Featuritis und der virtuelle Versand fordern ihren Tribut.

Gewindesicherheit

Beide <cstdio>und <iostream>sind insofern threadsicher, als jeder Funktionsaufruf atomar ist. Aber printf()pro Anruf wird viel mehr erledigt. Wenn Sie das folgende Programm mit der <cstdio>Option ausführen , wird nur eine Zeile von angezeigt f. Wenn Sie <iostream>auf einem Multicore-Computer verwenden, sehen Sie wahrscheinlich etwas anderes.

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

Die Antwort auf dieses Beispiel ist, dass die meisten Leute Disziplin üben, um niemals aus mehreren Threads in einen einzelnen Dateideskriptor zu schreiben. Nun, in diesem Fall müssen Sie beachten, dass dies <iostream>hilfreich ist, um jeden <<einzelnen zu sperren >>. Während in <cstdio>, werden Sie nicht so oft sperren, und Sie haben sogar die Möglichkeit, nicht zu sperren.

<iostream> verbraucht mehr Sperren, um ein weniger konsistentes Ergebnis zu erzielen.


2
Die meisten Implementierungen von printf verfügen über eine äußerst nützliche Funktion zur Lokalisierung: nummerierte Parameter. Wenn Sie eine Ausgabe in zwei verschiedenen Sprachen (wie Englisch und Französisch) erstellen müssen und die Wortreihenfolge unterschiedlich ist, können Sie dasselbe printf mit einer anderen Formatierungszeichenfolge verwenden und Parameter in unterschiedlicher Reihenfolge drucken.
Gnasher729

2
Diese zustandsbehaftete Formatierung von Streams muss so viele schwer zu findende Fehler verursacht haben, dass ich nicht weiß, was ich sagen soll. Gute Antwort. Würde mehr als einmal upvoten, wenn ich könnte.
Mathreadler

6
" std::coutIst um ein Vielfaches langsamer printf()" - Diese Behauptung wird im ganzen Netz wiederholt, ist aber seit Ewigkeiten nicht mehr wahr. Moderne IOstream-Implementierungen arbeiten auf Augenhöhe mit printf. Letzterer führt auch intern einen virtuellen Versand durch, um gepufferte Streams und lokalisierte E / A zu verarbeiten (vom Betriebssystem ausgeführt, aber dennoch ausgeführt).
Konrad Rudolph

3
@ KevinZ Und das ist großartig, aber es ist ein Benchmarking eines einzelnen, spezifischen Aufrufs, der die spezifischen Stärken von fmt zeigt (viele verschiedene Formate in einer einzigen Zeichenfolge). Bei einer typischeren Verwendung verringert sich der Unterschied zwischen printfund cout. Übrigens gibt es auf dieser Website unzählige solcher Benchmarks.
Konrad Rudolph

3
@KonradRudolph Das stimmt auch nicht. Mikrobenchmarks unterschätzen häufig die Kosten für Aufblähen und Indirektion, da sie bestimmte begrenzte Ressourcen (Register, Icache, Speicher, Verzweigungsvorhersagen) nicht erschöpfen, wenn ein echtes Programm dies tut. Wenn Sie auf "typischere Verwendung" anspielen, heißt das im Grunde, dass Sie an anderer Stelle erheblich mehr Blähungen haben, was in Ordnung ist, aber nicht zum Thema gehört. Wenn Sie keine Leistungsanforderungen haben, müssen Sie meiner Meinung nach nicht in C ++ programmieren.
KevinZ

18

Zusätzlich zu dem, was alle anderen Antworten gesagt haben,
gibt es auch die Tatsache , dass std::endlist nicht das gleiche wie '\n'.

Dies ist ein leider weit verbreitetes Missverständnis. std::endlbedeutet nicht "neue Zeile",
sondern "neue Zeile drucken und dann den Stream leeren ". Spülen ist nicht billig!

Wenn Sie die Unterschiede zwischen printfund std::coutfür einen Moment vollständig ignorieren , um funktional mit Ihrem C-Beispiel identisch zu sein, sollte Ihr C ++ - Beispiel folgendermaßen aussehen:

#include <iostream>

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

Und hier ist ein Beispiel dafür, wie Ihre Beispiele aussehen sollten, wenn Sie das Spülen einschließen.

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

Wenn Sie Code vergleichen, sollten Sie immer darauf achten, dass Sie Gleiches für Gleiches vergleichen und die Auswirkungen der Funktionsweise Ihres Codes verstehen. Manchmal sind selbst die einfachsten Beispiele komplizierter, als manche Leute glauben.


Tatsächlich verwenden std::endl ist das funktionelle Äquivalent eine neue Zeile in eine Zeile-gepufferte Stdio Strom zu schreiben. stdoutInsbesondere muss es entweder zeilengepuffert oder ungepuffert sein, wenn es an ein interaktives Gerät angeschlossen ist. Ich glaube, Linux besteht auf der zeilengepufferten Option.

Tatsächlich verfügt die iostream-Bibliothek nicht über einen zeilengepufferten Modus. Um den Effekt der std::endlZeilenpufferung zu erzielen, müssen Sie genau Zeilenumbrüche ausgeben.

@ Hurkyl bestehen? Was nützt es dann setvbuf(3)? Oder wollen Sie damit sagen, dass der Standardwert zeilengepuffert ist? Zu Ihrer Information: Normalerweise sind alle Dateien blockgepuffert. Wenn sich ein Stream auf ein Terminal bezieht (wie es normalerweise bei stdout der Fall ist), wird er zeilengepuffert. Der Standardfehlerstrom stderr ist standardmäßig immer ungepuffert.
Pryftan

printfWird nicht automatisch gespült, wenn ein Zeilenumbruch auftritt?
Bool3Max

1
@ bool3max Das würde mir nur sagen, was meine Umgebung tut, es könnte in anderen Umgebungen anders sein. Selbst wenn es sich in allen gängigen Implementierungen gleich verhält, heißt das nicht, dass es irgendwo einen Randfall gibt. Deshalb ist der Standard so wichtig - der Standard schreibt vor, ob etwas für alle Implementierungen gleich sein muss oder ob es zwischen den Implementierungen variieren darf.
Pharap

16

Obwohl die vorhandenen technischen Antworten korrekt sind, denke ich, dass die Frage letztendlich auf diesem Missverständnis beruht:

Es ist berühmt, dass Sie in C ++ für das bezahlen, was Sie essen.

Dies ist nur ein Marketinggespräch der C ++ - Community. (Um fair zu sein, gibt es in jeder Sprachgemeinschaft Marketinggespräche.) Es bedeutet nichts Konkretes, auf das Sie sich ernsthaft verlassen können.

"Sie zahlen für das, was Sie verwenden" soll bedeuten, dass eine C ++ - Funktion nur dann Overhead hat, wenn Sie diese Funktion verwenden. Aber die Definition von „einem Merkmal“ ist nicht unendlich körnig. Oft werden Sie Features aktivieren, die mehrere Aspekte haben, und obwohl Sie nur eine Teilmenge dieser Aspekte benötigen, ist es für die Implementierung oft nicht praktikabel oder möglich, die Funktion teilweise einzubringen.

Im Allgemeinen streben viele (wenn auch wohl nicht alle) Sprachen nach Effizienz und unterschiedlichem Erfolg. C ++ ist irgendwo auf der Skala, aber es gibt nichts Besonderes oder Magisches an seinem Design, das es ihm ermöglichen würde, dieses Ziel perfekt zu erreichen.


1
Ich kann mir nur zwei Dinge überlegen, wo Sie für etwas bezahlen, das Sie nicht verwenden: Ausnahmen und RTTI. Und ich glaube nicht, dass es Marketinggespräche sind. C ++ ist im Grunde ein leistungsfähigeres C, das auch "nicht für das bezahlen, was Sie verwenden" bedeutet.
Rakete1111

2
@ Rakete1111 Es ist seit langem bekannt, dass Ausnahmen keine Kosten verursachen, wenn sie nicht ausgelöst werden. Wenn Ihr Programm konsistent ausgelöst wird, sollte es neu gestaltet werden. Wenn die Fehlerbedingung außerhalb Ihrer Kontrolle liegt, sollten Sie die Bedingung mit einem Bool überprüfen, der die Überprüfung der Integrität zurückgibt, bevor Sie die Methode aufrufen, die darauf beruht, dass die Bedingung falsch ist.
Schulmaster

1
@schulmaster: Ausnahmen können Designbeschränkungen auferlegen, wenn in C ++ geschriebener Code mit in anderen Sprachen geschriebenem Code interagieren muss, da nicht lokale Steuerübertragungen nur dann reibungslos über Module hinweg funktionieren können, wenn die Module wissen, wie sie miteinander koordinieren können.
Supercat

1
(obwohl wohl nicht alle) Sprachen streben danach, effizient zu sein . Auf jeden Fall nicht alle: Esoterische Programmiersprachen streben danach, neuartig / interessant und nicht effizient zu sein. esolangs.org . Einige von ihnen, wie BrainFuck, sind bekanntermaßen ineffizient. Oder zum Beispiel die Shakespeare-Programmiersprache mit einer Mindestgröße von 227 Byte (Codegolf) zum Drucken aller Ganzzahlen . Von den Sprachen, die für den Produktionsgebrauch bestimmt sind, zielen die meisten auf Effizienz ab, aber einige (wie Bash) zielen hauptsächlich auf Bequemlichkeit ab und sind bekanntermaßen langsam.
Peter Cordes

2
Nun, es ist Marketing, aber es ist fast vollständig wahr. Sie können sich daran halten <cstdio>und nicht einschließen <iostream>, genau wie Sie damit kompilieren können -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables.
KevinZ

11

Die Eingabe- / Ausgabefunktionen in C ++ sind elegant geschrieben und so gestaltet, dass sie einfach zu verwenden sind. In vielerlei Hinsicht sind sie ein Schaufenster für die objektorientierten Funktionen in C ++.

Sie geben zwar im Gegenzug ein wenig Leistung auf, aber das ist vernachlässigbar im Vergleich zu der Zeit, die Ihr Betriebssystem benötigt, um die Funktionen auf einer niedrigeren Ebene auszuführen.

Sie können jederzeit auf die Funktionen im C-Stil zurückgreifen, da diese Teil des C ++ - Standards sind, oder die Portabilität ganz aufgeben und direkte Aufrufe an Ihr Betriebssystem verwenden.


23
"Die Eingabe- / Ausgabefunktionen in C ++ sind abscheuliche Monster, die Schwierigkeiten haben, ihre cthulianische Natur hinter einem dünnen Furnier von Nützlichkeit zu verbergen. In vielerlei Hinsicht sind sie ein Beispiel dafür, wie man modernen C ++ - Code nicht entwirft." Wäre wahrscheinlich genauer.
user673679

3
@ user673679: Sehr wahr. Das große Problem bei C ++ - E / A-Streams ist, was darunter liegt: Es ist wirklich viel Komplexität im Gange, und jeder, der sich jemals mit ihnen befasst hat (ich beziehe mich auf std::basic_*streamabwärts), kennt die eingehenden Probleme. Sie waren so konzipiert, dass sie allgemein gehalten und durch Vererbung erweitert wurden. Aber aufgrund ihrer Komplexität (es gibt buchstäblich Bücher über iostreams) hat das letztendlich niemand getan, so dass neue Bibliotheken nur dafür geboren wurden (z. B. Boost, ICU usw.). Ich bezweifle, dass wir jemals aufhören werden, für diesen Fehler zu bezahlen.
Edmz

1

Wie Sie in anderen Antworten gesehen haben, zahlen Sie, wenn Sie allgemeine Bibliotheken verknüpfen und komplexe Konstruktoren aufrufen. Hier gibt es keine besondere Frage, eher eine Beschwerde. Ich werde auf einige reale Aspekte hinweisen:

  1. Barne hatte ein zentrales Designprinzip, bei dem Effizienz niemals ein Grund dafür war, in C statt in C ++ zu bleiben. Das heißt, man muss vorsichtig sein, um diese Wirkungsgrade zu erzielen, und es gibt gelegentliche Wirkungsgrade, die immer funktionierten, aber nicht „technisch“ innerhalb der C-Spezifikation lagen. Beispielsweise wurde das Layout von Bitfeldern nicht wirklich angegeben.

  2. Versuchen Sie, durch den Strom zu schauen. Oh mein Gott, es ist aufgebläht! Es würde mich nicht wundern, dort einen Flugsimulator zu finden. Sogar stdlibs printf () läuft normalerweise ungefähr 50K. Dies sind keine faulen Programmierer: Die Hälfte der Druckgröße hatte mit indirekten Präzisionsargumenten zu tun, die die meisten Leute nie verwenden. Fast jede wirklich eingeschränkte Prozessorbibliothek erstellt anstelle von printf einen eigenen Ausgabecode.

  3. Die Vergrößerung bietet normalerweise eine geschlossenere und flexiblere Erfahrung. Analog dazu verkauft ein Verkaufsautomat eine Tasse kaffeeähnliche Substanz für ein paar Münzen, und die gesamte Transaktion dauert weniger als eine Minute. Um in ein gutes Restaurant zu kommen, muss man einen Tisch decken, sitzen, bestellen, warten, eine schöne Tasse bekommen, eine Rechnung bekommen, in den von Ihnen gewählten Formen bezahlen, ein Trinkgeld hinzufügen und sich auf dem Weg nach draußen einen guten Tag wünschen. Es ist eine andere Erfahrung und bequemer, wenn Sie mit Freunden zu einer komplexen Mahlzeit kommen.

  4. Die Leute schreiben immer noch ANSI C, obwohl selten K & R C. Ich habe die Erfahrung gemacht, dass wir es immer mit einem C ++ - Compiler kompilieren, indem wir ein paar Konfigurationsänderungen vornehmen, um das Ziehen einzuschränken. Es gibt gute Argumente für andere Sprachen: Go entfernt den polymorphen Overhead und den verrückten Präprozessor ;; Es gab einige gute Argumente für eine intelligentere Feldverpackung und ein intelligenteres Speicherlayout. IMHO Ich denke, jedes Sprachdesign sollte mit einer Auflistung von Zielen beginnen, ähnlich wie das Zen von Python .

Es war eine lustige Diskussion. Sie fragen sich, warum Sie keine magisch kleinen, einfachen, eleganten, vollständigen und flexiblen Bibliotheken haben können?

Es gibt keine Antwort. Es wird keine Antwort geben. Das ist die Antwort.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.