27 votes

Pourquoi ce code C est-il plus rapide que ce code C++ ? obtenir la plus grande ligne du fichier

J'ai deux versions d'un programme qui fait essentiellement la même chose, obtenir la plus grande longueur d'une ligne dans un fichier, j'ai un fichier avec environ 8 mille lignes, mon code en C est un peu plus primitif (bien sûr !) que le code que j'ai en C++. Le programme en C prend environ 2 secondes pour s'exécuter, alors que le programme en C++ prend 10 secondes pour s'exécuter (même fichier que je teste dans les deux cas). Mais pourquoi ? Je m'attendais à ce qu'il prenne le même temps ou un peu plus, mais pas 8 secondes de moins !

mon code en C :

#include <stdio.h>
#include <stdlib.h> 
#include <string.h>

#if _DEBUG
    #define DEBUG_PATH "../Debug/"
#else
    #define DEBUG_PATH ""
#endif

const char FILE_NAME[] = DEBUG_PATH "data.noun";

int main()
{   
    int sPos = 0;
    int maxCount = 0;
    int cPos = 0;
    int ch;
    FILE *in_file;              

    in_file = fopen(FILE_NAME, "r");
    if (in_file == NULL) 
    {
        printf("Cannot open %s\n", FILE_NAME);
        exit(8);
    }       

    while (1) 
    {
        ch = fgetc(in_file);
        if(ch == 0x0A || ch == EOF) // \n or \r or \r\n or end of file
        {           
            if ((cPos - sPos) > maxCount)
                maxCount = (cPos - sPos);

            if(ch == EOF)
                break;

            sPos = cPos;
        }
        else
            cPos++;
    }

    fclose(in_file);

    printf("Max line length: %i\n",  maxCount); 

    getch();
    return (0);
}

mon code en C++ :

#include <iostream>
#include <fstream>
#include <stdio.h>
#include <string>

using namespace std;

#ifdef _DEBUG
    #define FILE_PATH "../Debug/data.noun"
#else
    #define FILE_PATH "data.noun"
#endif

int main()
{
    string fileName = FILE_PATH;
    string s = "";
    ifstream file;
    int size = 0;

    file.open(fileName.c_str());
    if(!file)
    {
        printf("could not open file!");
        return 0;
    }

    while(getline(file, s) )
            size = (s.length() > size) ? s.length() : size;
    file.close();

    printf("biggest line in file: %i", size);   

    getchar();
    return 0;
}

74voto

Mark Wilkins Points 29291

Je pense qu'il s'agit d'un problème lié aux options du compilateur que vous utilisez, au compilateur lui-même ou au système de fichiers. Je viens de compiler les deux versions (avec les optimisations activées) et je les ai comparées à un fichier texte de 92 000 lignes :

c++ version:  113 ms
c version:    179 ms

Et je soupçonne que la raison pour laquelle la version C++ est plus rapide est que fgetc est très probablement plus lent. fgetc utilise bien les E/S en mémoire tampon, mais il fait un appel de fonction pour récupérer chaque caractère. Je l'ai déjà testé et fgetc n'est pas aussi rapide que de faire un appel pour lire toute la ligne en un seul appel (par exemple, par rapport à fgets ).

29voto

bames53 Points 38303

Dans quelques commentaires, je me suis fait l'écho des réponses des gens qui ont dit que le problème venait probablement de la copie supplémentaire effectuée par votre version C++, qui copie les lignes en mémoire dans une chaîne de caractères. Mais je voulais tester cela.

J'ai d'abord implémenté les versions fgetc et getline et je les ai chronométrées. J'ai confirmé qu'en mode débogage, la version getline est plus lente, environ 130 µs contre 60 µs pour la version fgetc. Ceci n'est pas surprenant étant donné la sagesse conventionnelle selon laquelle les iostreams sont plus lents que l'utilisation de stdio. Cependant, dans le passé, j'ai constaté que les iostreams gagnaient en vitesse grâce à l'optimisation. Cela a été confirmé lorsque j'ai comparé mes temps en mode release : environ 20 µs en utilisant getline et 48 µs avec fgetc.

Le fait que l'utilisation de getline avec iostreams soit plus rapide que fgetc, au moins en mode release, va à l'encontre du raisonnement selon lequel copier toutes ces données doit être plus lent que de ne pas les copier, donc je ne suis pas sûr de ce que l'optimisation permet d'éviter, et je n'ai pas vraiment cherché à trouver d'explication, mais il serait intéressant de comprendre ce qui est optimisé. edit : lorsque j'ai regardé les programmes avec un profileur, il n'était pas évident de comparer les performances puisque les différentes méthodes semblaient si différentes les unes des autres.

