salut je voudrais comprendre pourquoi le code suivant qui fait un split de chaîne en utilisant regex
#include<regex>
#include<vector>
#include<string>
std::vector<std::string> split(const std::string &s){
static const std::regex rsplit(" +");
auto rit = std::sregex_token_iterator(s.begin(), s.end(), rsplit, -1);
auto rend = std::sregex_token_iterator();
auto res = std::vector<std::string>(rit, rend);
return res;
}
int main(){
for(auto i=0; i< 10000; ++i)
split("a b c", " ");
return 0;
}
est plus lent que le code suivant python
import re
for i in range(10000):
re.split(' +', 'a b c')
voici
> python test.py 0.05s user 0.01s system 94% cpu 0.070 total
./test 0.26s user 0.00s system 99% cpu 0.296 total
J'utilise clang ++ sur osx.
la compilation avec -O3 le ramène à 0.09s user 0.00s system 99% cpu 0.109 total
Voir aussi cette réponse: https://stackoverflow.com/a/21708215 qui était la base pour EDIT 2 sur le bas ici.
J'ai augmenté la boucle à 1000000 pour obtenir une meilleure mesure de synchronisation.
C'est mon Python timing:
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;
}
Horaire:
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:
#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;
}
Horaire:
real 0m3.034s
user 0m3.029s
sys 0m0.004s
C'est près d'une amélioration de 100% des performances.
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 de désallocation de mémoire par clear()
, le vecteur maintient la mémoire et construit les chaînes en place .
Une autre augmentation des performances consisterait à éviter complètement la construction/destruction std::string
, Et donc l'allocation/la désallocation de ses objets.
Il s'agit d'une tentative dans ce sens:
#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;
}
}
Horaire:
real 0m2.509s
user 0m2.503s
sys 0m0.004s
Une amélioration ultime serait d'avoir un std::vector
De const char *
En retour, où chaque pointeur de caractère pointerait vers une sous-chaîne à l'intérieur du s
c d'origine chaîne elle-même. Le problème est que vous ne pouvez pas faire cela car chacun d'eux ne serait pas terminé par null (pour cela, voir l'utilisation de C++ 1y string_ref
Dans un exemple ultérieur).
Cette dernière amélioration pourrait également être réalisée avec ceci:
#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 d'expressions régulières sont plus performantes, mais dans tous les cas, les allocations/désallocations sont souvent un problème de performances.
Voici le timing boost::regex
Pour l'exemple d'arguments chaîne c :
real 0m1.284s
user 0m1.278s
sys 0m0.005s
Le même code, l'interface boost::regex
Et std::regex
Dans cet exemple sont identiques, juste nécessaires pour changer l'espace de noms et l'inclure.
Meilleurs voeux pour qu'elle s'améliore avec le temps, les implémentations de regex stdlib C++ en sont à leurs balbutiements.
Pour terminer, j'ai essayé ceci (la suggestion "d'amélioration ultime" mentionnée ci-dessus) et cela n'a amélioré en rien les performances de la version équivalente de std::vector<std::string> &v
:
#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 la proposition 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 moins cher de renvoyer un vecteur de string_ref
Plutôt que string
copies pour le cas de split
avec retour de vecteur.
Cette nouvelle solution est capable d'obtenir une sortie en retour. J'ai utilisé l'implémentation libc ++ de string_view
(string_ref
De Marshall Clow renommée) trouvée à 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);
}
}
Horaire:
real 0m0.385s
user 0m0.385s
sys 0m0.000s
Notez à quel point cela est plus rapide que les résultats précédents. Bien sûr, il ne remplit pas un vector
à l'intérieur de la boucle (ni ne correspond à rien à l'avance probablement aussi), mais vous obtenez quand même une plage, que vous pouvez étendre avec for
basé sur une plage, ou même l'utiliser pour remplir un vector
.
Comme le fait de parcourir le iterator_range
Crée des string_view
Sur une chaîne d'origine string
(ou une chaîne terminée par null null ), cela devient très léger, ne générant jamais d'allocations de chaînes inutiles.
Juste pour comparer en utilisant cette implémentation de split
mais en remplissant réellement un vector
, nous pourrions faire ceci:
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();
}
}
Cela utilise un 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 beaucoup de différence par rapport à la version optimisée du paramètre de sortie string_view
.
Notez également qu'il y a ne proposition pour un std::split
qui fonctionnerait ainsi.
Pour les optimisations, en général, vous voulez éviter deux choses:
Les deux peuvent être antithétiques car parfois cela finit par être plus rapide à calculer quelque chose qu'à mettre tout cela en mémoire ... c'est donc un jeu d'équilibre.
Analysons votre code:
std::vector<std::string> split(const std::string &s){
static const std::regex rsplit(" +"); // only computed once
// search for first occurrence of rsplit
auto rit = std::sregex_token_iterator(s.begin(), s.end(), rsplit, -1);
auto rend = std::sregex_token_iterator();
// simultaneously:
// - parses "s" from the second to the past the last occurrence
// - allocates one `std::string` for each match... at least! (there may be a copy)
// - allocates space in the `std::vector`, possibly multiple times
auto res = std::vector<std::string>(rit, rend);
return res;
}
Pouvons-nous faire mieux? Eh bien, si nous pouvions réutiliser le stockage existant au lieu de conserver l'allocation et la désallocation de mémoire, nous devrions voir une amélioration significative [1]:
// Overwrites 'result' with the matches, returns the number of matches
// (note: 'result' is never shrunk, but may be grown as necessary)
size_t split(std::string const& s, std::vector<std::string>& result){
static const std::regex rsplit(" +"); // only computed once
auto rit = std::cregex_token_iterator(s.begin(), s.end(), rsplit, -1);
auto rend = std::cregex_token_iterator();
size_t pos = 0;
// As long as possible, reuse the existing strings (in place)
for (size_t max = result.size();
rit != rend && pos != max;
++rit, ++pos)
{
result[pos].assign(rit->first, rit->second);
}
// When more matches than existing strings, extend capacity
for (; rit != rend; ++rit, ++pos) {
result.emplace_back(rit->first, rit->second);
}
return pos;
} // split
Dans le test que vous effectuez, où le nombre de sous-correspondances est constant d'une itération à l'autre, il est peu probable que cette version soit battue: elle n'allouera de la mémoire que lors de la première exécution (à la fois pour rsplit
et result
), puis continuez à réutiliser la mémoire existante.
[1]: Avertissement, j'ai seulement prouvé que ce code était correct, je ne l'ai pas testé (comme dirait Donald Knuth).
Et cette verion? Ce n'est pas une expression rationnelle, mais cela résout la scission assez rapidement ...
#include <vector>
#include <string>
#include <algorithm>
size_t split2(const std::string& s, std::vector<std::string>& result)
{
size_t count = 0;
result.clear();
std::string::const_iterator p1 = s.cbegin();
std::string::const_iterator p2 = p1;
bool run = true;
do
{
p2 = std::find(p1, s.cend(), ' ');
result.Push_back(std::string(p1, p2));
++count;
if (p2 != s.cend())
{
p1 = std::find_if(p2, s.cend(), [](char c) -> bool
{
return c != ' ';
});
}
else run = false;
} while (run);
return count;
}
int main()
{
std::vector<std::string> v;
std::string s = "a b c";
for (auto i = 0; i < 100000; ++i)
split2(s, v);
return 0;
}
$ time splittest.exe
réel 0m0.132s utilisateur 0m0.000s sys 0m0.109s