Pourquoi la lecture des lignes de stdin est-elle plus lente en C++ que celle de Python?

j'ai voulu 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++ courir un ordre de grandeur plus lent que le code Python équivalent. Puisque mon C++ est rouillé et que je ne suis pas encore un expert en Pythonista, s'il vous plaît dites-moi si je fais quelque chose de mal ou si je me méprends sur quelque chose.


(réponse TLDR: inclure l'énoncé: cin.sync_with_stdio(false) ou tout simplement utiliser fgets à la place.

TLDR résultats: faites défiler tout le chemin vers le bas de ma question et de regarder le tableau.)


le 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é cela à la fois 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

Minuscule indice additif et récapitulatif

pour être complet, j'ai pensé mettre à jour la vitesse de lecture pour le même fichier sur la même boîte avec le code C++ original (synchronisé). Encore une fois, c'est pour un fichier de ligne de 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
1512
demandé sur JJC 2012-02-21 06:17:50
la source

10 ответов

par défaut, cin est synchronisé avec stdio, ce qui lui permet d'éviter toute mise en tampon d'entrée. Si vous ajoutez cela au haut de votre main, vous devriez voir une bien meilleure performance:

std::ios_base::sync_with_stdio(false);

normalement, lorsqu'un flux d'entrée est tamponné, au lieu de lire un caractère à la fois, le flux sera lu en gros morceaux. Cela réduit le nombre d'appels système, qui sont généralement relativement coûteux. Toutefois, depuis le FILE* basé stdio et iostreams ont souvent des implémentations séparées et donc des tampons séparés, ce qui pourrait conduire à un problème si les deux étaient utilisés ensemble. Par exemple:

int myvalue1;
cin >> myvalue1;
int myvalue2;
scanf("%d",&myvalue2);

si plus d'entrées étaient lues par cin qu'il n'en fallait, alors la deuxième valeur entière ne serait pas disponible pour la fonction scanf , qui a son propre buffer 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'avoir cin lire chaque caractère un à la fois si nécessaire en utilisant les fonctions stdio . Malheureusement, cela introduit beaucoup de frais généraux. Pour de petites quantités d'entrées, ce n'est pas un gros problème, mais quand 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 être en mesure de désactiver cette fonctionnalité pour obtenir amélioration de la performance si vous saviez ce que vous faisiez, ils ont donc fourni la méthode sync_with_stdio .

1341
répondu Vaughn Cato 2018-01-30 05:38:39
la source

juste par curiosité, j'ai regardé ce qui se passe sous le capot, et j'ai utilisé dtruss/strace sur chaque test.

C++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

appels sudo dtruss -c ./a.out < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            6
pread                                           8
mprotect                                       17
mmap                                           22
stat64                                         30
read_nocancel                               25958

Python

./a.py < in
Read 6512402 lines in 1 seconds. LPS: 6512402

appels sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29
127
répondu 2mia 2014-04-02 22:27:57
la source

je suis quelques années de retard ici, mais:

Dans "Modifier les 4/5/6" de la poste, vous êtes à l'aide de la construction:

$ /usr/bin/time cat big_file | program_to_benchmark

C'est mal de plusieurs façons:

  1. vous chronométrez l'exécution de "cat", pas votre benchmark. L'utilisation des processeurs' user 'et' sys `affichée par` time `est celle de` cat', pas celle de votre programme référencé. Pire encore, le temps "réel" n'est pas non plus nécessairement exactes. Selon l'implémentation de `cat` et de pipelines dans votre système D'exploitation local, il est possible que `cat` écrive un tampon géant final et sorte bien avant que le processus de lecture ne termine son travail.

  2. 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 CPU et -- dans certaines générations d'ordinateurs -- I/O plus rapide que le CPU) -- le simple fait que "chat", était en cours d'exécution pourrait considérablement couleur les résultats. Vous êtes également soumis à la mise en mémoire tampon des entrées et des sorties et à d'autres traitements "cat". (Cela vous rapporterait probablement un 'Useless Use of Cat' award si J'étais Randal Schwartz.

une meilleure construction serait:

$ /usr/bin/time program_to_benchmark < big_file

dans cette déclaration, c'est le shell qui ouvre big_file, le passer à votre programme (et, en fait de "temps" qui exécute votre programme comme un sous-processus) comme un descripteur de fichier ouvert. 100% de la lecture de fichier est strictement la responsabilité du programme que vous essayez de benchmark. Cela vous donne une lecture réelle de sa performance sans complications fallacieuses.

je mentionnerai deux "corrections" possibles, mais en fait erronées, qui pourraient également être considérées (mais je les "numérote" différemment car ce ne sont pas des choses qui étaient faux dans le post original):