Quoi qu'il en soit, j'ai voulu voir si je pouvais obtenir une version plus rapide en évitant la copie à l'aide de la fonction get() sur l'objet fstream et faire exactement ce que fait la version C. Lorsque j'ai fait cela, j'ai été assez surpris de constater que l'utilisation de fstream::get() était beaucoup plus lente que les méthodes fgetc et getline, aussi bien en version debug qu'en version release ; environ 230 µs en version debug, et 80 µs en version release.

Afin de déterminer la cause du ralentissement, j'ai réalisé une autre version, cette fois en utilisant le stream_buf attaché à l'objet fstream, et j'ai obtenu les résultats suivants snextc() sur ce point. Cette version est de loin la plus rapide : 25 µs en debug et 6 µs en release.

Je suppose que la chose qui rend le fstream::get() est qu'elle construit un objet sentinelle pour chaque appel. Bien que je ne l'aie pas testé, je ne vois pas en quoi la méthode get() ne fait rien d'autre que de récupérer le caractère suivant dans le stream_buf, à l'exception de ces objets sentinelles.

Quoi qu'il en soit, la morale de l'histoire est que si vous voulez un io rapide, il est préférable d'utiliser des fonctions iostream de haut niveau plutôt que stdio, et pour un io vraiment rapide, d'accéder au stream_buf sous-jacent. edit : en fait, cette morale ne s'applique qu'à MSVC, voir la mise à jour en bas de page pour les résultats obtenus avec une chaîne d'outils différente.

Pour référence :

J'ai utilisé VS2010 et le chrono de boost 1.47 pour le timing. J'ai construit des binaires 32 bits (apparemment requis par boost chrono parce qu'il ne semble pas trouver de version 64 bits de cette librairie). Je n'ai pas modifié les options de compilation mais il se peut qu'elles ne soient pas tout à fait standard puisque j'ai fait cela dans un projet scratch vs que je garde à portée de main.

Le fichier que j'ai testé est la version texte brut de 1,1 Mo et 20 000 lignes des Oeuvres Complètes de Frédéric Bastiat, tome 1 de Frédéric Bastiat du Projet Gutenberg, http://www.gutenberg.org/ebooks/35390

Durée du mode de déclenchement

fgetc time is: 48150 microseconds
snextc time is: 6019 microseconds
get time is: 79600 microseconds
getline time is: 19881 microseconds

Durée du mode débogage :

fgetc time is: 59593 microseconds
snextc time is: 24915 microseconds
get time is: 228643 microseconds
getline time is: 130807 microseconds

Voici mon fgetc() version :

{
    auto begin = boost::chrono::high_resolution_clock::now();
    FILE *cin = fopen("D:/bames/automata/pg35390.txt","rb");
    assert(cin);
    unsigned maxLength = 0;
    unsigned i = 0;
    int ch;
    while(1) {
        ch = fgetc(cin);
        if(ch == 0x0A || ch == EOF) {
            maxLength = std::max(i,maxLength);
            i = 0;
            if(ch==EOF)
                break;
        } else {
            ++i;
        }
    }
    fclose(cin);
    auto end = boost::chrono::high_resolution_clock::now();
    std::cout << "max line is: " << maxLength << '\n';
    std::cout << "fgetc time is: " << boost::chrono::duration_cast<boost::chrono::microseconds>(end-begin) << '\n';
}

Voici mon getline() version :

{
    auto begin = boost::chrono::high_resolution_clock::now();
    std::ifstream fin("D:/bames/automata/pg35390.txt",std::ios::binary);
    unsigned maxLength = 0;
    std::string line;
    while(std::getline(fin,line)) {
        maxLength = std::max(line.size(),maxLength);
    }
    auto end = boost::chrono::high_resolution_clock::now();
    std::cout << "max line is: " << maxLength << '\n';
    std::cout << "getline time is: " << boost::chrono::duration_cast<boost::chrono::microseconds>(end-begin) << '\n';
}

les fstream::get() version

{
    auto begin = boost::chrono::high_resolution_clock::now();
    std::ifstream fin("D:/bames/automata/pg35390.txt",std::ios::binary);
    unsigned maxLength = 0;
    unsigned i = 0;
    while(1) {
        int ch = fin.get();
        if(fin.good() && ch == 0x0A || fin.eof()) {
            maxLength = std::max(i,maxLength);
            i = 0;
            if(fin.eof())
                break;
        } else {
            ++i;
        }
    }
    auto end = boost::chrono::high_resolution_clock::now();
    std::cout << "max line is: " << maxLength << '\n';
    std::cout << "get time is: " << boost::chrono::duration_cast<boost::chrono::microseconds>(end-begin) << '\n';
}

et le snextc() version

{
    auto begin = boost::chrono::high_resolution_clock::now();
    std::ifstream fin("D:/bames/automata/pg35390.txt",std::ios::binary);
    std::filebuf &buf = *fin.rdbuf();
    unsigned maxLength = 0;
    unsigned i = 0;
    while(1) {
        int ch = buf.snextc();
        if(ch == 0x0A || ch == std::char_traits<char>::eof()) {
            maxLength = std::max(i,maxLength);
            i = 0;
            if(ch == std::char_traits<char>::eof())
                break;
        } else {
            ++i;
        }
    }
    auto end = boost::chrono::high_resolution_clock::now();
    std::cout << "max line is: " << maxLength << '\n';
    std::cout << "snextc time is: " << boost::chrono::duration_cast<boost::chrono::microseconds>(end-begin) << '\n';
}

mettre à jour :

J'ai refait les tests en utilisant clang (trunk) sur OS X avec libc++. Les résultats pour les implémentations basées sur iostream sont restés relativement les mêmes (avec l'optimisation activée) ; fstream::get() beaucoup plus lent que std::getline() beaucoup plus lent que filebuf::snextc() . Mais la performance des fgetc() s'est améliorée par rapport à la getline() et est devenu plus rapide. Cela s'explique peut-être par le fait que la copie effectuée par getline() devient un problème avec cette chaîne d'outils alors qu'il ne l'était pas avec MSVC ? Peut-être que l'implémentation CRT de Microsoft de fgetc() est mauvaise ou quelque chose comme ça ?

