Я пытаюсь преобразовать код с Python на C ++, чтобы немного ускориться и отточить мои ржавые навыки C ++. Вчера я был шокирован, когда наивная реализация чтения строк из stdin была намного быстрее в Python, чем в C ++ (см. Это ). Сегодня я наконец понял, как разбить строку в C ++ с помощью разделителей слияния (семантика аналогична функции split () в Python), и теперь я испытываю дежавю! Мой код на C ++ выполняет работу намного дольше (хотя и не на порядок больше, как было на вчерашнем уроке).
Код 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('')
Код 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
Обратите внимание, что я пробовал две разные реализации разделения. Один (split1) использует строковые методы для поиска токенов и может объединять несколько токенов, а также обрабатывать множество токенов ( отсюда ). Второй (split2) использует getline для чтения строки как потока, не объединяет разделители и поддерживает только один символ разделителя (который был отправлен несколькими пользователями StackOverflow в ответах на вопросы о разделении строк).
Я запускал это несколько раз в разном порядке. Моя тестовая машина - Macbook Pro (2011 г., 8 ГБ, четырехъядерный), но это не имеет большого значения. Я тестирую текстовый файл размером 20 мегабайт с тремя разделенными пробелами столбцами, каждый из которых выглядит примерно так: «foo.bar 127.0.0.1 home.foo.bar»
Полученные результаты:
$ /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
Что я делаю не так? Есть ли лучший способ сделать разделение строк в C ++, который не полагается на внешние библиотеки (т.е. без повышения), поддерживает слияние последовательностей разделителей (например, разбиение на Python), является потокобезопасным (поэтому без strtok) и производительность которого не ниже наравне с питоном?
Изменить 1 / частичное решение ?:
Я попытался сделать его более справедливым сравнением, заставив python сбрасывать фиктивный список и добавлять к нему каждый раз, как это делает C ++. Это все еще не совсем то, что делает код C ++, но немного ближе. По сути, цикл теперь такой:
for line in sys.stdin:
dummy = []
dummy += line.split()
count += 1
Производительность python теперь примерно такая же, как у реализации split1 C ++.
/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
Я все еще удивлен, что, даже если Python настолько оптимизирован для обработки строк (как предложил Мэтт Джойнер), эти реализации C ++ не будут быстрее. Если у кого-то есть идеи, как сделать это более оптимальным образом с помощью C ++, поделитесь своим кодом. (Я думаю, что моим следующим шагом будет попытка реализовать это на чистом C, хотя я не собираюсь жертвовать производительностью программистов, чтобы заново реализовать мой общий проект на C, так что это будет просто эксперимент по скорости разделения строк.)
Спасибо всем за помощь.
Окончательное редактирование / решение:
См. Принятый ответ Альфа. Поскольку python работает со строками строго по ссылке, а строки STL часто копируются, производительность лучше при использовании ванильных реализаций python. Для сравнения я скомпилировал и прогнал свои данные через код Альфа, и вот производительность на той же машине, что и все другие прогоны, по сути идентична наивной реализации python (хотя и быстрее, чем реализация python, которая сбрасывает / добавляет список, поскольку показано в приведенном выше редактировании):
$ /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
Моя единственная небольшая оставшаяся проблема касается количества кода, необходимого для того, чтобы C ++ работал в этом случае.
Один из уроков, которые можно извлечь из этой проблемы и вчерашней проблемы чтения строки stdin (ссылка выше), заключается в том, что всегда следует проводить тесты, а не делать наивные предположения об относительной производительности языков по умолчанию. Я ценю образование.
Еще раз спасибо всем за ваши предложения!
g++ -Wall -O3 -o split1 split_1.cpp
@JJC: Как обстоят дела с вашим тестом, когда вы на самом деле используетеdummy
и,spline
соответственно, Python удаляет вызов,line.split()
потому что у него нет побочных эффектов?Ответы:
Предположительно, строки Python являются неизменяемыми строками с подсчетом ссылок, поэтому строки не копируются в код Python, а C ++
std::string
- это изменяемый тип значения, который копируется при малейшей возможности.Если целью является быстрое разделение, то можно использовать операции с подстроками с постоянным временем, что означает обращение только к частям исходной строки, как в Python (и Java, и C #…).
Однако у
std::string
класса C ++ есть одна полезная особенность: он является стандартным , так что его можно использовать для безопасной и переносимой передачи строк там, где эффективность не является главным соображением. Но хватит болтовни. Код - и на моей машине это, конечно, быстрее, чем Python, поскольку обработка строк Python реализована на C, который является подмножеством C ++ (он, он):#include <iostream> #include <string> #include <sstream> #include <time.h> #include <vector> using namespace std; class StringRef { private: char const* begin_; int size_; public: int size() const { return size_; } char const* begin() const { return begin_; } char const* end() const { return begin_ + size_; } StringRef( char const* const begin, int const size ) : begin_( begin ) , size_( size ) {} }; vector<StringRef> split3( string const& str, char delimiter = ' ' ) { vector<StringRef> result; enum State { inSpace, inToken }; State state = inSpace; char const* pTokenBegin = 0; // Init to satisfy compiler. for( auto it = str.begin(); it != str.end(); ++it ) { State const newState = (*it == delimiter? inSpace : inToken); if( newState != state ) { switch( newState ) { case inSpace: result.push_back( StringRef( pTokenBegin, &*it - pTokenBegin ) ); break; case inToken: pTokenBegin = &*it; } } state = newState; } if( state == inToken ) { result.push_back( StringRef( pTokenBegin, &*str.end() - pTokenBegin ) ); } return result; } 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); vector<StringRef> const v = split3( 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 -std=c++0x
Отказ от ответственности: я надеюсь, что ошибок нет. Функциональность не тестировал, а только скорость проверил. Но я думаю, что даже если есть одна или две ошибки, их исправление не сильно повлияет на скорость.
источник
StringRef
, вы можетеstd::string
очень легко скопировать подстроку в файлstring( sr.begin(), sr.end() )
.PyString_FromStringAndSize()
эти вызовыPyObject_MALLOC()
. Таким образом, нет оптимизации с общим представлением, которое эксплуатирует неизменность строк в Python.Я не предлагаю лучших решений (по крайней мере, с точки зрения производительности), но некоторые дополнительные данные могут быть интересны.
Использование
strtok_r
(реентерабельный вариантstrtok
):void splitc1(vector<string> &tokens, const string &str, const string &delimiters = " ") { char *saveptr; char *cpy, *token; cpy = (char*)malloc(str.size() + 1); strcpy(cpy, str.c_str()); for(token = strtok_r(cpy, delimiters.c_str(), &saveptr); token != NULL; token = strtok_r(NULL, delimiters.c_str(), &saveptr)) { tokens.push_back(string(token)); } free(cpy); }
Дополнительно используются символьные строки для параметров и
fgets
для ввода:void splitc2(vector<string> &tokens, const char *str, const char *delimiters) { char *saveptr; char *cpy, *token; cpy = (char*)malloc(strlen(str) + 1); strcpy(cpy, str); for(token = strtok_r(cpy, delimiters, &saveptr); token != NULL; token = strtok_r(NULL, delimiters, &saveptr)) { tokens.push_back(string(token)); } free(cpy); }
И в некоторых случаях, когда допустимо уничтожение входной строки:
void splitc3(vector<string> &tokens, char *str, const char *delimiters) { char *saveptr; char *token; for(token = strtok_r(str, delimiters, &saveptr); token != NULL; token = strtok_r(NULL, delimiters, &saveptr)) { tokens.push_back(string(token)); } }
Сроки для них следующие (включая мои результаты для других вариантов из вопроса и принятый ответ):
split1.cpp: C++ : Saw 20000000 lines in 31 seconds. Crunch speed: 645161 split2.cpp: C++ : Saw 20000000 lines in 45 seconds. Crunch speed: 444444 split.py: Python: Saw 20000000 lines in 33 seconds. Crunch Speed: 606060 split5.py: Python: Saw 20000000 lines in 35 seconds. Crunch Speed: 571428 split6.cpp: C++ : Saw 20000000 lines in 18 seconds. Crunch speed: 1111111 splitc1.cpp: C++ : Saw 20000000 lines in 27 seconds. Crunch speed: 740740 splitc2.cpp: C++ : Saw 20000000 lines in 22 seconds. Crunch speed: 909090 splitc3.cpp: C++ : Saw 20000000 lines in 20 seconds. Crunch speed: 1000000
Как мы видим, решение из принятого ответа по-прежнему является самым быстрым.
Для тех, кто хотел бы провести дополнительные тесты, я также разместил репозиторий Github со всеми программами из вопроса, принятого ответа, этого ответа, а также Makefile и скрипт для генерации тестовых данных: https: // github. com / tobbez / разделение строк .
источник
memcpy
, а неstrcpy
, если компилятор не замечает эту оптимизацию.strcpy
обычно использует стратегию более медленного запуска, которая обеспечивает баланс между быстрым для коротких строк и нарастанием до полного SIMD для длинных строк.memcpy
знает размер сразу и не должен использовать какие-либо уловки SIMD для проверки конца строки неявной длины. (Ничего страшного для современных x86). Созданиеstd::string
объектов с помощью(char*, len)
конструктора тоже могло бы быть быстрее, если бы вы могли это сделатьsaveptr-token
. Очевидно, что быстрее всего было бы просто хранитьchar*
токены: PЯ подозреваю, что это из-за того, как
std::vector
изменяется размер в процессе вызова функции push_back (). Если вы попытаетесь использоватьstd::list
илиstd::vector::reserve()
зарезервировать достаточно места для предложений, вы получите гораздо лучшую производительность. Или вы можете использовать комбинацию того и другого, как показано ниже для split1 ():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); list<string> token_list; while (string::npos != pos || string::npos != lastPos) { // Found a token, add it to the list token_list.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); } tokens.assign(token_list.begin(), token_list.end()); }
EDIT : Другая очевидная вещь , которую я вижу в том , что переменная Python
dummy
получает назначается каждый раз , но не модифицируется. Так что это несправедливое сравнение с C ++. Вы должны попробовать изменить свой код Python, чтобы онdummy = []
инициализировал его, а затем сделайте этоdummy += line.split()
. Можете ли вы сообщить о времени выполнения после этого?EDIT2 : Чтобы сделать его еще более справедливым, вы можете изменить цикл while в коде C ++ следующим образом:
while(cin) { getline(cin, input_line); std::vector<string> spline; // create a new vector //I'm trying one of the two implementations, per compilation, obviously: // split1(spline, input_line); split2(spline, input_line); count++; };
источник
Я думаю, что следующий код лучше, используя некоторые функции C ++ 17 и C ++ 14:
// These codes are un-tested when I write this post, but I'll test it // When I'm free, and I sincerely welcome others to test and modify this // code. // C++17 #include <istream> // For std::istream. #include <string_view> // new feature in C++17, sizeof(std::string_view) == 16 in libc++ on my x86-64 debian 9.4 computer. #include <string> #include <utility> // C++14 feature std::move. template <template <class...> class Container, class Allocator> void split1(Container<std::string_view, Allocator> &tokens, std::string_view str, std::string_view delimiter = " ") { /* * The model of the input string: * * (optional) delimiter | content | delimiter | content | delimiter| * ... | delimiter | content * * Using std::string::find_first_not_of or * std::string_view::find_first_not_of is a bad idea, because it * actually does the following thing: * * Finds the first character not equal to any of the characters * in the given character sequence. * * Which means it does not treeat your delimiters as a whole, but as * a group of characters. * * This has 2 effects: * * 1. When your delimiters is not a single character, this function * won't behave as you predicted. * * 2. When your delimiters is just a single character, the function * may have an additional overhead due to the fact that it has to * check every character with a range of characters, although * there's only one, but in order to assure the correctness, it still * has an inner loop, which adds to the overhead. * * So, as a solution, I wrote the following code. * * The code below will skip the first delimiter prefix. * However, if there's nothing between 2 delimiter, this code'll * still treat as if there's sth. there. * * Note: * Here I use C++ std version of substring search algorithm, but u * can change it to Boyer-Moore, KMP(takes additional memory), * Rabin-Karp and other algorithm to speed your code. * */ // Establish the loop invariant 1. typename std::string_view::size_type next, delimiter_size = delimiter.size(), pos = str.find(delimiter) ? 0 : delimiter_size; // The loop invariant: // 1. At pos, it is the content that should be saved. // 2. The next pos of delimiter is stored in next, which could be 0 // or std::string_view::npos. do { // Find the next delimiter, maintain loop invariant 2. next = str.find(delimiter, pos); // Found a token, add it to the vector tokens.push_back(str.substr(pos, next)); // Skip delimiters, maintain the loop invariant 1. // // @ next is the size of the just pushed token. // Because when next == std::string_view::npos, the loop will // terminate, so it doesn't matter even if the following // expression have undefined behavior due to the overflow of // argument. pos = next + delimiter_size; } while(next != std::string_view::npos); } template <template <class...> class Container, class traits, class Allocator2, class Allocator> void split2(Container<std::basic_string<char, traits, Allocator2>, Allocator> &tokens, std::istream &stream, char delimiter = ' ') { std::string<char, traits, Allocator2> item; // Unfortunately, std::getline can only accept a single-character // delimiter. while(std::getline(stream, item, delimiter)) // Move item into token. I haven't checked whether item can be // reused after being moved. tokens.push_back(std::move(item)); }
Выбор тары:
std::vector
.Предполагая, что начальный размер выделенного внутреннего массива равен 1, а конечный размер равен N, вы будете выделять и освобождать для log2 (N) раз, и вы скопируете (2 ^ (log2 (N) + 1) - 1) = (2н - 1) раз. Как указано в разделе Является ли низкая производительность std :: vector из-за того, что realloc не вызывается логарифмическое количество раз? , это может иметь низкую производительность, когда размер вектора непредсказуем и может быть очень большим. Но если вы сможете оценить его размер, это не будет проблемой.
std::list
.Для каждого push_back время, которое он потребляет, является постоянным, но, вероятно, это займет больше времени, чем std :: vector для отдельного push_back. Использование пула памяти для каждого потока и специального распределителя может облегчить эту проблему.
std::forward_list
.То же, что и std :: list, но занимает меньше памяти на каждый элемент. Требуется класс-оболочка для работы из-за отсутствия API push_back.
std::array
.Если вы знаете предел роста, вы можете использовать std :: array. Конечно, вы не можете использовать его напрямую, так как у него нет API push_back. Но вы можете определить оболочку, и я думаю, что это самый быстрый способ здесь и может сэкономить немного памяти, если ваша оценка достаточно точна.
std::deque
.Эта опция позволяет обменять память на производительность. Не будет копии элемента (2 ^ (N + 1) - 1) раз, будет только выделение N раз и не будет освобождения. Кроме того, у вас будет постоянное время произвольного доступа и возможность добавлять новые элементы с обеих сторон.
Согласно std :: deque-cppreference
или вы можете использовать комбинацию из них:
std::vector< std::array<T, 2 ^ M> >
Это похоже на std :: deque, разница только в том, что этот контейнер не поддерживает добавление элемента впереди. Но он по-прежнему работает быстрее из-за того, что он не будет копировать базовый массив std :: array для (2 ^ (N + 1) - 1) раз, он просто скопирует массив указателей для (2 ^ (N - M + 1) - 1) раз и выделяет новый массив только тогда, когда текущий заполнен и не нужно ничего освобождать. Кстати, вы можете получить постоянное время произвольного доступа.
std::list< std::array<T, ...> >
Значительно облегчить давление фрагментации памяти. Он будет выделять новый массив только тогда, когда текущий будет заполнен, и не нужно ничего копировать. Вам все равно придется заплатить цену за дополнительный указатель по сравнению с комбо 1.
std::forward_list< std::array<T, ...> >
То же, что и 2, но стоит столько же памяти, что и комбо 1.
источник
N
корпуса. Жаль, что std :: vector нельзя использовать,realloc
чтобы потенциально разрешить отображение большего количества страниц в конце текущего выделения , поэтому он примерно в 2 раза медленнее.stringview::remove_prefix
же дешево, как просто отслеживать текущую позицию в обычной строке?std::basic_string::find
имеет необязательный второй аргумент,pos = 0
позволяющий начать поиск со смещения.log2(size - 1) + 2
с использованием целочисленного журнала). Первое распределение переместило 0 строк, второе переместило 1, затем 2, затем 4, затем 8, затем, наконец, 16, всего 31 ход (2^(log2(size - 1) + 1) - 1)
). Это O (n), а не O (2 ^ n). Это будет намного лучшеstd::list
.Вы делаете ошибочное предположение, что выбранная вами реализация C ++ обязательно быстрее, чем Python. Обработка строк в Python сильно оптимизирована. См. Этот вопрос для получения дополнительной информации: Почему операции std :: string работают плохо?
источник
Если вы возьмете реализацию split1 и измените подпись, чтобы она больше соответствовала сигнатуре split2, изменив это:
void split1(vector<string> &tokens, const string &str, const string &delimiters = " ")
к этому:
void split1(vector<string> &tokens, const string &str, const char delimiters = ' ')
Вы получаете более резкую разницу между split1 и split2 и более справедливое сравнение:
split1 C++ : Saw 10000000 lines in 41 seconds. Crunch speed: 243902 split2 C++ : Saw 10000000 lines in 144 seconds. Crunch speed: 69444 split1' C++ : Saw 10000000 lines in 33 seconds. Crunch speed: 303030
источник
void split5(vector<string> &tokens, const string &str, char delim=' ') { enum { do_token, do_delim } state = do_delim; int idx = 0, tok_start = 0; for (string::const_iterator it = str.begin() ; ; ++it, ++idx) { switch (state) { case do_token: if (it == str.end()) { tokens.push_back (str.substr(tok_start, idx-tok_start)); return; } else if (*it == delim) { state = do_delim; tokens.push_back (str.substr(tok_start, idx-tok_start)); } break; case do_delim: if (it == str.end()) { return; } if (*it != delim) { state = do_token; tok_start = idx; } break; } } }
источник
Я подозреваю, что это связано с буферизацией sys.stdin в Python, но не с буферизацией в реализации C ++.
См. Этот пост для получения подробной информации о том, как изменить размер буфера, а затем попробуйте сравнение еще раз: Установка меньшего размера буфера для sys.stdin?
источник