web-dev-qa-db-fra.com

Grande différence (x9) dans le temps d'exécution entre du code presque identique en C et C ++

J'essayais de résoudre cet exercice sur www.spoj.com: FCTRL - Factorial

Vous n'avez pas vraiment besoin de le lire, faites-le si vous êtes curieux :)

Je l'ai d'abord implémenté en C++ (voici ma solution):

#include <iostream>
using namespace std;

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    std::ios_base::sync_with_stdio(false); // turn off synchronization with the C library’s stdio buffers (from https://stackoverflow.com/a/22225421/5218277)

    cin >> num_of_inputs;

    while (num_of_inputs--)
    {
        cin >> fact_num;

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        cout << num_of_trailing_zeros << "\n";
    }

    return 0;
}

Je l'ai téléchargé comme solution pour g ++ 5.1

Le résultat était: Time 0.18 Mem 3.3M C++ execution results

Mais ensuite, j'ai vu des commentaires qui affirmaient que leur temps d'exécution était inférieur à 0,1. Comme je ne pouvais pas penser à un algorithme plus rapide, j'ai essayé d'implémenter le même code dans [~ # ~] c [~ # ~] :

#include <stdio.h>

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    scanf("%d", &num_of_inputs);

    while (num_of_inputs--)
    {
        scanf("%d", &fact_num);

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        printf("%d", num_of_trailing_zeros);
        printf("%s","\n");
    }

    return 0;
}

Je l'ai téléchargé comme solution pour gcc 5.1

Cette fois, le résultat était: Time 0.02 Mem 2.1M C execution results

Maintenant, le code est presque le même, j'ai ajouté std::ios_base::sync_with_stdio(false); au code C++ comme cela a été suggéré ici pour désactiver la synchronisation avec la bibliothèque C tampons stdio. J'ai également divisé la printf("%d\n", num_of_trailing_zeros); en printf("%d", num_of_trailing_zeros); printf("%s","\n"); pour compenser le double appel de operator<< Dans cout << num_of_trailing_zeros << "\n";.

Mais j'ai toujours vu x9 de meilleures performances et une utilisation de la mémoire plus faible en C qu'en C++.

Pourquoi donc?