Quoi qu'il en soit, voici les temps (j'ai utilisé un fichier beaucoup plus grand, 5,3 MB) :

en utilisant -Os

fgetc time is: 39004 microseconds
snextc time is: 19374 microseconds
get time is: 145233 microseconds
getline time is: 67316 microseconds

en utilisant -O0

fgetc time is: 44061 microseconds
snextc time is: 92894 microseconds
get time is: 184967 microseconds
getline time is: 209529 microseconds

-O2

fgetc time is: 39356 microseconds
snextc time is: 21324 microseconds
get time is: 149048 microseconds
getline time is: 63983 microseconds

-O3

fgetc time is: 37527 microseconds
snextc time is: 22863 microseconds
get time is: 145176 microseconds
getline time is: 67899 microseconds

14voto

datenwolf Points 85093

La version C++ alloue et désalloue constamment des instances de std::string. L'allocation de mémoire est une opération coûteuse. En outre, les constructeurs/destructeurs sont exécutés.

La version C, quant à elle, utilise une mémoire constante et se contente de faire ce qui est nécessaire : lecture de caractères simples, réglage du compteur de longueur de ligne à la nouvelle valeur si elle est plus élevée, pour chaque nouvelle ligne, et c'est tout.

11voto

dasblinkenlight Points 264350

Vous ne comparez pas des pommes avec des pommes. Votre programme C ne copie pas les données de FILE* dans la mémoire de votre programme. Il fonctionne également avec des fichiers bruts.

Votre programme C++ doit parcourir la longueur de chaque chaîne plusieurs fois - une fois dans le code du flux pour savoir quand terminer la chaîne qu'il vous renvoie, une fois dans le constructeur de std::string , et une fois dans l'appel de votre code à s.length() .

Il est possible que vous puissiez améliorer les performances de votre programme C, par exemple en utilisant getc_unlocked s'il est disponible pour vous. Mais le plus grand avantage réside dans le fait que vous n'avez pas à copier vos données.

EDIT : édité en réponse à un commentaire de bames53

5voto

fortran Points 26495

2 secondes pour seulement 8.000 lignes ? Je ne sais pas quelle est la longueur de vos lignes, mais il y a de fortes chances que vous fassiez quelque chose de très mal.

Ce programme Python trivial s'exécute presque instantanément avec El Quijote téléchargé depuis le Projet Gutenberg (40006 lignes, 2.2MB) :

import sys
print max(len(s) for s in sys.stdin)

Le timing :

~/test$ time python maxlen.py < pg996.txt
76

real    0m0.034s
user    0m0.020s
sys     0m0.010s

Vous pourriez améliorer votre code C en mettant l'entrée en mémoire tampon plutôt qu'en la lisant caractère par caractère.

La raison pour laquelle le C++ est plus lent que le C est liée à la construction des objets chaîne de caractères et à l'appel de la méthode length. En C, vous comptez simplement les caractères au fur et à mesure.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X