A. Vous pourriez 'corriger' cela en chronométrant seulement votre programme:

$ cat big_file | /usr/bin/time program_to_benchmark

B. ou en chronométrant la totalité du pipeline:

$ /usr/bin/time sh -c 'cat big_file | program_to_benchmark'

ce sont des erreurs pour les mêmes raisons que #2: ils utilisent encore `cat` inutilement. Je les mentionne pour quelques raisons:

  • ils sont plus "naturels" pour les gens qui ne sont pas tout à fait à l'aise avec l'E/S installations de redirection 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 à être référencé: "sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output")

    1519170920"
  • dans la pratique , sur les machines modernes, le " cat "ajouté dans le pipeline n'a probablement pas de conséquence réelle

mais je dis cette dernière chose avec une certaine hésitation. Si nous examinons le dernier résultat dans "Modifier 5' --

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

-- cela signifie que " cat " a consommé 74% du CPU pendant l'essai; et en effet 1,34/1,83 est d'environ 74%. Peut-être une série de:

$ /usr/bin/time wc -l < temp_big_file

aurait pris que le reste .49 secondes! Probablement pas: `cat` ici, il a fallu payer pour les appels système de read () (ou l'équivalent) qui ont transféré le fichier à partir du 'disque' (en fait le cache tampon), ainsi que les Écritures pipe pour les livrer à `wc`. Le test correct aurait quand même dû faire ces appels read (); seuls les appels write-to-pipe et read-from-pipe auraient été sauvegardés, et ceux-ci devraient être assez bon marché.

encore, je prédis que vous seriez en mesure de mesurer la différence entre `cat file | wc-l` et `wc-l < file` et de trouver un différence notable (pourcentage à deux chiffres). Chacun des tests plus lents aura payé une pénalité semblable en temps absolu, ce qui représenterait toutefois une fraction plus petite de son temps total plus important.

en fait, j'ai fait quelques tests rapides avec un fichier de déchets de 1,5 gigaoctet, sur un système Linux 3.13 (Ubuntu 14.04), l'obtention de ces résultats (ce sont en fait 'best of 3' résultats; après l'amorçage du cache, bien sûr):

$ time wc -l < /tmp/junk
real 0.280s user 0.156s sys 0.124s (total cpu 0.280s)
$ time cat /tmp/junk | wc -l
real 0.407s user 0.157s sys 0.618s (total cpu 0.775s)
$ time sh -c 'cat /tmp/junk | wc -l'
real 0.411s user 0.118s sys 0.660s (total cpu 0.778s)

Notez que les deux les résultats du pipeline indiquent qu'il a fallu plus de temps CPU (user+sys) que de temps réel. C'est parce que j'utilise la commande'time 'intégrée de shell (Bash), qui est consciente du pipeline; et je suis sur une machine multi-core où les processus séparés dans un pipeline peuvent utiliser des noyaux séparés, accumulant le temps CPU plus rapidement que le temps réel. En utilisant /usr/bin / time, je vois que le temps CPU est plus petit que le temps réel -- ce qui montre qu'il ne peut chronométrer que l'élément de pipeline unique qui lui est passé sur sa ligne de commande. Aussi, le shell la sortie donne des millisecondes alors que /usr/bin/time ne donne que des centièmes de seconde.

ainsi au niveau d'efficacité de 'wc-l`, Le` cat' fait une énorme différence: 409 / 283 = 1,453 ou 45,3% plus de temps réel, et 775 / 280 = 2,768, ou un énorme 177% plus de CPU utilisé! Sur mon hasard-est-il-à-la-fois le test de la boîte.

je dois ajouter qu'il y a au moins une autre différence significative entre ces styles de test, et je ne peux pas dire si c'est un avantage ou faute; vous devez décider vous-même:

quand vous lancez ' cat big_file | / usr/bin / time my_program`, votre programme reçoit des entrées à partir d'un pipe, au rythme précis envoyé par `cat`, et en morceaux pas plus grands qu'écrits par `cat`.

lorsque vous exécutez `/usr/bin/time my_program < big_file`, votre programme reçoit un descripteur de fichier ouvert au fichier réel. Votre programme -- ou dans de nombreux cas, les bibliothèques e/s de la langue dans lequel il a été écrit -- peut prendre différentes mesures lorsqu'il est présenté avec un descripteur de fichier référencement d'un fichier régulier. Il peut utiliser mmap(2) pour mapper le fichier d'entrée dans son espace d'adressage, au lieu d'utiliser explicite en lecture(2) les appels système. Ces différences pourraient avoir un effet beaucoup plus grand sur vos résultats de référence que le petit coût d'exécution du binaire "cat".

bien sûr, c'est un résultat de référence intéressant si le même programme fonctionne de manière très 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 d'intéressant, comme utiliser mmap(). Ainsi, dans la pratique, il pourrait être bon d'utiliser les points de référence dans les deux sens; en faisant peut-être Abstraction du résultat du "chat" par un petit facteur pour "pardonner" le coût d'exécution du "chat" lui-même.

96
répondu Bela Lubkin 2018-08-06 00:47:30
la source

j'ai reproduit le résultat original sur mon ordinateur en utilisant g++ sur un Mac.

ajouter les énoncés suivants à la version C++ juste avant la boucle while le met en ligne avec la version Python :

std::ios_base::sync_with_stdio(false);
char buffer[1048576];
std::cin.rdbuf()->pubsetbuf(buffer, sizeof(buffer));

sync_with_stdio a amélioré la vitesse à 2 secondes, et le réglage d'un tampon plus grand l'a ramené à 1 seconde.

78
répondu karunski 2014-04-02 22:26:00
la source

getline , les opérateurs de flux, scanf , peut être pratique si vous ne vous souciez pas du temps de chargement du fichier ou si vous chargez de petits fichiers de texte. Mais, si la performance est quelque chose que vous vous souciez, vous devriez vraiment juste amortir le dossier entier dans la mémoire (en supposant qu'il conviendra).

voici un exemple:

//open file in binary mode
std::fstream file( filename, std::ios::in|::std::ios::binary );
if( !file ) return NULL;

//read the size...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

//read into memory buffer, then close it.
char *filebuf = new char[length+1];
file.read(filebuf, length);
filebuf[length] = '"151900920"'; //make it null-terminated
file.close();

si vous voulez, vous pouvez enrouler un ruisseau autour de cette zone tampon pour un accès plus pratique comme ceci:

std::istrstream header(&filebuf[0], length);

aussi, si vous êtes en contrôle du fichier, envisager d'utiliser un format de données binaires Plat au lieu de texte. Il est plus fiable pour lire et écrire parce que vous n'avez pas à faire face à toutes les ambiguïtés de whitespace. Il est également plus petit et beaucoup plus rapide à analyser.

27
répondu Stu 2018-05-28 20:55:09
la source

soit dit en passant, la raison pour laquelle le nombre de lignes pour la version c++ est un plus grand que le nombre pour la version Python est que le drapeau eof n'est activé que lorsqu'une tentative est faite pour lire au-delà de eof. Donc la bonne boucle serait:

while (cin) {
    getline(cin, input_line);

    if (!cin.eof())
        line_count++;
};
11
répondu Gregg 2012-03-12 08:04:01
la source

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 de caractères et cherche n'importe quel caractère d'Espace (Espace, tab, newline).

aussi, oui, CPython fait de la mise en cache pour éviter les lectures de disque dur.

10
répondu davinchi 2012-02-21 07:32:17
la source

le code suivant a été plus rapide pour moi que l'autre code posté ici jusqu'à présent: (Visual Studio 2013, 64-bit, Fichier de 500 Mo avec une longueur de ligne uniforme en [0, 1000))).

const int buffer_size = 500 * 1024;  // Too large/small buffer is not good.
std::vector<char> buffer(buffer_size);
int size;
while ((size = fread(buffer.data(), sizeof(char), buffer_size, stdin)) > 0) {
    line_count += count_if(buffer.begin(), buffer.begin() + size, [](char ch) { return ch == '\n'; });
}

il bat toutes mes tentatives de Python par Plus d'un facteur 2.

10
répondu Petter 2014-04-23 18:56:24
la source

Un premier élément de réponse: <iostream> est lente. Sacrément lent. Je reçois un énorme coup de pouce de performance avec scanf comme dans le ci-dessous, mais il est toujours deux fois plus lent que Python.

#include <iostream>
#include <time.h>
#include <cstdio>

using namespace std;

int main() {
    char buffer[10000];
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    int read = 1;
    while(read > 0) {
        read = scanf("%s", buffer);
        line_count++;
    };
    sec = (int) time(NULL) - start;
    line_count--;
    cerr << "Saw " << line_count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}
8
répondu J.N. 2014-04-02 22:25:14
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 est sloooooooooooooooow). Maintenant, si vous passez de scanf à fgets , vous verrez un autre boost dans la performance: fgets est la fonction C++ la plus rapide pour l'entrée de chaîne de caractères.

BTW, Je ne savais pas pour la synchronisation, nice. Mais vous devriez quand même essayer fgets .

7
répondu José Ernesto Lara Rodríguez 2017-08-30 03:16:25
la source

Autres questions sur c++ python iostream benchmarking getline