[~ # ~] modifier [~ # ~]

J'ai corrigé unsigned long À unsigned int Dans le code C. Il aurait dû être unsigned int Et les résultats indiqués ci-dessus sont liés à la nouvelle version (unsigned int).

85
Alex Lop.

Les deux programmes font exactement la même chose. Ils utilisent le même algorithme exact, et étant donné sa faible complexité, leurs performances sont principalement liées à l'efficacité de la gestion des entrées et des sorties.

balayer l'entrée avec scanf("%d", &fact_num); d'un côté et cin >> fact_num; de l'autre ne semble pas très coûteux de toute façon. En fait, cela devrait être moins coûteux en C++ car le type de conversion est connu au moment de la compilation et le bon analyseur peut être appelé directement par le compilateur C++. Il en va de même pour la sortie. Vous vous efforcez même d'écrire un appel distinct pour printf("%s","\n");, mais le compilateur C est assez bon pour le compiler comme un appel à putchar('\n');.

Donc, en considérant la complexité des E/S et du calcul, la version C++ devrait être plus rapide que la version C.

La désactivation complète de la mise en mémoire tampon de stdout ralentit l'implémentation C à quelque chose d'encore plus lent que la version C++. Un autre test d'AlexLop avec une fflush(stdout); après la dernière printf donne des performances similaires à la version C++. Ce n'est pas aussi lent que de désactiver complètement la mise en mémoire tampon car la sortie est écrite sur le système en petits morceaux au lieu d'un octet à la fois.

Cela semble indiquer un comportement spécifique dans votre bibliothèque C++: je soupçonne que l'implémentation de votre système de cin et cout vide la sortie à cout lorsque l'entrée est demandée à cin. Certaines bibliothèques C le font également, mais généralement uniquement lors de la lecture/écriture vers et depuis le terminal. Le benchmarking effectué par le site www.spoj.com redirige probablement les entrées et sorties vers et depuis les fichiers.

AlexLop a fait un autre test: lire toutes les entrées à la fois dans un vecteur et ensuite calculer et écrire toutes les sorties permet de comprendre pourquoi la version C++ est tellement plus lente. Il augmente les performances à celles de la version C, cela prouve mon point de vue et supprime les soupçons sur le code de formatage C++.

Un autre test de Blastfurnace, stockant toutes les sorties dans un std::ostringstream Et vidant cela d'un coup à la fin, améliore les performances C++ par rapport à celles de la version C de base. QED.

L'entrelacement des entrées de cin et des sorties vers cout semble entraîner une gestion d'E/S très inefficace, ce qui met en échec le schéma de mise en mémoire tampon des flux. réduire les performances d'un facteur 10.

PS: votre algorithme est incorrect pour fact_num >= UINT_MAX / 5 Car fives *= 5 Va déborder et se boucler avant de devenir > fact_num. Vous pouvez corriger cela en créant fives un unsigned long Ou un unsigned long long Si l'un de ces types est plus grand que unsigned int. Utilisez également %u Comme format scanf. Vous avez de la chance que les gars de www.spoj.com ne soient pas trop stricts dans leurs repères.

EDIT: Comme expliqué plus loin par vitaux, ce comportement est en effet rendu obligatoire par la norme C++. cin est lié à cout par défaut. Une opération d'entrée à partir de cin pour laquelle le tampon d'entrée doit être rechargé entraînera le vidage de la sortie en attente par cout. Dans l'implémentation de l'OP, cin semble vider cout systématiquement, ce qui est un peu exagéré et visiblement inefficace.

Ilya Popov a fourni une solution simple pour cela: cin peut être détaché de cout en lançant un autre sort magique en plus de std::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

Notez également qu'un tel vidage forcé se produit également lorsque vous utilisez std::endl Au lieu de '\n' Pour produire une fin de ligne sur cout. Changer la ligne de sortie en un aspect plus idiomatique et innocent C++ cout << num_of_trailing_zeros << endl; Dégraderait les performances de la même manière.

56
chqrlie

Une autre astuce pour rendre iostreams plus rapide lorsque vous utilisez à la fois cin et cout consiste à appeler

cin.tie(nullptr);

Par défaut, lorsque vous saisissez quoi que ce soit à partir de cin, il vide cout. Cela peut nuire considérablement aux performances si vous effectuez des entrées et des sorties entrelacées. Ceci est fait pour les utilisations de l'interface de ligne de commande, où vous affichez une invite et attendez les données:

std::string name;
cout << "Enter your name:";
cin >> name;

Dans ce cas, vous voulez vous assurer que l'invite est réellement affichée avant de commencer à attendre l'entrée. Avec la ligne ci-dessus, vous rompez ce lien, cin et cout deviennent indépendants.

Depuis C++ 11, une autre façon d'obtenir de meilleures performances avec iostreams est d'utiliser std::getline ensemble avec std::stoi, comme ça:

std::string line;
for (int i = 0; i < n && std::getline(std::cin, line); ++i)
{
    int x = std::stoi(line);
}

De cette façon, les performances peuvent se rapprocher du style C, voire dépasser scanf. Utiliser getchar et surtout getchar_unlocked associé à une analyse manuscrite offre toujours de meilleures performances.

PS. J'ai écrit n article comparant plusieurs façons d'entrer des nombres en C++, utile pour les juges en ligne, mais ce n'est qu'en russe, désolé. Les exemples de code et la table finale doivent cependant être compréhensibles.

44
Ilya Popov

Le problème est que, en citant cppreference :

toute entrée depuis std :: cin, sortie vers std :: cerr, ou la terminaison d'un programme force un appel à std :: cout.flush ()

C'est facile à tester: si vous remplacez

cin >> fact_num;

avec

scanf("%d", &fact_num);

et pareil pour cin >> num_of_inputs mais gardez cout vous obtiendrez à peu près les mêmes performances dans votre version C++ (ou, plutôt, la version IOStream) que dans C one:

enter image description here

La même chose se produit si vous conservez cin mais remplacez

cout << num_of_trailing_zeros << "\n";

avec

printf("%d", num_of_trailing_zeros);
printf("%s","\n");

Une solution simple consiste à délier cout et cin comme mentionné par Ilya Popov:

cin.tie(nullptr);

Les implémentations de bibliothèque standard sont autorisées à omettre l'appel à vidage dans certains cas, mais pas toujours. Voici une citation de C++ 14 27.7.2.1.3 (merci à chqrlie):

Classe basic_istream :: sentry: Premièrement, si is.tie () n'est pas un pointeur nul, la fonction appelle is.tie () -> flush () pour synchroniser la séquence de sortie avec tout flux C externe associé. Sauf que cet appel peut être supprimé si la zone de vente de is.tie () est vide. En outre, une implémentation est autorisée à différer l'appel à vider jusqu'à ce qu'un appel à is.rdbuf () -> underflow () se produise. Si aucun appel de ce type ne se produit avant que l'objet sentinelle ne soit détruit, l'appel de vidage peut être entièrement éliminé.

27
vitaut