C++11 регулярное выражение медленнее, чем python
Привет я хотел бы понять, почему следующий код, который делает разбивает строку с помощью регулярных выражений
#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;
}
медленнее, чем следующий код python
import re
for i in range(10000):
re.split(' +', 'a b c')
вот!--5-->
> 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
Im с помощью clang++ на osx.
компиляция с -O3 сводит его к0.09s user 0.00s system 99% cpu 0.109 total
4 ответа:
обратите внимание
Смотрите также этот ответ:https://stackoverflow.com/a/21708215 который был основой для EDIT 2 внизу здесь.
я увеличил цикл до 1000000, чтобы получить лучшую временную меру.
это мой Python timing:
real 0m2.038s user 0m2.009s sys 0m0.024s
вот эквивалент вашего кода, только немного красивее:
#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; }
сроки:
real 0m5.786s user 0m5.779s sys 0m0.005s
это оптимизация во избежание построения / выделения векторных и строковых объектов:
#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; }
сроки:
real 0m3.034s user 0m3.029s sys 0m0.004s
это почти 100% улучшение производительности.
вектор создается до цикла, и может увеличить свою память в первой итерации. После этого нет никакого освобождения памяти
clear()
вектор поддерживает память и построения строк на месте.
еще одно повышение производительности будет чтобы избежать строительства/разрушения
std::string
полностью, и, следовательно, выделение/освобождение своих объектов.это предварительный вариант в этом направлении:
#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; } }
сроки:
real 0m2.509s user 0m2.503s sys 0m0.004s
конечным улучшением было бы иметь
std::vector
наconst char *
как возврат, где каждый указатель char будет указывать на подстроку внутри оригиналаs
строка c сам по себе. Проблема в том, что вы не можете этого сделать, потому что каждый из них не будет null terminated (для этого см. Использование C++1ystring_ref
в более поздний образец).
это последнее улучшение может быть достигнуто с помощью этого:
#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; }
я построил образцы с clang 3.3 (из ствола) с-O3. Возможно, другие библиотеки регулярных выражений могут работать лучше, но в любом случае распределения/освобождения часто являются хитом производительности.
импульс.Регулярное выражение
это
boost::regex
сроки строка c аргументов пример:real 0m1.284s user 0m1.278s sys 0m0.005s
кодекса
boost::regex
иstd::regex
интерфейс в этом примере идентичны, просто необходимо изменить пространство имен и включить.наилучшие пожелания, чтобы со временем он стал лучше, реализации регулярных выражений c++ stdlib находятся в зачаточном состоянии.
EDIT
для завершения я попробовал это (вышеупомянутое предложение "окончательное улучшение"), и это не улучшило производительность эквивалент
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; }
это имеет отношение к array_ref и string_ref предложение. Вот пример кода, использующего его:
#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; }
также будет дешевле вернуть вектор
string_ref
, а неstring
копии для случаяsplit
С возвращением вектора.EDIT 2
это новое решение может получить выход путем возврата. Я использовал Маршалла Клоу
string_view
(string_ref
была переименована) реализация libc++ найдена по адресу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); } }
сроки:
real 0m0.385s user 0m0.385s sys 0m0.000s
обратите внимание, как быстро это по сравнению с предыдущими результатами. Конечно, это не заполнение
vector
внутри цикла (ни совпадения ничего заранее, вероятно, тоже), но вы все равно получаете диапазон, который вы можете варьировать с помощью диапазона на основеfor
, или даже использовать его для заполненияvector
.как ранжирование по
iterator_range
создаетstring_view
s над оригиналомstring
(или строка с нулевым завершением), это становится очень легким, никогда не генерируя ненужные выделения строк.просто для сравнения с помощью этой
split
реализация, но на самом деле заполнениеvector
мы могли бы сделать это: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(); } }
это использует алгоритм копирования диапазона повышения для заполнения вектора в каждой итерации, время:
real 0m1.002s user 0m0.997s sys 0m0.004s
как видно, особой разницы по сравнению с оптимизировано
string_view
выходная версия param.обратите внимание также есть предложение
std::split
это будет работать так.
для оптимизации, в общем, вы хотите избежать двух вещей:
- сжигание циклов процессора для ненужных вещей
- ожидание лениво что-то произойдет (чтение памяти, чтение диска, чтение сети, ...)
эти два могут быть противоположными, так как иногда это заканчивается более быстрым вычислением чего-то, чем кэширование всего этого в памяти... так что это игра на равновесие.
давайте проанализируем ваш код:
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; }
можем ли мы сделать лучше ? Ну, если бы мы могли повторно использовать существующее хранилище вместо того, чтобы выделять и освобождать память, мы бы увидели значительное улучшение [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
В тест, который вы выполняете, где число частичные совпадения в итерации, эта версия вряд ли будет побит: он будет только выделять память при первом запуске (как для
rsplit
иresult
), а затем продолжайте повторно использовать существующую память.[1]: Отказ от ответственности, я только доказал, что этот код правильно, я не проверял его (как сказал бы Дональд Кнут).
Как насчет этой версии? Это не регулярное выражение, но это решает довольно быстро ...
#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
реальные 0m0.132s пользователь 0m0. 000s представление sys 0m0.Облигации превышает 109
Я бы сказал, что регулярное выражение C++11 намного медленнее, чем perl и, возможно, чем python.
чтобы правильно измерить производительность, лучше сделать тесты используя какое-то нетривиальное выражение, иначе вы все измеряете но само регулярное выражение.
например, для сравнения C++11 и perl
в C++11 тестовый код
#include <iostream> #include <string> #include <regex> #include <chrono> int main () { int veces = 10000; int count = 0; std::regex expres ("([^-]*)-([^-]*)-(\d\d\d:\d\d)---(.*)"); std::string text ("some-random-text-453:10--- etc etc blah"); std::smatch macha; auto start = std::chrono::system_clock::now(); for (int ii = 0; ii < veces; ii ++) count += std::regex_search (text, macha, expres); auto milli = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start).count(); std::cout << count << "/" << veces << " matches " << milli << " ms --> " << (milli / (float) veces) << " ms per regex_search" << std::endl; return 0; }
в моем компьютере компиляции с gcc 4.9.3 я получаю выход
10000/10000 matches 1704 ms --> 0.1704 ms per regex_search
perl тест код
use Time::HiRes qw/ time sleep /; my $veces = 1000000; my $conta = 0; my $phrase = "some-random-text-453:10--- etc etc blah"; my $start = time; for (my $ii = 0; $ii < $veces; $ii++) { if ($phrase =~ m/([^-]*)-([^-]*)-(\d\d\d:\d\d)---(.*)/) { $conta = $conta + 1; } } my $elapsed = (time - $start) * 1000.0; print $conta . "/" . $veces . " matches " . $elapsed . " ms --> " . ($elapsed / $veces) . " ms per regex\n";
использование perl v5.8. 8 снова в моем компьютере
1000000/1000000 matches 2929.55303192139 ms --> 0.00292955303192139 ms per regex
Итак, в этом тесте соотношение C++11 / perl равно
0.1704 / 0.002929 = 58.17 times slower than perl
в реальных сценариях я получаю отношения примерно в 100-200 раз медленнее. Так, например разбор большого файла с миллионом строк занимает около секунды для perl, в то время как это может занять больше минут(!) для C++11 программа с использованием регулярных выражений.