En C ++, est-ce que je paie ce que je ne mange pas?

170

Considérons les exemples Hello World suivants en C et 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;
}

Quand je les compile dans godbolt à l'assemblage, la taille du code C n'est que de 9 lignes ( gcc -O3):

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

Mais la taille du code C ++ est de 22 lignes ( 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

... ce qui est beaucoup plus grand.

Il est connu qu'en C ++, vous payez ce que vous mangez. Alors, dans ce cas, qu'est-ce que je paie?

Saher
la source
3
Les commentaires ne sont pas destinés à une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew
26
Jamais entendu le terme eatassocié à C ++. Je crois que vous voulez dire: "Vous ne payez que ce que vous utilisez "?
Giacomo Alzetta
7
@GiacomoAlzetta, ... c'est un langage familier, appliquant le concept d'un buffet à volonté. Utiliser le terme plus précis est certainement préférable avec un public mondial, mais en tant que anglophone, le titre me semble logique.
Charles Duffy
5
@ trolley813 Les fuites de mémoire n'ont rien à voir avec le devis et la question OP. Le point de «Vous ne payez que pour ce que vous utilisez» / «Vous ne payez pas pour ce que vous n'utilisez pas» est de dire qu'aucune performance n'est atteinte si vous n'utilisez pas une fonctionnalité / abstraction spécifique. Les fuites de mémoire n'ont rien à voir avec cela, et cela montre seulement que le terme eatest plus ambigu et doit être évité.
Giacomo Alzetta

Réponses:

60

Ce que vous payez, c'est d'appeler une bibliothèque lourde (pas aussi lourde que l'impression dans la console). Vous initialisez un ostreamobjet. Il y a du stockage caché. Ensuite, vous appelez std::endlce qui n'est pas synonyme de \n. La iostreambibliothèque vous aide à ajuster de nombreux paramètres et à mettre le fardeau sur le processeur plutôt que sur le programmeur. C'est ce que vous payez.

Passons en revue le code:

.LC0:
        .string "Hello world"
main:

Initialisation d'un objet 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)

Appel à coutnouveau pour imprimer une nouvelle ligne et rincer

    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

Initialisation du stockage statique:

_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

Aussi, il est essentiel de faire la distinction entre la langue et la bibliothèque.

BTW, ce n'est qu'une partie de l'histoire. Vous ne savez pas ce qui est écrit dans les fonctions que vous appelez.

Arash
la source
5
Comme note supplémentaire, des tests approfondis montreront que l'ajout d'un programme C ++ avec "ios_base :: sync_with_stdio (false);" et "cin.tie (NULL);" rendra cout plus rapide que printf (Printf a une surcharge de chaîne de format). Le premier élimine la surcharge liée à la vérification des cout; printf; coutécritures dans l'ordre (puisqu'ils ont leurs propres tampons). Le second va désynchroniser coutet cin, ce qui amènera cout; cinpotentiellement à demander à l'utilisateur des informations en premier. Le rinçage le forcera à se synchroniser uniquement lorsque vous en aurez réellement besoin.
Nicholas Pipitone
Salut Nicolas, merci beaucoup d'avoir ajouté ces notes utiles.
Arash
«il est essentiel de faire la distinction entre le langage et la bibliothèque»: Eh bien oui, mais la bibliothèque standard livrée avec un langage est la seule disponible partout, c'est donc celle qui est utilisée partout (et oui, la bibliothèque standard C fait partie de la spécification C ++, donc il peut être utilisé quand on le souhaite). Quant à "Vous ne savez pas ce qui est écrit dans les fonctions que vous appelez": Vous pouvez créer un lien statique si vous voulez vraiment savoir, et en effet le code appelant que vous examinez n'est probablement pas pertinent.
Peter - Réintègre Monica
211

Alors, dans ce cas, qu'est-ce que je paie?

std::coutest plus puissant et compliqué que printf. Il prend en charge des éléments tels que les paramètres régionaux, les indicateurs de mise en forme avec état, etc.

Si vous n'en avez pas besoin, utilisez std::printfou std::puts- ils sont disponibles dans <cstdio>.


Il est connu qu'en C ++, vous payez ce que vous mangez.

Je tiens également à préciser que C ++ ! = La bibliothèque standard C ++. La bibliothèque standard est censée être polyvalente et "assez rapide", mais elle sera souvent plus lente qu'une implémentation spécialisée de ce dont vous avez besoin.

D'autre part, le langage C ++ s'efforce de rendre possible l'écriture de code sans payer de coûts cachés supplémentaires inutiles (par exemple, opt-in virtual, pas de garbage collection).

Vittorio Romeo
la source
4
+1 pour dire que la bibliothèque standard est censée être polyvalente et "assez rapide", mais elle sera souvent plus lente qu'une implémentation spécialisée de ce dont vous avez besoin. Beaucoup semblent utiliser allègrement les composants STL sans tenir compte des implications en termes de performances par rapport au déploiement des vôtres.
Craig Estey
7
@Craig OTOH de nombreuses parties de la bibliothèque standard sont généralement plus rapides et plus correctes que ce que l'on pourrait généralement produire à la place.
Peter - Réintègre Monica
2
@ PeterA.Schneider OTOH, lorsque la version STL est 20x-30x plus lente, rouler la vôtre est une bonne chose. Voir ma réponse ici: codereview.stackexchange.com/questions/191747 / ... Là-dedans, d'autres ont également suggéré [au moins une partie] de rouler le vôtre.
Craig Estey
1
@CraigEstey Un vecteur est (mis à part l'allocation dynamique initiale qui peut être significative, selon la quantité de travail qui sera éventuellement effectuée avec une instance donnée) pas moins efficace qu'un tableau C; il est conçu pour ne pas l'être. Il faut prendre soin de ne pas le copier autour, de réserver suffisamment d'espace au départ, etc. En ce qui concerne votre exemple lié: Oui, un vecteur de vecteurs entraînera (à moins d'être optimisé à l'écart) une indirection supplémentaire par rapport à un tableau 2D, mais je suppose que l'efficacité 20x n'est pas enracinée ici mais dans l'algorithme.
Peter - Réintègre Monica
174

Vous ne comparez pas C et C ++. Vous comparez printfet std::cout, qui sont capables de différentes choses (locales, formatage avec état, etc.).

Essayez d'utiliser le code suivant à des fins de comparaison. Godbolt génère le même assemblage pour les deux fichiers (testé avec gcc 8.2, -O3).

principal 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
la source
Merci d'avoir montré un code équivalent et d'expliquer la raison.
HackSlash
134

Vos annonces comparent en effet des pommes et des oranges, mais pas pour la raison impliquée dans la plupart des autres réponses.

Vérifions ce que fait réellement votre code:

C:

  • imprimer une seule chaîne, "Hello world\n"

C ++:

  • diffuser la chaîne "Hello world"dansstd::cout
  • diffuser le std::endlmanipulateur dansstd::cout

Apparemment, votre code C ++ fait deux fois plus de travail. Pour une comparaison équitable, nous devons combiner ceci:

#include <iostream>

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

… Et tout à coup votre code d'assemblage pour mainressemble beaucoup à 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

En fait, on peut comparer les codes C et C ++ ligne par ligne, et il y a très peu de différences :

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

La seule vraie différence est qu'en C ++, nous appelons operator <<avec deux arguments ( std::coutet la chaîne). Nous pourrions supprimer même cette légère différence en utilisant un équivalent C plus proche:, fprintfqui a également un premier argument spécifiant le flux.

Cela laisse le code d'assembly pour _GLOBAL__sub_I_main, qui est généré pour C ++ mais pas C. C'est la seule vraie surcharge visible dans cette liste d'assembly (il y a plus, une surcharge invisible pour les deux langues, bien sûr). Ce code exécute une configuration unique de certaines fonctions de bibliothèque standard C ++ au démarrage du programme C ++.

Mais, comme expliqué dans d'autres réponses, la différence pertinente entre ces deux programmes ne se trouvera pas dans la sortie d'assemblage de la mainfonction, car tout le gros du travail se passe dans les coulisses.

Konrad Rudolph
la source
21
Incidemment, le runtime C doit également être configuré, et cela se produit dans une fonction appelée _startmais son code fait partie de la bibliothèque d'exécution C. En tout cas, cela se produit à la fois pour C et C ++.
Konrad Rudolph
2
@Deduplicator: En fait, par défaut, la bibliothèque iostream ne fait aucune mise en mémoire tampon std::coutet passe les E / S à l'implémentation stdio (qui utilise ses propres mécanismes de mise en mémoire tampon). En particulier, lorsque vous êtes connecté à (ce que l'on sait être) un terminal interactif, par défaut, vous ne verrez jamais une sortie entièrement mise en mémoire tampon lors de l'écriture dans std::cout. Vous devez désactiver explicitement la synchronisation avec stdio si vous souhaitez que la bibliothèque iostream utilise ses propres mécanismes de mise en mémoire tampon pour std::cout.
6
@KonradRudolph: En fait, il printfn'est pas nécessaire de vider les flux ici. En fait, dans un cas d'utilisation courant (sortie redirigée vers un fichier), vous constaterez généralement que l' printfinstruction ne se vide pas . Ce n'est que lorsque la sortie est en tampon de ligne ou sans tampon que le printfdéclenchement déclenchera un vidage.
2
@PeterCordes: Oui, vous ne pouvez pas bloquer avec des tampons de sortie non vidés, mais vous pouvez tomber dans la surprise où le programme a accepté votre entrée et a marché sans afficher la sortie attendue. Je le sais car j'ai eu l'occasion de déboguer une "Aide, mon programme se bloque pendant la saisie mais je ne comprends pas pourquoi!" qui avait donné des ajustements à un autre développeur pendant quelques jours.
2
@PeterCordes: L'argument que je fais est "écris ce que tu veux dire" - les sauts de ligne sont appropriés lorsque vous voulez que la sortie soit finalement disponible, et endl est approprié lorsque vous voulez que la sortie soit disponible immédiatement.
53

Il est connu qu'en C ++, vous payez ce que vous mangez. Alors, dans ce cas, qu'est-ce que je paie?

C'est simple. Vous payez std::cout. «Vous ne payez que ce que vous mangez» ne veut pas dire «vous obtenez toujours les meilleurs prix». Bien sûr, printfc'est moins cher. On peut soutenir que std::coutc'est plus sûr et plus polyvalent, donc son coût plus élevé est justifié (cela coûte plus cher, mais offre plus de valeur), mais cela manque le point. Vous n'utilisez pas printf, vous utilisez std::cout, donc vous payez pour l'utilisation std::cout. Vous ne payez pas pour l'utilisation printf.

Les fonctions virtuelles en sont un bon exemple. Les fonctions virtuelles ont un coût d'exécution et des exigences d'espace - mais uniquement si vous les utilisez réellement . Si vous n'utilisez pas de fonctions virtuelles, vous ne payez rien.

Quelques remarques

  1. Même si le code C ++ évalue plus d'instructions d'assemblage, il s'agit toujours d'une poignée d'instructions, et toute surcharge de performances est toujours probablement éclipsée par les opérations d'E / S réelles.

  2. En fait, parfois c'est encore mieux que "en C ++, vous payez ce que vous mangez". Par exemple, le compilateur peut déduire que l'appel de fonction virtuelle n'est pas nécessaire dans certaines circonstances et le transformer en appel non virtuel. Cela signifie que vous pouvez obtenir des fonctions virtuelles gratuitement . N'est-ce pas génial?

el.pescado
la source
6
Vous n'obtenez pas de fonctions virtuelles gratuitement. Vous devez toujours payer le coût de leur première écriture, puis du débogage de la transformation de votre code par le compilateur lorsqu'il ne correspond pas à votre idée de ce qu'il était censé faire.
alephzero
2
@alephzero Je ne suis pas sûr qu'il soit particulièrement pertinent de comparer les coûts de développement avec les coûts de performance.
Une si belle opportunité pour un jeu de mots perdu ... Vous auriez pu utiliser le mot «calories» au lieu de «prix». À partir de là, vous pourriez dire que C ++ est plus gros que C. Ou du moins ... le code spécifique en question (je suis biaisé contre C ++ en faveur de C donc je ne peux pas aller au-delà). Hélas. @Bilkokuya Ce n'est peut-être pas pertinent dans tous les cas, mais c'est certainement quelque chose qu'il ne faut pas négliger. Il est donc pertinent dans l'ensemble.
Pryftan
46

La "liste d'assemblage pour printf" n'est PAS pour printf, mais pour les put (sorte d'optimisation du compilateur?); printf est bien plus complexe que met ... n'oubliez pas!

