J'essaie de convertir du code de Python en C ++ dans le but de gagner un peu de vitesse et d'aiguiser mes compétences en C ++. Hier, j'ai été choqué lorsqu'une implémentation naïve de lecture de lignes depuis stdin était beaucoup plus rapide en Python qu'en C ++ (voir ceci ). Aujourd'hui, j'ai enfin compris comment diviser une chaîne en C ++ avec des délimiteurs de fusion (sémantique similaire à celle de python split ()), et je fais maintenant l'expérience du déjà vu! Mon code C ++ prend beaucoup plus de temps pour faire le travail (mais pas un ordre de grandeur de plus, comme ce fut le cas pour la leçon d'hier).
Code Python:
#!/usr/bin/env python
from __future__ import print_function
import time
import sys
count = 0
start_time = time.time()
dummy = None
for line in sys.stdin:
dummy = line.split()
count += 1
delta_sec = int(time.time() - start_time)
print("Python: Saw {0} lines in {1} seconds. ".format(count, delta_sec), end='')
if delta_sec > 0:
lps = int(count/delta_sec)
print(" Crunch Speed: {0}".format(lps))
else:
print('')
Code C ++:
#include <iostream>
#include <string>
#include <sstream>
#include <time.h>
#include <vector>
using namespace std;
void split1(vector<string> &tokens, const string &str,
const string &delimiters = " ") {
// Skip delimiters at beginning
string::size_type lastPos = str.find_first_not_of(delimiters, 0);
// Find first non-delimiter
string::size_type pos = str.find_first_of(delimiters, lastPos);
while (string::npos != pos || string::npos != lastPos) {
// Found a token, add it to the vector
tokens.push_back(str.substr(lastPos, pos - lastPos));
// Skip delimiters
lastPos = str.find_first_not_of(delimiters, pos);
// Find next non-delimiter
pos = str.find_first_of(delimiters, lastPos);
}
}
void split2(vector<string> &tokens, const string &str, char delim=' ') {
stringstream ss(str); //convert string to stream
string item;
while(getline(ss, item, delim)) {
tokens.push_back(item); //add token to vector
}
}
int main() {
string input_line;
vector<string> spline;
long count = 0;
int sec, lps;
time_t start = time(NULL);
cin.sync_with_stdio(false); //disable synchronous IO
while(cin) {
getline(cin, input_line);
spline.clear(); //empty the vector for the next line to parse
//I'm trying one of the two implementations, per compilation, obviously:
// split1(spline, input_line);
split2(spline, input_line);
count++;
};
count--; //subtract for final over-read
sec = (int) time(NULL) - start;
cerr << "C++ : Saw " << count << " lines in " << sec << " seconds." ;
if (sec > 0) {
lps = count / sec;
cerr << " Crunch speed: " << lps << endl;
} else
cerr << endl;
return 0;
//compiled with: g++ -Wall -O3 -o split1 split_1.cpp
Notez que j'ai essayé deux implémentations séparées différentes. One (split1) utilise des méthodes de chaîne pour rechercher des jetons et est capable de fusionner plusieurs jetons ainsi que de gérer de nombreux jetons (cela vient d' ici ). Le second (split2) utilise getline pour lire la chaîne en tant que flux, ne fusionne pas les délimiteurs et ne prend en charge qu'un seul caractère de délimitation (celui-ci a été publié par plusieurs utilisateurs de StackOverflow dans les réponses aux questions de fractionnement de chaîne).
J'ai couru cela plusieurs fois dans divers ordres. Ma machine de test est un Macbook Pro (2011, 8 Go, Quad Core), pas que cela compte beaucoup. Je teste avec un fichier texte de 20 millions de lignes avec trois colonnes séparées par des espaces qui ressemblent chacune à ceci: "foo.bar 127.0.0.1 home.foo.bar"
Résultats:
$ /usr/bin/time cat test_lines_double | ./split.py
15.61 real 0.01 user 0.38 sys
Python: Saw 20000000 lines in 15 seconds. Crunch Speed: 1333333
$ /usr/bin/time cat test_lines_double | ./split1
23.50 real 0.01 user 0.46 sys
C++ : Saw 20000000 lines in 23 seconds. Crunch speed: 869565
$ /usr/bin/time cat test_lines_double | ./split2
44.69 real 0.02 user 0.62 sys
C++ : Saw 20000000 lines in 45 seconds. Crunch speed: 444444
Qu'est-ce que je fais mal? Existe-t-il un meilleur moyen de fractionner des chaînes en C ++ qui ne repose pas sur des bibliothèques externes (c'est-à-dire pas de boost), prend en charge la fusion de séquences de délimiteurs (comme le fractionnement de python), est thread-safe (donc pas de strtok), et dont les performances sont au moins à égalité avec python?
Modifier 1 / Solution partielle?:
J'ai essayé d'en faire une comparaison plus juste en demandant à python de réinitialiser la liste factice et de l'ajouter à chaque fois, comme le fait C ++. Ce n'est toujours pas exactement ce que fait le code C ++, mais c'est un peu plus proche. Fondamentalement, la boucle est maintenant:
for line in sys.stdin:
dummy = []
dummy += line.split()
count += 1
Les performances de python sont désormais à peu près les mêmes que celles de l'implémentation C ++ de split1.
/usr/bin/time cat test_lines_double | ./split5.py
22.61 real 0.01 user 0.40 sys
Python: Saw 20000000 lines in 22 seconds. Crunch Speed: 909090
Je suis toujours surpris que, même si Python est tellement optimisé pour le traitement des chaînes (comme Matt Joiner l'a suggéré), ces implémentations C ++ ne seraient pas plus rapides. Si quelqu'un a des idées sur la façon de faire cela de manière plus optimale en utilisant C ++, veuillez partager votre code. (Je pense que ma prochaine étape consistera à essayer de l'implémenter en C pur, bien que je ne fasse pas de compromis sur la productivité du programmeur pour réimplémenter mon projet global en C, donc ce ne sera qu'une expérience pour la vitesse de division des chaînes.)
Merci a tous pour votre aide.
Modification finale / solution:
Veuillez voir la réponse acceptée d'Alf. Étant donné que python traite les chaînes strictement par référence et que les chaînes STL sont souvent copiées, les performances sont meilleures avec les implémentations python vanilla. À titre de comparaison, j'ai compilé et exécuté mes données via le code d'Alf, et voici les performances sur la même machine que toutes les autres exécutions, essentiellement identiques à l'implémentation naïve de python (bien que plus rapide que l'implémentation de python qui réinitialise / ajoute la liste, comme montré dans l'édition ci-dessus):
$ /usr/bin/time cat test_lines_double | ./split6
15.09 real 0.01 user 0.45 sys
C++ : Saw 20000000 lines in 15 seconds. Crunch speed: 1333333
Mon seul petit reproche restant concerne la quantité de code nécessaire pour que C ++ fonctionne dans ce cas.
L'une des leçons tirées de ce numéro et du problème de lecture de la ligne stdin d'hier (lié ci-dessus) est qu'il faut toujours comparer au lieu de faire des hypothèses naïves sur les performances relatives "par défaut" des langues. J'apprécie l'éducation.
Merci encore à tous pour vos suggestions!