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 rouillées en C++. Hier, j'ai été choqué de constater qu'une implémentation naïve de la lecture de lignes depuis stdin était beaucoup plus rapide en Python qu'en C++ (cf. ce ). Aujourd'hui, j'ai finalement trouvé comment diviser une chaîne de caractères en C++ avec des délimiteurs de fusion (sémantique similaire à celle de split() de Python), et j'ai maintenant une expérience de déjà vu ! Mon code C++ prend beaucoup plus de temps pour faire le travail (mais pas un ordre de grandeur de plus, comme c'était 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 différentes du fractionnement. L'une d'entre elles (split1) utilise des méthodes de chaîne de caractères pour rechercher des tokens et est capable de fusionner plusieurs tokens ainsi que de gérer de nombreux tokens (elle provient de l'application ici ). La seconde (split2) utilise getline pour lire la chaîne en tant que flux, ne fusionne pas les délimiteurs, et ne supporte qu'un seul caractère de délimitation (celle-ci a été postée par plusieurs utilisateurs de StackOverflow en réponse à des questions sur la division des chaînes).
Je l'ai exécuté plusieurs fois dans des ordres différents. Ma machine de test est un Macbook Pro (2011, 8GB, Quad Core), pas que cela importe beaucoup. Je teste avec un fichier texte de 20M 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 de mal ? Existe-t-il une meilleure façon de diviser une chaîne de caractères en C++ qui ne dépende pas de bibliothèques externes (donc pas de boost), qui supporte la fusion de séquences de délimiteurs (comme le split de python), qui soit thread safe (donc pas de strtok), et dont les performances soient au moins équivalentes à celles de python ?
Edit 1 / Partial Solution? :
J'ai essayé de rendre la comparaison plus équitable en faisant en sorte que python réinitialise la liste fictive et l'ajoute à 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. En gros, la boucle est maintenant :
for line in sys.stdin:
dummy = []
dummy += line.split()
count += 1
Les performances de python sont maintenant à 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 de caractères (comme Matt Joiner l'a suggéré), que ces implémentations C++ ne soient pas plus rapides. Si quelqu'un a des idées sur la façon de faire ceci de manière plus optimale en utilisant C++, merci de partager votre code. (Je pense que ma prochaine étape sera d'essayer d'implémenter ceci en C pur, bien que je ne vais pas troquer la productivité du programmeur pour ré-implémenter mon projet global en C, donc ce sera juste une expérience pour la vitesse de séparation des chaînes).
Merci à tous pour votre aide.
Final Edit/Solution :
Veuillez consulter la réponse acceptée d'Alf. Comme 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 vanille. A titre de comparaison, j'ai compilé et exécuté mes données avec le code d'Alf, et voici les performances sur la même machine que toutes les autres exécutions, essentiellement identiques à l'implémentation python naïve (bien que plus rapide que l'implémentation python qui réinitialise/applique la liste, comme indiqué 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
Le seul petit reproche qui me reste concerne la quantité de code nécessaire pour que le C++ fonctionne dans ce cas.
L'une des leçons tirées de ce problème et de celui de la lecture de la ligne stdin d'hier (lien ci-dessus) est qu'il faut toujours procéder à une évaluation comparative au lieu de faire des suppositions naïves sur les performances relatives "par défaut" des langues. J'apprécie cette leçon.
Merci encore à tous pour vos suggestions !
2 votes
Comment avez-vous compilé le programme C++ ? Les optimisations sont-elles activées ?
2 votes
@interjay : C'est dans le dernier commentaire de sa source :
g++ -Wall -O3 -o split1 split_1.cpp
@JJC : Comment se comporte votre référence quand vous utilisez réellementdummy
etspline
respectivement, peut-être que Python supprime l'appel àline.split()
parce qu'il n'a pas d'effets secondaires ?2 votes
Quels résultats obtenez-vous si vous supprimez le fractionnement et ne lisez que les lignes de stdin ?
2 votes
Python est écrit en C. Cela signifie qu'il existe un moyen efficace de le faire, en C. Peut-être existe-t-il un meilleur moyen de diviser une chaîne que d'utiliser la STL ?
0 votes
@interjay Veuillez consulter ma question d'hier (dont le lien figure dans ma question près du sommet). La lecture des lignes de stdin est un peu plus rapide en C++ qu'en Python après avoir désactivé la synchronisation io, c'est-à-dire cin.sync_with_stdio(false).
0 votes
Cela pourrait avoir un rapport avec le push_back qui redimensionne le vecteur en permanence ? Essayez
reserve(200)
et voir ce que ça donne.0 votes
@ixe013 : C'est exactement ce que je pense aussi. J'avais donné une solution pour utiliser soit
reserve()
ou utiliserstd::list
pour repousser les phrases et attribuer la liste au vecteur à la fin.3 votes
Duplicata possible de Pourquoi les opérations std::string sont-elles peu performantes ?
0 votes
@JJC : J'ai modifié ma réponse pour y inclure ce que je pense être à ajouter à votre code Python pour en faire une comparaison "équitable" avec le C++.
0 votes
@MattJoiner Je n'ai pas vu cette question avant, bien que je pense que la discussion générée ici, y compris vos commentaires et ceux d'Alf, méritent d'être préservés en tant que tels. Le code de séparation des chaînes de caractères est également susceptible d'être utile aux programmeurs python apprenant le C++ et qui pourraient rechercher ces expressions exactes (c'est-à-dire python, c++, séparation).
0 votes
@JJC : Pouvez-vous essayer une autre modification mentionnée dans mon message ?
0 votes
Sur mon système,
split2
est un peu plus rapide quesplit.py
et un fractionnement écrit à la main qui correspond exactement à la fonctionnalité de Python est plus de deux fois plus rapide que le fractionnement de Python.split.py
.0 votes
@n.m. Hmmm, je me demande si cela ne dépend pas des données. J'ai exécuté mes benshmarks sur deux machines différentes et j'ai obtenu des résultats cohérents. Pourriez-vous partager votre code fractionné écrit à la main ?
0 votes
@ViteFalcon Je l'ai essayé, j'ai posté les résultats à votre réponse.
0 votes
@JJC : Merci de l'avoir essayé. Cela a également permis de dissiper certains de mes doutes. Une très bonne question et il est bon de savoir que vous avez trouvé une réponse. J'ai noté et ajouté une étoile :)
0 votes
@ViteFalcon Merci et merci pour vos suggestions ! Cela m'a ouvert les yeux.
0 votes
Mon code est similaire à celui de la réponse acceptée, sauf que je n'utilise que le code normal.
std::string
s partout et non StringRef.0 votes
@n.m. Pouvez-vous le partager quand même ? Si vous n'avez pas eu besoin d'écrire votre propre classe StringRef alors votre code sera plus parcimonieux et donc une meilleure solution pour la plupart des gens. S'il vous plaît, partagez, je promets de l'upvoter ;-)
0 votes
@JJC : l'article suivant donne une assez bonne implémentation de la séparation des chaînes de caractères en c++ : codeproject.com/Articles/23198/C-String-Toolkit-StrTk-Tokenizer
0 votes
Une partie du problème est que vous n'utilisez pas les données. Si vous voyez les résultats en comptant le nombre de mots et de caractères ( github.com/tobbez/string-splitting/pull/2 ), alors presque toutes les versions C/C++ battent les versions Python.