Álvaro Gustavo López
la source
13
C'est jusqu'à présent la meilleure réponse, puisque tous les autres sont accrochés à un hareng rouge concernant std::coutles internes de, qui ne sont pas visibles dans la liste de l'assemblage.
Konrad Rudolph
12
La liste d'assembly est pour un appel à puts , qui semble identique à un appel à printfsi vous ne transmettez qu'une seule chaîne de format et zéro argument supplémentaire. (sauf qu'il y aura aussi un xor %eax,%eaxparce que nous passons zéro argument FP dans les registres à une fonction variadique.) Ni l'un ni l'autre ne sont l'implémentation, juste en passant un pointeur vers une chaîne vers la fonction de bibliothèque. Mais oui, l'optimisation printfvers putsest quelque chose que gcc fait pour les formats qui n'ont que "%s", ou quand il n'y a pas de conversion, et la chaîne se termine par une nouvelle ligne.
Peter Cordes
45

Je vois des réponses valables ici, mais je vais entrer un peu plus dans les détails.

Allez au résumé ci-dessous pour la réponse à votre question principale si vous ne voulez pas parcourir tout ce mur de texte.


Abstraction

Alors, dans ce cas, qu'est-ce que je paie?

Vous payez pour l' abstraction . Etre capable d'écrire du code plus simple et plus convivial a un coût. En C ++, qui est un langage orienté objet, presque tout est un objet. Lorsque vous utilisez un objet, trois choses principales se produiront toujours sous le capot:

  1. Création d'objet, essentiellement allocation de mémoire pour l'objet lui-même et ses données.
  2. Initialisation d'objet (généralement via une init()méthode). Habituellement, l'allocation de mémoire se produit sous le capot comme la première chose de cette étape.
  3. Destruction d'objets (pas toujours).

Vous ne le voyez pas dans le code, mais chaque fois que vous utilisez un objet, les trois choses ci-dessus doivent se produire d'une manière ou d'une autre. Si vous deviez tout faire manuellement, le code serait évidemment beaucoup plus long.

Désormais, l'abstraction peut être faite efficacement sans ajouter de surcharge: l'inclusion de méthodes et d'autres techniques peuvent être utilisées à la fois par les compilateurs et les programmeurs pour supprimer les frais généraux d'abstraction, mais ce n'est pas votre cas.

Que se passe-t-il vraiment en C ++?

Le voici, décomposé:

  1. La std::ios_baseclasse est initialisée, qui est la classe de base pour tout ce qui concerne les E / S.
  2. L' std::coutobjet est initialisé.
  3. Votre chaîne est chargée et transmise à std::__ostream_insert, qui (comme vous l'avez déjà compris par le nom) est une méthode de std::cout(essentiellement l' <<opérateur) qui ajoute une chaîne au flux.
  4. cout::endlest également passé à std::__ostream_insert.
  5. __std_dso_handleest passé à __cxa_atexit, qui est une fonction globale qui est responsable du «nettoyage» avant de quitter le programme. __std_dso_handlelui-même est appelé par cette fonction pour désallouer et détruire les objets globaux restants.

Donc utiliser C == ne paie rien?

Dans le code C, très peu d'étapes se produisent:

  1. Votre chaîne est chargée et transmise putsvia le ediregistre.
  2. puts est appelé.

Aucun objet nulle part, donc pas besoin d'initialiser / détruire quoi que ce soit.

Cela ne veut pas dire que vous n'êtes pas « payer » pour quoi que ce soit en C . Vous payez toujours pour l'abstraction, et l'initialisation de la bibliothèque standard C et la résolution dynamique de la printffonction (ou, en fait puts, qui est optimisée par le compilateur puisque vous n'avez besoin d'aucune chaîne de format) se produisent toujours sous le capot.

Si vous deviez écrire ce programme en assemblage pur, il ressemblerait à ceci:

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

Ce qui aboutit essentiellement à l'invocation de l'appel write système suivi de l' exitappel système. Maintenant, ce serait le strict minimum pour accomplir la même chose.


Résumer

C est beaucoup plus simple et ne fait que le strict minimum nécessaire, laissant un contrôle total à l'utilisateur, qui est capable d'optimiser et de personnaliser complètement tout ce qu'il veut. Vous dites au processeur de charger une chaîne dans un registre, puis d'appeler une fonction de bibliothèque pour utiliser cette chaîne. Le C ++, quant à lui, est beaucoup plus complexe et abstrait . Cela présente un énorme avantage lors de l'écriture de code compliqué, et permet un code plus facile à écrire et plus convivial, mais cela a évidemment un coût. Il y aura toujours un inconvénient dans les performances en C ++ par rapport à C dans des cas comme celui-ci, car C ++ offre plus que ce qui est nécessaire pour accomplir ces tâches de base, et donc il ajoute plus de surcharge .

Répondre à votre question principale :

Est-ce que je paie ce que je ne mange pas?

Dans ce cas précis, oui . Vous ne profitez pas de tout ce que C ++ a à offrir plus que C, mais c'est simplement parce qu'il n'y a rien dans ce simple morceau de code que C ++ pourrait vous aider: c'est si simple que vous n'avez vraiment pas besoin de C ++ du tout.


Oh, et encore une chose!

Les avantages de C ++ peuvent ne pas sembler évidents à première vue, puisque vous avez écrit un programme très simple et petit, mais regardez un exemple un peu plus complexe et voyez la différence (les deux programmes font exactement la même chose):

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

J'espère que vous pouvez clairement voir ce que je veux dire ici. Notez également comment en C vous devez gérer la mémoire à un niveau inférieur en utilisant mallocet freecomment vous devez faire plus attention à l'indexation et aux tailles, et comment vous devez être très spécifique lors de la saisie et de l'impression.

Marco Bonelli
la source
27

Il y a quelques idées fausses pour commencer. Premièrement, le programme C ++ ne donne pas 22 instructions, c'est plutôt 22 000 d'entre elles (j'ai tiré ce nombre de mon chapeau, mais c'est approximativement dans le ballpark). De plus, le code C ne donne pas non plus 9 instructions. Ce ne sont que ceux que vous voyez.

Ce que fait le code C, c'est qu'après avoir fait beaucoup de choses que vous ne voyez pas, il appelle une fonction du CRT (qui est généralement mais pas nécessairement présente en tant que bibliothèque partagée), puis ne vérifie pas la valeur de retour ou la poignée erreurs, et renfloue. Selon les paramètres du compilateur et d'optimisation, il n'appelle même pas vraiment printfmais puts, ou quelque chose d'encore plus primitif.
Vous auriez pu écrire plus ou moins le même programme (à l'exception de certaines fonctions d'initialisation invisibles) en C ++, si seulement vous appeliez cette même fonction de la même manière. Ou, si vous voulez être super correct, cette même fonction préfixée avec std::.

Le code C ++ correspondant n'est en réalité pas du tout la même chose. Alors que l'ensemble <iostream>est bien connu pour être un gros cochon laid qui ajoute une énorme surcharge pour les petits programmes (dans un "vrai" programme, vous ne remarquez pas vraiment beaucoup), une interprétation un peu plus juste est qu'il fait un horrible beaucoup de choses que vous ne voyez pas et qui fonctionnent . Y compris, mais sans s'y limiter, le formatage magique de pratiquement tous les éléments aléatoires, y compris différents formats de nombres et paramètres régionaux, etc., ainsi que la mise en mémoire tampon et la gestion appropriée des erreurs. La gestion des erreurs? Eh bien oui, devinez quoi, la sortie d'une chaîne peut en fait échouer, et contrairement au programme C, le programme C ++ ne l' ignorerait pas en silence. Considérant quoistd::ostreamfait sous le capot, et sans que personne ne s'en rende compte, il est en fait assez léger. Pas comme si je l'utilisais car je déteste la syntaxe du flux avec passion. Mais quand même, c'est assez génial si vous considérez ce que cela fait.

Mais bien sûr, C ++ dans son ensemble n'est pas aussi efficace que C peut l'être. Il ne peut pas être aussi efficace car il est pas la même chose et il ne fait la même chose. Si rien d'autre, C ++ génère des exceptions (et du code pour les générer, les gérer ou les échouer) et il donne des garanties que C ne donne pas. Donc, bien sûr, un programme C ++ doit nécessairement être un peu plus gros. Dans l'ensemble, cependant, cela n'a aucune importance. Au contraire, pour les vrais programmes, je n'ai pas rarement trouvé le C ++ plus performant car pour une raison ou une autre, il semble prêter des optimisations plus favorables. Ne me demandez pas pourquoi en particulier, je ne saurais pas.

Si, au lieu de feu-et-oubliez-l'espoir-pour-le-mieux, vous voulez écrire du code C qui est correct (c'est-à-dire que vous vérifiez réellement les erreurs et que le programme se comporte correctement en présence d'erreurs) alors la différence est marginale, s'il existe.

Damon
la source
16
Très bonne réponse, sauf que cette assertion: «Mais bien sûr, le C ++ dans l'ensemble n'est pas aussi efficace que le C peut l'être» est tout simplement fausse. C ++ peut être aussi efficace que C, et un code de niveau suffisamment élevé peut être plus efficace qu'un code C équivalent. Oui, C ++ a une certaine surcharge due à la gestion des exceptions, mais sur les compilateurs modernes, la surcharge est négligeable par rapport aux gains de performances provenant de meilleures abstractions gratuites.
Konrad Rudolph
Si j'ai bien compris, std::coutjette-t-il aussi des exceptions?
Saher le
6
@Saher: Oui, non, peut-être. std::coutest un std::basic_ostreamet que l'on peut lancer, et il peut renvoyer des exceptions se produisant autrement s'il est configuré pour le faire ou il peut avaler des exceptions. Le fait est que des choses peuvent échouer, et C ++ ainsi que la bibliothèque standard C ++ sont (principalement) construites afin que les échecs ne passent pas facilement inaperçus. C'est un ennui et une bénédiction (mais, plus de bénédiction que de contrariété). C d'autre part vous montre juste le majeur. Vous ne vérifiez pas un code de retour, vous ne savez jamais ce qui s'est passé.
Damon
1
@KonradRudolph: C'est vrai, c'est ce que j'essayais de souligner avec "Je n'ai pas rarement trouvé le C ++ plus performant car pour une raison ou une autre, il semble prêter pour des optimisations plus favorables. Ne me demandez pas pourquoi en particulier" . Il n'est pas immédiatement évident pourquoi, mais pas rarement, il optimise mieux. Pour quelque raison que ce soit. On pourrait penser que c'est la même chose pour l'optimiseur, mais ce n'est pas le cas.
Damon
22

Vous payez pour une erreur. Dans les années 80, lorsque les compilateurs ne sont pas assez bons pour vérifier les chaînes de format, la surcharge des opérateurs était considérée comme un bon moyen d'imposer un semblant de sécurité de type pendant io. Cependant, chacune de ses fonctionnalités de bannière est soit mal implémentée, soit conceptuellement en faillite dès le début:

<iomanip>

La partie la plus répugnante du flux C ++ io api est l'existence de cette bibliothèque d'en-têtes de formatage. En plus d'être avec état, laid et sujet aux erreurs, il couple le formatage au flux.

Supposons que vous vouliez imprimer une ligne avec 8 chiffres zéro hexadécimal non signé int suivi d'un espace suivi d'un double avec 3 décimales. Avec <cstdio>, vous pouvez lire une chaîne de format concis. Avec <ostream>, vous devez enregistrer l'ancien état, définir l'alignement à droite, définir le caractère de remplissage, définir la largeur de remplissage, définir la base sur hexadécimal, afficher l'entier, restaurer l'état enregistré (sinon votre formatage d'entier polluera votre formatage flottant), afficher l'espace , définissez la notation sur fixe, définissez la précision, sortez le double et la nouvelle ligne, puis restaurez l'ancien formatage.

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

Surcharge de l'opérateur

<iostream> est l'enfant de l'affiche de la façon de ne pas utiliser la surcharge d'opérateurs:

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

Performance

std::coutest plusieurs fois plus lent printf(). La fonctionnalité rampante et la répartition virtuelle font des ravages.

Sécurité du fil

Les deux <cstdio>et <iostream>sont thread-safe en ce que chaque appel de fonction est atomique. Mais, printf()fait beaucoup plus par appel. Si vous exécutez le programme suivant avec l' <cstdio>option, vous ne verrez qu'une ligne de f. Si vous utilisez <iostream>sur une machine multicœur, vous verrez probablement autre chose.

// 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 réplique à cet exemple est que la plupart des gens exercent une discipline pour ne jamais écrire dans un seul descripteur de fichier à partir de plusieurs threads de toute façon. Eh bien, dans ce cas, vous devrez observer que cela <iostream>attirera utilement un verrou sur <<chaque >>. Alors que dans <cstdio>, vous ne verrouillez pas aussi souvent, et vous avez même la possibilité de ne pas verrouiller.

<iostream> dépense plus de verrous pour obtenir un résultat moins cohérent.

KevinZ
la source
2
La plupart des implémentations de printf ont une fonction extrêmement utile pour la localisation: des paramètres numérotés. Si vous avez besoin de produire une sortie dans deux langues différentes (comme l'anglais et le français) et que l'ordre des mots est différent, vous pouvez utiliser le même printf avec une chaîne de formatage différente, et il peut imprimer les paramètres dans un ordre différent.
gnasher729
2
Ce formatage avec état des flux a dû donner tellement de bogues difficiles à trouver que je ne sais pas quoi dire. Très bonne réponse. Je voterais plus d'une fois si je le pouvais.
mathreadler le
6
« std::coutEst plusieurs fois plus lent printf()» - Cette affirmation est répétée partout sur le net, mais cela n'a pas été vrai depuis des lustres. Les implémentations IOstream modernes fonctionnent à égalité avec printf. Ce dernier effectue également un envoi virtuel en interne pour gérer les flux tamponnés et les E / S localisées (effectuées par le système d'exploitation mais néanmoins réalisées).
Konrad Rudolph
3
@KevinZ Et c'est génial, mais il s'agit de comparer un seul appel spécifique, qui présente les forces spécifiques de fmt (beaucoup de formats différents dans une seule chaîne). Dans un usage plus courant, la différence entre printfet coutdiminue. Soit dit en passant, il y a des tonnes de telles références sur ce même site.
Konrad Rudolph
3
@KonradRudolph Ce n'est pas vrai non plus. Les microbenchmarks sous-estiment souvent le coût du gonflement et de l'indirection car ils n'épuisent pas certaines ressources limitées (qu'il s'agisse de registres, d'icache, de mémoire, de prédicteurs de branche) là où un programme réel le fera. Lorsque vous faites allusion à un «usage plus typique», cela signifie essentiellement que vous avez beaucoup plus de ballonnements ailleurs, ce qui est bien, mais hors sujet. À mon avis, si vous n'avez pas d'exigences de performances, vous n'avez pas besoin de programmer en C ++.
KevinZ
18

En plus de ce que toutes les autres réponses ont dit,
il y a aussi le fait que ce std::endln'est pas la même chose que '\n'.

C'est une idée fausse malheureusement courante. std::endlne signifie pas "nouvelle ligne",
cela signifie "imprimer une nouvelle ligne et ensuite vider le flux ". Le rinçage n'est pas bon marché!

En ignorant complètement les différences entre printfet std::coutpendant un moment, pour être fonctionnellement équivalent à votre exemple C, votre exemple C ++ devrait ressembler à ceci:

#include <iostream>

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

Et voici un exemple de ce à quoi vos exemples devraient ressembler si vous incluez le rinçage.

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

Lorsque vous comparez du code, vous devez toujours faire attention à ce que vous compariez comme pour le même et que vous compreniez les implications de ce que fait votre code. Parfois, même les exemples les plus simples sont plus compliqués que certains ne le pensent.

Pharap
la source
En fait, utiliser std::endl est l'équivalent fonctionnel de l'écriture d'une nouvelle ligne dans un flux stdio tamponné en ligne. stdout, en particulier, doit être soit en tampon de ligne, soit sans tampon lorsqu'il est connecté à un dispositif interactif. Linux, je crois, insiste sur l'option tamponnée en ligne.
En fait, la bibliothèque iostream n'a pas de mode de mise en tampon de ligne ... la façon d'obtenir l'effet de mise en tampon de ligne est précisément de l'utiliser std::endlpour générer des nouvelles lignes.
@Hurkyl Insist? Alors à quoi sert setvbuf(3)? Ou voulez-vous dire que la ligne par défaut est tamponnée? FYI: Normalement, tous les fichiers sont mis en mémoire tampon. Si un flux fait référence à un terminal (comme le fait normalement stdout), il est mis en tampon en ligne. Le flux d'erreur standard stderr est toujours sans tampon par défaut.
Pryftan
Ne se printfvide pas automatiquement lorsque vous rencontrez un personnage de nouvelle ligne?
bool3max
1
@ bool3max Cela me dirait seulement ce que fait mon environnement, cela pourrait être différent dans d'autres environnements. Même s'il se comporte de la même manière dans toutes les implémentations les plus populaires, cela ne signifie pas qu'il y a un cas limite quelque part. C'est pourquoi la norme est si importante - la norme dicte si quelque chose doit être le même pour toutes les implémentations ou s'il est autorisé à varier entre les implémentations.
Pharap
16

Bien que les réponses techniques existantes soient correctes, je pense que la question découle en fin de compte de cette idée fausse:

Il est connu qu'en C ++, vous payez ce que vous mangez.

Il ne s'agit que d'un discours marketing de la communauté C ++. (Pour être honnête, il y a des discussions marketing dans toutes les communautés linguistiques.) Cela ne signifie rien de concret sur lequel vous pouvez compter sérieusement.

«Vous payez pour ce que vous utilisez» est censé signifier qu'une fonctionnalité C ++ n'a de surcharge que si vous utilisez cette fonctionnalité. Mais la définition d '«une caractéristique» n'est pas infiniment granulaire. Souvent, vous finirez par activer des fonctionnalités qui ont plusieurs aspects, et même si vous n'avez besoin que d'un sous-ensemble de ces aspects, il n'est souvent pas pratique ou possible pour l'implémentation d'intégrer partiellement la fonctionnalité.

En général, de nombreuses langues (mais pas toutes) s'efforcent d'être efficaces, avec plus ou moins de succès. C ++ est quelque part sur l'échelle, mais il n'y a rien de spécial ou de magique dans sa conception qui lui permettrait de réussir parfaitement dans cet objectif.

Theodoros Chatzigiannakis
la source
1
Il y a juste deux choses auxquelles je peux penser où vous payez pour quelque chose que vous n'utilisez pas: les exceptions et le RTTI. Et je ne pense pas que ce soit un discours marketing; C ++ est fondamentalement un C plus puissant, qui est également "ne payez pas pour ce que vous utilisez".
Rakete1111
2
@ Rakete1111 Il est établi depuis longtemps que si les exceptions ne sont pas lancées, elles ne coûtent rien. Si votre programme est lancé de manière cohérente, il doit être repensé. Si la condition d'échec est hors de votre contrôle, vous devez vérifier la condition avec une booléenne retournant une vérification de cohérence, avant d'appeler la méthode qui repose sur la condition fausse.
schulmaster
1
@schulmaster: Les exceptions peuvent imposer des contraintes de conception lorsque le code écrit en C ++ a besoin d'interagir avec du code écrit dans d'autres langages, car les transferts de contrôle non locaux ne peuvent fonctionner sans problème entre les modules que si les modules savent comment se coordonner.
supercat du
1
(bien que pas toutes) les langues s'efforcent d'être efficaces . Certainement pas tous: les langages de programmation ésotériques s'efforcent d'être novateurs / intéressants, pas efficaces. esolangs.org . Certains d'entre eux, comme BrainFuck, sont notoirement inefficaces. Ou par exemple, le langage de programmation Shakespeare, taille minimum de 227 octets (codegolf) pour imprimer tous les entiers . Parmi les langages destinés à une utilisation en production, la plupart visent l'efficacité, mais certains (comme bash) visent principalement la commodité et sont connus pour être lents.
Peter Cordes
2
Eh bien, c'est du marketing, mais c'est presque tout à fait vrai. Vous pouvez vous en tenir à <cstdio>et ne pas inclure <iostream>, tout comme vous pouvez compiler avec -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables.
KevinZ
11

Les fonctions d'entrée / sortie en C ++ sont élégamment écrites et sont conçues pour être simples à utiliser. À bien des égards, ils sont une vitrine pour les fonctionnalités orientées objet en C ++.

Mais vous abandonnez effectivement un peu de performance en retour, mais c'est négligeable par rapport au temps mis par votre système d'exploitation pour gérer les fonctions à un niveau inférieur.

Vous pouvez toujours revenir aux fonctions de style C car elles font partie de la norme C ++, ou peut-être renoncer complètement à la portabilité et utiliser des appels directs à votre système d'exploitation.

Bathsheba
la source
23
"Les fonctions d'entrée / sortie en C ++ sont des monstres hideux qui luttent pour cacher leur nature cthulienne derrière un mince vernis d'utilité. À bien des égards, elles sont une vitrine pour savoir comment ne pas concevoir de code C ++ moderne". Serait probablement plus précis.
user673679
3
@ user673679: Très vrai. Le grand problème avec les flux d'E / S C ++ est ce qu'il y a en dessous: il y a vraiment beaucoup de complexité, et quiconque a déjà traité avec eux (je me réfère à la std::basic_*streambaisse) connaît les maux de tête entrants. Ils ont été conçus pour être largement généraux et étendus par héritage; mais personne n'a finalement fait ça, en raison de leur complexité (il y a littéralement des livres écrits sur iostreams), à tel point que de nouvelles bibliothèques sont nées juste pour cela (par exemple, boost, ICU, etc.). Je doute que nous cesserions jamais de payer pour cette erreur.
edmz
1

Comme vous l'avez vu dans d'autres réponses, vous payez lorsque vous créez des liens dans des bibliothèques générales et appelez des constructeurs complexes. Il n'y a pas de question particulière ici, plus un reproche. Je vais souligner quelques aspects du monde réel:

  1. Barne avait un principe de conception de base pour ne jamais laisser l'efficacité être une raison de rester en C plutôt qu'en C ++. Cela dit, il faut être prudent pour obtenir ces gains d'efficacité, et il y a des gains d'efficacité occasionnels qui ont toujours fonctionné mais qui n'étaient pas «techniquement» dans la spécification C. Par exemple, la disposition des champs de bits n'était pas vraiment spécifiée.

  2. Essayez de regarder à travers ostream. Oh mon dieu c'est gonflé! Je ne serais pas surpris de trouver un simulateur de vol là-dedans. Même printf () de stdlib tourne généralement autour de 50K. Ce ne sont pas des programmeurs paresseux: la moitié de la taille de printf était liée à des arguments de précision indirecte que la plupart des gens n'utilisent jamais. Presque chaque bibliothèque de processeur vraiment contrainte crée son propre code de sortie au lieu de printf.

  3. L'augmentation de la taille offre généralement une expérience plus contenue et plus flexible. Par analogie, un distributeur automatique vendra une tasse de substance semblable à du café pour quelques pièces et toute la transaction prend moins d'une minute. Se rendre dans un bon restaurant implique de mettre une table, d'être assis, de commander, d'attendre, de prendre une bonne tasse, d'obtenir une facture, de payer dans votre choix de formes, d'ajouter un pourboire et de se faire souhaiter une bonne journée en sortant. C'est une expérience différente et plus pratique si vous passez avec des amis pour un repas complexe.

  4. Les gens écrivent toujours ANSI C, bien que rarement K&R C. Mon expérience est que nous le compilons toujours avec un compilateur C ++ en utilisant quelques ajustements de configuration pour limiter ce qui est traîné. Il y a de bons arguments pour d'autres langages: Go supprime la surcharge polymorphe et le préprocesseur fou ; il y a eu de bons arguments pour un empaquetage de champ et une disposition de mémoire plus intelligents. IMHO Je pense que toute conception de langage devrait commencer par une liste d'objectifs, un peu comme le Zen de Python .

Ce fut une discussion amusante. Vous vous demandez pourquoi vous ne pouvez pas avoir de bibliothèques magiquement petites, simples, élégantes, complètes et flexibles?

Il n'y a pas de reponse. Il n'y aura pas de réponse. Voilà la réponse.

Charles Merriam
la source