Avis
Voir aussi cette réponse : https://stackoverflow.com/a/21708215 qui a servi de base à la EDIT 2 en bas ici.
J'ai augmenté la boucle à 1000000 pour obtenir une meilleure mesure du temps.
C'est mon timing Python :
real 0m2.038s
user 0m2.009s
sys 0m0.024s
Voici un équivalent de votre code, juste un peu plus joli :
#include <regex>
#include <vector>
#include <string>
std::vector<std::string> split(const std::string &s, const std::regex &r)
{
return {
std::sregex_token_iterator(s.begin(), s.end(), r, -1),
std::sregex_token_iterator()
};
}
int main()
{
const std::regex r(" +");
for(auto i=0; i < 1000000; ++i)
split("a b c", r);
return 0;
}
Timing :
real 0m5.786s
user 0m5.779s
sys 0m0.005s
Il s'agit d'une optimisation pour éviter la construction/allocation d'objets vectoriels et de chaînes de caractères :
#include <regex>
#include <vector>
#include <string>
void split(const std::string &s, const std::regex &r, std::vector<std::string> &v)
{
auto rit = std::sregex_token_iterator(s.begin(), s.end(), r, -1);
auto rend = std::sregex_token_iterator();
v.clear();
while(rit != rend)
{
v.push_back(*rit);
++rit;
}
}
int main()
{
const std::regex r(" +");
std::vector<std::string> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v);
return 0;
}
Timing :
real 0m3.034s
user 0m3.029s
sys 0m0.004s
Cela représente une amélioration des performances de près de 100 %.
Le vecteur est créé avant la boucle, et peut augmenter sa mémoire lors de la première itération. Ensuite, il n'y a pas d'allocation de mémoire par clear()
le vecteur maintient la mémoire et construit des chaînes de caractères. en place .
Une autre amélioration des performances consisterait à éviter la construction/destruction. std::string
complètement, et donc l'allocation/désallocation de ses objets.
Il s'agit d'une tentative dans cette direction :
#include <regex>
#include <vector>
#include <string>
void split(const char *s, const std::regex &r, std::vector<std::string> &v)
{
auto rit = std::cregex_token_iterator(s, s + std::strlen(s), r, -1);
auto rend = std::cregex_token_iterator();
v.clear();
while(rit != rend)
{
v.push_back(*rit);
++rit;
}
}
Timing :
real 0m2.509s
user 0m2.503s
sys 0m0.004s
Une amélioration ultime serait d'avoir un std::vector
de const char *
comme retour, où chaque pointeur de caractère pointerait vers une sous-chaîne à l'intérieur de l'original s
c chaîne même. Le problème est que vous ne pouvez pas le faire parce que chacun d'entre eux ne serait pas terminé par un null (pour cela, voir l'utilisation de C++1y). string_ref
dans un échantillon ultérieur).
Cette dernière amélioration pourrait également être obtenue grâce à cela :
#include <regex>
#include <vector>
#include <string>
void split(const std::string &s, const std::regex &r, std::vector<std::string> &v)
{
auto rit = std::cregex_token_iterator(s.data(), s.data() + s.length(), r, -1);
auto rend = std::cregex_token_iterator();
v.clear();
while(rit != rend)
{
v.push_back(*rit);
++rit;
}
}
int main()
{
const std::regex r(" +");
std::vector<std::string> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v); // the constant string("a b c") should be optimized
// by the compiler. I got the same performance as
// if it was an object outside the loop
return 0;
}
J'ai construit les échantillons avec clang 3.3 (à partir du tronc) avec -O3. Peut-être que d'autres bibliothèques de regex sont plus performantes, mais dans tous les cas, les allocations/désallocations sont souvent un problème de performance.
Boost.Regex
C'est le boost::regex
le timing pour le c chaîne échantillon d'arguments :
real 0m1.284s
user 0m1.278s
sys 0m0.005s
Même code, boost::regex
y std::regex
dans cet exemple sont identiques, il suffit de changer l'espace de noms et l'inclusion.
Je vous souhaite de vous améliorer avec le temps, les implémentations C++ stdlib regex n'en sont qu'à leurs débuts.
EDIT
Pour être complet, j'ai essayé ceci (la suggestion "d'amélioration ultime" mentionnée ci-dessus) et cela n'a pas amélioré les performances de l'équivalent std::vector<std::string> &v
version en quoi que ce soit :
#include <regex>
#include <vector>
#include <string>
template<typename Iterator> class intrusive_substring
{
private:
Iterator begin_, end_;
public:
intrusive_substring(Iterator begin, Iterator end) : begin_(begin), end_(end) {}
Iterator begin() {return begin_;}
Iterator end() {return end_;}
};
using intrusive_char_substring = intrusive_substring<const char *>;
void split(const std::string &s, const std::regex &r, std::vector<intrusive_char_substring> &v)
{
auto rit = std::cregex_token_iterator(s.data(), s.data() + s.length(), r, -1);
auto rend = std::cregex_token_iterator();
v.clear(); // This can potentially be optimized away by the compiler because
// the intrusive_char_substring destructor does nothing, so
// resetting the internal size is the only thing to be done.
// Formerly allocated memory is maintained.
while(rit != rend)
{
v.emplace_back(rit->first, rit->second);
++rit;
}
}
int main()
{
const std::regex r(" +");
std::vector<intrusive_char_substring> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v);
return 0;
}
Cela a à voir avec le Proposition de array_ref et string_ref . Voici un exemple de code l'utilisant :
#include <regex>
#include <vector>
#include <string>
#include <string_ref>
void split(const std::string &s, const std::regex &r, std::vector<std::string_ref> &v)
{
auto rit = std::cregex_token_iterator(s.data(), s.data() + s.length(), r, -1);
auto rend = std::cregex_token_iterator();
v.clear();
while(rit != rend)
{
v.emplace_back(rit->first, rit->length());
++rit;
}
}
int main()
{
const std::regex r(" +");
std::vector<std::string_ref> v;
for(auto i=0; i < 1000000; ++i)
split("a b c", r, v);
return 0;
}
Il sera également plus économique de retourner un vecteur de string_ref
plutôt que string
copies pour le cas de split
avec retour vectoriel.
EDIT 2
Cette nouvelle solution est capable d'obtenir la sortie par retour. J'ai utilisé la méthode de Marshall Clow string_view
( string_ref
(qui a été renommé) de libc++ qui se trouve à l'adresse https://github.com/mclow/string_view .
#include <string>
#include <string_view>
#include <boost/regex.hpp>
#include <boost/range/iterator_range.hpp>
#include <boost/iterator/transform_iterator.hpp>
using namespace std;
using namespace std::experimental;
using namespace boost;
string_view stringfier(const cregex_token_iterator::value_type &match) {
return {match.first, static_cast<size_t>(match.length())};
}
using string_view_iterator =
transform_iterator<decltype(&stringfier), cregex_token_iterator>;
iterator_range<string_view_iterator> split(string_view s, const regex &r) {
return {
string_view_iterator(
cregex_token_iterator(s.begin(), s.end(), r, -1),
stringfier
),
string_view_iterator()
};
}
int main() {
const regex r(" +");
for (size_t i = 0; i < 1000000; ++i) {
split("a b c", r);
}
}
Timing :
real 0m0.385s
user 0m0.385s
sys 0m0.000s
Notez à quel point ce résultat est plus rapide que les résultats précédents. Bien sûr, il ne s'agit pas de remplir un vector
à l'intérieur de la boucle (et il est probable qu'il n'y ait pas non plus de correspondance à l'avance), mais vous obtenez tout de même une plage, sur laquelle vous pouvez étendre la plage avec la fonction for
ou même l'utiliser pour remplir un vector
.
Comme on peut le voir sur le iterator_range
crée string_view
sur un original string
(ou un chaîne de caractères à terminaison nulle ), cela devient très léger, ne générant jamais d'allocations de chaînes inutiles.
Juste pour comparer en utilisant ceci split
mais qui remplit en fait une vector
nous pourrions le faire :
int main() {
const regex r(" +");
vector<string_view> v;
v.reserve(10);
for (size_t i = 0; i < 1000000; ++i) {
copy(split("a b c", r), back_inserter(v));
v.clear();
}
}
Ceci utilise l'algorithme de copie de plage de boost pour remplir le vecteur à chaque itération, le timing est :
real 0m1.002s
user 0m0.997s
sys 0m0.004s
Comme on peut le voir, il n'y a pas de grande différence par rapport à la méthode optimisée. string_view
sortie param version.
Notez également qu'il y a une proposition de std::split
qui fonctionnerait comme ceci.
9 votes
Utilisez-vous une version de débogage ? Lorsque vous utilisez des modèles, assurez-vous que les options sont activées et que le débogage est désactivé. Sinon, de nombreux contrôles de sécurité se retrouvent dans votre code.
0 votes
Non juste clang++ -o test.o -c -std=c++11 -stdlib=libc++ -Wall -O3 test.cpp
0 votes
Eh bien, Python est génial, il faut donc s'y attendre.
26 votes
Ils ne font pas la même chose. Par exemple, le code C++ fait la concaténation de chaînes de caractères et le code Python ne la fait pas.
0 votes
Je pense que vous passez beaucoup de temps à créer des chaînes temporaires en C++. Et peut-être
tosplit + '+'
pourrait être plus rapide quetosplit + "+"
.0 votes
@interjay bon point mais cela ne fait pas de différence. J'ai mis à jour ma question
21 votes
Dans le cas de Python, la regex peut être compilée/optimisée une seule fois. La bibliothèque de regex C++ va construire et optimiser la regex encore et encore. Juste pour mémoire, essayez de définir l'élément
rsplit
regex comme une constante statique. Dans le cas de Python, la bibliothèque re peut travailler avec le compilateur en maintenant une liste de regex optimisés.1 votes
Vous êtes potentiellement en train de faire un nombre énorme d'expressions régulières dans la version C++. Que se passe-t-il si vous déplacez la déclaration de
rsplit
à l'extérieur desplit
?0 votes
En le rendant statique ou en le déplaçant à l'extérieur, on descend à 0,09 en utilisant -O3
1 votes
Ok, donc une optimisation vous a rapporté environ 20%. Maintenant, trouvez la suivante :) N'oubliez pas que Python (l'interpréteur) a déjà mis en œuvre la plupart de ces optimisations.
0 votes
Augmentez également le nombre d'exécutions de boucles pour que le timing soit plus cohérent.
0 votes
@ Diego Sevilla augmentant à 100000 dans pytohnn 0.31 vs c++11 -O4 0.9s
5 votes
Très lié : stackoverflow.com/questions/9378500/
9 votes
C'est la raison pour laquelle les gens utilisent Python pour ce genre de tâches : il dispense le programmeur de la nécessité d'entrer dans ces analyses très techniques de l'impact sur les performances.
0 votes
@NateKohl - Bonne réponse. En outre, je suppose que toute la gestion de la mémoire (commutation entre Iterator et vecteur). Essayez de retourner
rit
immédiatement, et voyez si vous ne voyez pas une augmentation massive des performances.0 votes
@FrankieTheKneeMan : Je l'ai fait et l'augmentation des performances est massive, mais python met tout dans une liste.
1 votes
Bien sûr que oui, car c'est la façon pythonique de traiter cet objet. Mais le C++ (ou plutôt, celui qui a écrit cette bibliothèque) ne voit pas le monde de la même manière que Python, et en tant que tel, ne vous donne que l'option d'utiliser un itérateur. Je parie que vous pourriez écrire vous-même une implémentation assez efficace en C++, mais vous avez spécifiquement choisi une manière inefficace d'implémenter la solution. L'efficacité est un jeu de pouces, et alors que je suis sûr que Python n'itère sur la chaîne qu'une seule fois pendant la correspondance, vous le faites au moins deux fois, si ce n'est plus, selon la façon dont s.end() est implémenté.
0 votes
Quel CRT/heap utilisez-vous ? Python dispose d'un petit tas de blocs optimisé, de sorte que l'allocation et, plus important encore, la désallocation d'une chaîne de caractères "a" ne sont pas aussi douloureuses que dans une bibliothèque C++ qui n'en dispose pas. Je pense que c'est la source de nombreux problèmes du type "pourquoi le langage de script est-il plus rapide que le C++".
0 votes
Ben : les chaînes de caractères plus courtes que 16 caractères sont allouées sur la pile dans toutes les implémentations stl que j'ai vues.
9 votes
Je peux approximativement reproduire vos résultats, et le simple remplacement de std::regex de libc++ par boost::regex permet à la version C++ de battre python d'environ 10-15%. Je ne pense pas que l'implémentation de libc++ soit encore particulièrement efficace.
0 votes
@Cubbi : merci, je vais jeter un coup d'oeil à l'implémentation.
4 votes
Il me semble qu'avoir un
vector<string> res
effectivement alloué dansmain()
en disantres = split("a b c")
pourrait donner à l'optimiseur plus d'éléments sur lesquels travailler (modification : ou de toute façon un autre motif à faire correspondre) -- tel qu'il est, il n'y a pas d'endroit pour que le code mette le retourres
donc, puisque le compilateur n'élide pas simplement le programme entier comme le no-op qu'il est, il y a des limites à l'analyse qu'il fait. Avez-vous essayé ceci avec un gcc récent ?0 votes
@ViktorSehr : Vous n'avez pas vu l'implémentation de libstdc++ (avant C++11) ; elle utilisait COW et nécessitait une allocation de mémoire.
0 votes
Il y a une proposition dans C++1y pour
std::basic_string_ref
une interface de type chaîne de caractères sans propriété de la mémoire sous-jacente. J'espère qu'ils la rendront compatible avec ce code !