Pourquoi ce code C est-il plus rapide que ce code C++? obtenir la plus grande ligne dans le fichier
J'ai deux versions d'un programme qui fait la même chose, l'obtention de 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 primitive (bien sûr!) que le code que j'ai en C++. Le programme C prend environ 2 secondes à exécuter, tandis que le programme en C++ prend 10 secondes à exécuter (même fichier que je teste pour les deux cas). Mais pourquoi? Je m'attendais à ce que cela prenne le même temps ou un peu plus mais pas 8 secondes plus lent!
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 %sn", FILE_NAME);
exit(8);
}
while (1)
{
ch = fgetc(in_file);
if(ch == 0x0A || ch == EOF) // n or r or rn 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: %in", 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;
}
8 réponses
La version c++ Alloue et désalloue constamment les instances de std:: string. L'allocation de mémoire est une opération coûteuse. En plus de cela, les constructeurs / destructeurs sont exécutés.
La version C utilise cependant une mémoire constante, et c'est juste nécessaire: lire en caractères simples, définir le compteur de longueur de ligne à la nouvelle valeur si elle est supérieure, pour chaque nouvelle ligne et c'est tout.
Je suppose que c'est un problème avec les options du compilateur que vous utilisez, le compilateur lui-même ou le système de fichiers. Je viens maintenant de compiler les deux versions (avec des optimisations) et de les exécuter sur 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 probablement plus lent. fgetc
utilise des e/s tamponnées, mais il effectue 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 la ligne entière dans un appel (par exemple, par rapport à fgets
).
Donc, dans quelques commentaires, j'ai fait écho aux réponses des gens que le problème était probablement la copie supplémentaire effectuée par votre version C++, où il copie les lignes en mémoire dans une chaîne. Mais j'ai voulu le tester.
J'ai d'abord implémenté les versions fgetc et getline et les ai chronométrées. J'ai confirmé qu'en mode débogage la version getline est plus lente, environ 130 µs vs 60 µs pour la version fgetc. Ce n'est pas surprenant étant donné la sagesse conventionnelle que iostreams sont plus lents que d'utiliser stdio. Toutefois, dans le passé, il a été mon expérience que iostreams obtenir une vitesse significative de l'optimisation. Cela a été confirmé lorsque j'ai comparé mes temps de mode de libération: 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 toute optimisation peut éviter, et je n'ai pas vraiment cherché, mais il serait intéressant de comprendre ce qui est optimisé. edit: quand j'ai regardé les programmes avec un profileur, il n'était pas évident de comparer les performances car les différentes méthodes semblaient si différentes les unes des autres
Anwyay je voulais voir si je pouvais obtenir une version plus rapide en évitant la copie en utilisant la méthode get()
sur l'objet fstream et juste faire exactement ce que fait la version C. Quand j'ai fait cela, j'ai été assez surpris de constater que l'utilisation de fstream::get()
était un peu plus lent que les méthodes fgetc et getline à la fois dans debug et release; environ 230 µs dans debug, et 80 µs dans Release.
Pour affiner quel que soit le ralentissement, je suis allé de l'avant et j'ai fait une autre version, cette fois en utilisant le stream_buf attaché à l'objet fstream, et la méthode snextc()
à ce sujet. Cette version est de loin la plus rapide; 25 µs en debug et 6 µs en release.
Je suppose que la chose qui rend la méthode fstream::get()
tellement plus lente est qu'elle construit un sentry objets pour chaque appel. Bien que je n'ai pas testé cela, je ne peux pas voir que get()
fait bien au-delà du simple fait d'obtenir le caractère suivant du stream_buf, à l'exception de ces objets sentry.
Quoi qu'il en soit, la morale de l'histoire est que si vous voulez des E / S rapides, il vaut probablement mieux utiliser des fonctions iostream de haut niveau plutôt que stdio, et pour des e / s très rapides, accéder au stream_buf sous-jacent. edit: en fait, cette morale ne peut s'appliquer qu'à MSVC, voir Mise à jour en bas pour les résultats d'un différents outils.
Pour référence:
J'ai utilisé VS2010 et chrono de boost 1.47 Pour le chronométrage. J'ai construit des binaires 32 bits (semble requis par Boost chrono car il ne semble pas trouver une version 64 bits de cette lib). Je n'ai pas modifié les options de compilation, mais elles ne sont peut-être pas complètement standard puisque je l'ai fait dans un projet scratch vs que je garde.
Le fichier avec lequel j'ai testé était la version en texte brut 1.1 MB 20 000 lignes D'Œuvres Complètes de Frédéric Bastiat, tome 1 par Frédéric Bastiat du Projet Gutenberg, http://www.gutenberg.org/ebooks/35390
Temps du mode de Libération
fgetc time is: 48150 microseconds
snextc time is: 6019 microseconds
get time is: 79600 microseconds
getline time is: 19881 microseconds
Temps 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 ma version fgetc()
:
{
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 ma version getline()
:
{
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';
}
La version fstream::get()
{
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 la version snextc()
{
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';
}
Mise à jour:
Je redirige les tests en utilisant clang (trunk) sur OS X avec libc++. Les résultats pour les implémentations basées sur iostream resté relativement le même (avec optimisation activée); fstream::get()
beaucoup plus lent que std::getline()
beaucoup plus lent que filebuf::snextc()
. Mais les performances de fgetc()
se sont améliorées par rapport à l'implémentation getline()
et sont devenues plus rapides. Peut-être est-ce parce que la copie effectuée par getline()
devient un problème avec cette chaîne d'outils alors que ce n'était pas avec MSVC? Peut-être que L'implémentation CRT de Microsoft de fgetc () est mauvaise ou quelque chose?
De toute façon, 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
Vous ne comparez pas les pommes aux pommes. Votre programme C ne copie pas les données du tampon FILE*
dans la mémoire de votre programme. Il fonctionne également sur les fichiers raw.
Votre programme C++ doit parcourir la longueur de chaque chaîne plusieurs fois-une fois dans le code de 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 C programme, par exemple en utilisant getc_unlocked
si elle est disponible pour vous. Mais la plus grande victoire vient de ne pas avoir à copier vos données.
EDIT: édité en réponse à un commentaire par bames53
2 secondes pour seulement 8.000 lignes? Je ne sais pas combien de temps vos lignes sont, mais les chances sont que vous faites quelque chose de très mal.
Ce programme Python trivial s'exécute presque instantanément avec El Quijote téléchargé à partir du Projet Gutenberg (40006 lignes, 2.2 MB):
import sys
print max(len(s) for s in sys.stdin)
Le calendrier:
~/test$ time python maxlen.py < pg996.txt
76
real 0m0.034s
user 0m0.020s
sys 0m0.010s
Vous pouvez améliorer votre code C en mettant en mémoire tampon l'entrée plutôt que de lire char par char.
Pourquoi le c++ est-il plus lent que C, il devrait être lié à la construction du objets string, puis appeler la méthode length. En C, vous ne faites que compter les caractères au fur et à mesure.
J'ai essayé de compiler et d'exécuter vos programmes contre 40K lignes de source C++ et ils ont tous deux terminé dans environ 25ms. Je ne peux que conclure que vos fichiers d'entrée ont extrêmement longues lignes, éventuellement 10k-100k caractères par ligne. Dans ce cas, la version C n'a aucune performance négative de la longue longueur de ligne tandis que la version c++ devrait continuer à augmenter la taille de la chaîne et à copier les anciennes données dans le nouveau tampon. Si elle devait augmenter en taille a nombre suffisant de fois qui pourraient expliquer la différence de performance excessive.
La clé ici est que les deux programmes ne font pas la même chose, donc vous ne pouvez pas vraiment comparer leurs résultats. Si vous avez été en mesure de fournir le fichier d'entrée, nous pourrions être en mesure de fournir des détails supplémentaires.
Vous pouvez probablement utiliser tellg
et ignore
pour le faire plus rapidement en C++.
Le programme c++ construit des objets de chaîne des lignes, tandis que le programme C lit simplement les caractères et regarde les caractères.
Modifier:
Merci pour les upvotes, mais après la discussion, je pense maintenant que cette réponse est fausse. C'était une première supposition raisonnable, mais dans ce cas, il semble que les temps d'exécution différents (et très lents) soient causés par d'autres choses.
Je suis d'accord avec les gens de la théorie. Mais soyons empiriques.
J'ai généré un fichier avec 13 millions de lignes de fichier texte pour travailler avec.
~$ for i in {0..1000}; do cat /etc/* | strings; done &> huge.txt
Le code d'origine modifié pour lire à partir de stdin
(ne devrait pas affecter trop les performances)
fait en presque 2 min.
Code C++:
#include <iostream>
#include <stdio.h>
using namespace std;
int main(void)
{
string s = "";
int size = 0;
while (cin) {
getline(cin, s);
size = (s.length() > size) ? s.length() : size;
}
printf("Biggest line in file: %i\n", size);
return 0;
}
Temps c++:
~$ time ./cplusplus < huge.txt
real 1m53.122s
user 1m29.254s
sys 0m0.544s
Une version' C':
#include <stdio.h>
int main(void)
{
char *line = NULL;
int read, max = 0, len = 0;
while ((read = getline(&line, &len, stdin)) != -1)
if (max < read)
max = read -1;
printf("Biggest line in file %d\n", max);
return 0;
}
C rendement:
~$ time ./ansic < huge.txt
real 0m4.015s
user 0m3.432s
sys 0m0.328s
Faites vos propres calculs...