Je voulais comparer les lignes de lecture des entrées de chaîne de stdin en utilisant Python et C ++ et j'ai été choqué de voir mon code C ++ s'exécuter un ordre de grandeur plus lentement que le code Python équivalent. Comme mon C ++ est rouillé et que je ne suis pas encore un expert Pythonista, dites-moi si je fais quelque chose de mal ou si je me méprends sur quelque chose.
(Réponse TLDR: incluez la déclaration: cin.sync_with_stdio(false)
ou utilisez simplement à la fgets
place.
Résultats TLDR: faites défiler jusqu'au bas de ma question et regardez le tableau.)
Code C ++:
#include <iostream>
#include <time.h>
using namespace std;
int main() {
string input_line;
long line_count = 0;
time_t start = time(NULL);
int sec;
int lps;
while (cin) {
getline(cin, input_line);
if (!cin.eof())
line_count++;
};
sec = (int) time(NULL) - start;
cerr << "Read " << line_count << " lines in " << sec << " seconds.";
if (sec > 0) {
lps = line_count / sec;
cerr << " LPS: " << lps << endl;
} else
cerr << endl;
return 0;
}
// Compiled with:
// g++ -O3 -o readline_test_cpp foo.cpp
Équivalent Python:
#!/usr/bin/env python
import time
import sys
count = 0
start = time.time()
for line in sys.stdin:
count += 1
delta_sec = int(time.time() - start_time)
if delta_sec >= 0:
lines_per_sec = int(round(count/delta_sec))
print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
lines_per_sec))
Voici mes résultats:
$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889
$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. LPS: 5570000
Je dois noter que j'ai essayé ceci sous Mac OS X v10.6.8 (Snow Leopard) et Linux 2.6.32 (Red Hat Linux 6.2). Le premier est un MacBook Pro, et le second est un serveur très costaud, pas que ce soit trop pertinent.
$ for i in {1..5}; do echo "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP: Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP: Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Petit addenda de référence et récapitulation
Pour être complet, j'ai pensé mettre à jour la vitesse de lecture du même fichier sur la même boîte avec le code C ++ d'origine (synchronisé). Encore une fois, c'est pour un fichier de ligne 100M sur un disque rapide. Voici la comparaison, avec plusieurs solutions / approches:
Implementation Lines per second
python (default) 3,571,428
cin (default/naive) 819,672
cin (no sync) 12,500,000
fgets 14,285,714
wc (not fair comparison) 54,644,808
<iostream>
performances sont nulles. Pas la première fois que ça arrive. 2) Python est assez intelligent pour ne pas copier les données dans la boucle for car vous ne les utilisez pas. Vous pouvez retester en essayant d'utiliserscanf
et achar[]
. Alternativement, vous pouvez essayer de réécrire la boucle afin que quelque chose soit fait avec la chaîne (par exemple, gardez la 5ème lettre et concaténez-la en résultat).cin.eof()
!! Mettez l'getline
appel dans l'instruction «if».wc -l
est rapide car il lit le flux sur plusieurs lignes à la fois (il peut s'agir d'unefread(stdin)/memchr('\n')
combinaison). Les résultats Python sont dans le même ordre de grandeur, par exemple,wc-l.py
Réponses:
Par défaut,
cin
est synchronisé avec stdio, ce qui lui permet d'éviter toute mise en mémoire tampon d'entrée. Si vous ajoutez ceci en haut de votre main, vous devriez voir des performances bien meilleures:Normalement, lorsqu'un flux d'entrée est mis en mémoire tampon, au lieu de lire un caractère à la fois, le flux sera lu en plus gros morceaux. Cela réduit le nombre d'appels système, qui sont généralement relativement coûteux. Cependant, étant donné que les
FILE*
basesstdio
etiostreams
ont souvent des implémentations distinctes et donc des tampons séparés, cela pourrait entraîner un problème si les deux étaient utilisés ensemble. Par exemple:Si plus d'entrée a été lue
cin
que nécessaire, la deuxième valeur entière ne serait pas disponible pour lascanf
fonction, qui a son propre tampon indépendant. Cela conduirait à des résultats inattendus.Pour éviter cela, par défaut, les flux sont synchronisés avec
stdio
. Une façon courante d'y parvenir est d'avoircin
lu chaque caractère un par un selon les besoins à l'aide desstdio
fonctions. Malheureusement, cela introduit beaucoup de frais généraux. Pour de petites quantités d'entrée, ce n'est pas un gros problème, mais lorsque vous lisez des millions de lignes, la pénalité de performance est importante.Heureusement, les concepteurs de la bibliothèque ont décidé que vous devriez également pouvoir désactiver cette fonctionnalité pour obtenir de meilleures performances si vous saviez ce que vous faisiez, ils ont donc fourni la
sync_with_stdio
méthode.la source
fscanf
appel, car cela ne fait tout simplement pas autant de travail que Python. Python doit allouer de la mémoire pour la chaîne, éventuellement plusieurs fois car l'allocation existante est jugée insuffisante - exactement comme l'approche C ++ avecstd::string
. Cette tâche est presque certainement liée aux E / S et il y a beaucoup trop de FUD autour du coût de création d'std::string
objets en C ++ ou d'utilisation<iostream>
en soi.sync_with_stdio()
s'agit d'une fonction membre statique et qu'un appel à cette fonction sur n'importe quel objet de flux (par exemplecin
) active ou désactive la synchronisation pour tous les objets iostream standard.Par simple curiosité, j'ai jeté un œil à ce qui se passe sous le capot, et j'ai utilisé dtruss / strace à chaque test.
C ++
appels système
sudo dtruss -c ./a.out < in
Python
appels système
sudo dtruss -c ./a.py < in
la source
J'ai quelques années de retard ici, mais:
Dans 'Edit 4/5/6' du post d'origine, vous utilisez la construction:
C'est faux de deux manières différentes:
Vous planifiez réellement l'exécution de
cat
, pas votre référence. L'utilisation du processeur «utilisateur» et «sys» affichée partime
est celle decat
, pas votre programme de référence. Pire encore, le temps «réel» n'est pas nécessairement précis. Selon l'implémentation decat
et des pipelines dans votre système d'exploitation local, il est possible que lecat
dernier tampon géant soit écrit et se termine bien avant que le processus de lecture ne termine son travail.L'utilisation de
cat
est inutile et en fait contre-productive; vous ajoutez des pièces mobiles. Si vous étiez sur un système suffisamment ancien (c.-à-d. Avec un seul processeur et - dans certaines générations d'ordinateurs - des E / S plus rapides que le processeur) - le simple fait decat
fonctionner pourrait considérablement colorer les résultats. Vous êtes également soumis à tout ce que la mise en mémoire tampon d'entrée et de sortie et tout autre traitementcat
peuvent faire. (Cela vous gagnerait probablement un prix `` Utilisation inutile du chat '' si j'étais Randal Schwartz.Une meilleure construction serait:
Dans cette déclaration, c'est le shell qui ouvre big_file, en le passant à votre programme (enfin,
time
auquel il exécute ensuite votre programme en tant que sous-processus) en tant que descripteur de fichier déjà ouvert. 100% de la lecture du fichier est strictement la responsabilité du programme que vous essayez de comparer. Cela vous donne une vraie lecture de ses performances sans complications parasites.Je mentionnerai deux «correctifs» possibles, mais en fait erronés, qui pourraient également être pris en compte (mais je les «numérote» différemment car ce ne sont pas des choses qui étaient erronées dans le message d'origine):
A. Vous pouvez «corriger» cela en chronométrant uniquement votre programme:
B. ou en chronométrant l'intégralité du pipeline:
Ce sont des erreurs pour les mêmes raisons que # 2: ils utilisent toujours
cat
inutilement. Je les mentionne pour plusieurs raisons:ils sont plus «naturels» pour les personnes qui ne sont pas entièrement à l'aise avec les fonctions de redirection d'E / S du shell POSIX
il peut y avoir des cas où
cat
est nécessaire (par exemple: le fichier à lire nécessite une sorte de privilège d'accès, et vous ne voulez pas accorder ce privilège au programme à benchmarkée:sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output
)dans la pratique , sur les machines modernes, l'ajout
cat
dans le pipeline est probablement sans conséquence réelle.Mais je dis cette dernière chose avec une certaine hésitation. Si nous examinons le dernier résultat dans «Edit 5» -
- cela prétend avoir
cat
consommé 74% du CPU pendant le test; et en effet 1,34 / 1,83 est d'environ 74%. Peut-être une série de:n'aurait pris que les 0,49 secondes restantes! Probablement pas:
cat
ici, il fallait payer pour lesread()
appels système (ou équivalent) qui ont transféré le fichier depuis le «disque» (en fait le cache de tampon), ainsi que le tube écrit pour les livrerwc
. Le test correct aurait toujours dû faire cesread()
appels; seuls les appels d'écriture sur le tuyau et de lecture sur le tuyau auraient été enregistrés, et ceux-ci devraient être assez bon marché.Pourtant, je prédis que vous seriez en mesure de mesurer la différence entre
cat file | wc -l
etwc -l < file
et de trouver une différence notable (pourcentage à 2 chiffres). Chacun des tests les plus lents aura payé une pénalité similaire en temps absolu; ce qui équivaudrait cependant à une fraction plus petite de son temps total plus grand.En fait, j'ai fait quelques tests rapides avec un fichier d'ordures de 1,5 gigaoctet, sur un système Linux 3.13 (Ubuntu 14.04), obtenant ces résultats (ce sont en fait les «meilleurs des 3» résultats; après avoir amorcé le cache, bien sûr):
Notez que les deux résultats du pipeline prétendent avoir pris plus de temps CPU (utilisateur + sys) que le temps réel d'horloge murale. C'est parce que j'utilise la commande «time» intégrée du shell (bash), qui connaît le pipeline; et je suis sur une machine multicœur où des processus séparés dans un pipeline peuvent utiliser des cœurs distincts, accumulant du temps CPU plus rapidement qu'en temps réel. En utilisant
/usr/bin/time
je vois un temps CPU plus petit qu'en temps réel - montrant qu'il ne peut chronométrer que l'élément de pipeline unique qui lui est passé sur sa ligne de commande. De plus, la sortie du shell donne des millisecondes alors qu'elle/usr/bin/time
ne donne que des centièmes de seconde.Ainsi, au niveau de l'efficacité de
wc -l
, lecat
fait une énorme différence: 409/283 = 1,453 ou 45,3% de temps réel en plus, et 775/280 = 2,768, soit un énorme processeur utilisé de 177%! Sur ma boîte de test aléatoire, il était là au moment.Je dois ajouter qu'il existe au moins une autre différence significative entre ces styles de test, et je ne peux pas dire s'il s'agit d'un avantage ou d'une faute; vous devez décider vous-même:
Lorsque vous exécutez
cat big_file | /usr/bin/time my_program
, votre programme reçoit des entrées d'un canal, à la vitesse précisément envoyée parcat
, et en morceaux pas plus grands que ceux écrits parcat
.Lorsque vous exécutez
/usr/bin/time my_program < big_file
, votre programme reçoit un descripteur de fichier ouvert vers le fichier réel. Votre programme - ou dans de nombreux cas les bibliothèques d'E / S de la langue dans laquelle il a été écrit - peut prendre différentes mesures lorsqu'il est présenté avec un descripteur de fichier référençant un fichier standard. Il peut utilisermmap(2)
pour mapper le fichier d'entrée dans son espace d'adressage, au lieu d'utiliser desread(2)
appels système explicites . Ces différences pourraient avoir un effet beaucoup plus important sur vos résultats de référence que le faible coût d'exécution ducat
binaire.Bien sûr, c'est un résultat de référence intéressant si le même programme fonctionne de manière sensiblement différente entre les deux cas. Il montre que, en effet, le programme ou ses bibliothèques d' E / S sont en train de faire quelque chose intéressant, comme l' utilisation
mmap()
. Donc, dans la pratique, il peut être judicieux d'exécuter les repères dans les deux sens; peut-être en escomptant lecat
résultat par un petit facteur pour "pardonner" le coût de fonctionnementcat
lui-même.la source
$ < big_file time my_program
$ time < big_file my_program
cela devrait fonctionner dans n'importe quel shell POSIX (c'est -à- dire pas `csh `et je ne suis pas sûr d'exotica comme` rc`:)time
mesure l'ensemble du pipeline au lieu du premier programme.time seq 2 | while read; do sleep 1; done
imprime 2 sec,/usr/bin/time seq 2 | while read; do sleep 1; done
imprime 0 sec.J'ai reproduit le résultat original sur mon ordinateur en utilisant g ++ sur un Mac.
L'ajout des instructions suivantes à la version C ++ juste avant la
while
boucle la met en ligne avec la version Python :sync_with_stdio a amélioré la vitesse à 2 secondes et la définition d'un tampon plus important l'a ramenée à 1 seconde.
la source
getline
, les opérateurs de fluxscanf
, peuvent être pratiques si vous ne vous souciez pas du temps de chargement des fichiers ou si vous chargez de petits fichiers texte. Mais, si les performances vous intéressent, vous devez vraiment simplement mettre tout le fichier en mémoire tampon (en supposant qu'il convienne).Voici un exemple:
Si vous le souhaitez, vous pouvez envelopper un flux autour de ce tampon pour un accès plus pratique comme celui-ci:
De plus, si vous contrôlez le fichier, envisagez d'utiliser un format de données binaires plat au lieu du texte. Il est plus fiable de lire et d'écrire car vous n'avez pas à gérer toutes les ambiguïtés des espaces blancs. Il est également plus petit et beaucoup plus rapide à analyser.
la source
Le code suivant était plus rapide pour moi que l'autre code publié ici jusqu'à présent: (Visual Studio 2013, 64 bits, fichier de 500 Mo avec une longueur de ligne uniforme en [0, 1000)).
Il bat toutes mes tentatives Python de plus d'un facteur 2.
la source
read
transforme de manière itérative soit des appels système sans tampon en un tampon statique de longueurBUFSIZE
ou via lesmmap
appels système correspondants équivalents , puis fouille dans ce tampon en comptant les nouvelles lignes à lafor (char *cp = buf; *cp; cp++) count += *cp == "\n"
. Vous devrez cependant réglerBUFSIZE
votre système, ce que stdio aura déjà fait pour vous. Mais cettefor
boucle devrait se compiler en instructions de langage d'assembleur incroyablement rapides pour le matériel de votre boîte.Soit dit en passant, la raison pour laquelle le nombre de lignes pour la version C ++ est supérieur au nombre pour la version Python est que l'indicateur eof n'est défini que lorsqu'une tentative est effectuée pour lire au-delà de eof. La boucle correcte serait donc:
la source
while (getline(cin, input_line)) line_count++;
++line_count;
et pasline_count++;
.long
, et le compilateur est tout à fait capable de dire que le résultat de l'incrément n'est pas utilisé. S'il ne génère pas de code identique pour le post-incrément et le pré-incrément, il est cassé.++line_count;
au lieu deline_count++;
ne ferait pas de mal :)while
, non? Serait-il important s'il y avait une sorte d'erreur et que vous vouliez vous assurer queline_count
c'était correct? Je ne fais que deviner, mais je ne comprends pas pourquoi cela importerait.Dans votre deuxième exemple (avec scanf ()), la raison pour laquelle cela est encore plus lent pourrait être parce que scanf ("% s") analyse la chaîne et recherche tout caractère d'espace (espace, tabulation, nouvelle ligne).
En outre, oui, CPython effectue une mise en cache pour éviter les lectures sur disque dur.
la source
Un premier élément de réponse:
<iostream>
est lent. Zut lent. J'obtiens une énorme amélioration des performances avecscanf
comme ci-dessous, mais il est toujours deux fois plus lent que Python.la source
Eh bien, je vois que dans votre deuxième solution, vous êtes passé de
cin
àscanf
, ce qui était la première suggestion que j'allais vous faire (cin is sloooooooooooow). Maintenant, si vous passez descanf
àfgets
, vous verriez une autre amélioration des performances:fgets
c'est la fonction C ++ la plus rapide pour l'entrée de chaîne.BTW, je ne savais pas à propos de cette synchronisation, c'est bien. Mais vous devriez quand même essayer
fgets
.la source
fgets
sera faux (en termes de nombre de lignes, et en termes de fractionnement des lignes entre les boucles si vous avez réellement besoin de les utiliser) pour des lignes suffisamment grandes, sans vérification supplémentaire des lignes incomplètes (et tenter de compenser cela implique d'allouer des tampons inutilement grands , oùstd::getline
gère la réallocation pour correspondre parfaitement à l'entrée réelle). Rapide et faux est facile, mais cela vaut presque toujours la peine d'utiliser "légèrement plus lent, mais correct", ce quisync_with_stdio
vous désactive .