Почему расщепление строки медленнее на С++, чем на Python?

Я пытаюсь преобразовать некоторый код из Python в С++, чтобы получить немного скорости и обострить свои ржавые навыки С++. Вчера я был потрясен, когда наивная реализация строк чтения из stdin была намного быстрее в Python, чем С++ (см. this). Сегодня я, наконец, понял, как разбить строку на С++ с слиянием разделителей (аналогичная семантика на python split()), и теперь я испытываю дежавю! Мой код на С++ занимает гораздо больше времени, чтобы выполнить работу (хотя и не на порядок больше, как это было в случае вчерашнего урока).

Код 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('')

Код С++:

#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

Обратите внимание, что я пробовал две разные реализации разнесения. One (split1) использует строковые методы для поиска токенов и может объединять несколько токенов, а также обрабатывать многочисленные токены (это происходит от здесь), Второй (split2) использует getline для чтения строки в виде потока, не объединяет разделители и поддерживает только один символ деминера (тот был опубликован несколькими пользователями StackOverflow в ответах на вопросы о разделении строк).

Я запускал это несколько раз в разных порядках. Моя тестовая машина - Macbook Pro (2011, 8 ГБ, Quad Core), но это не так важно. Я тестирую текстовый файл с 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

Что я делаю неправильно? Есть ли лучший способ сделать разбиение строк на С++, который не полагается на внешние библиотеки (то есть без повышения), поддерживает слияние последовательностей разделителей (например, разделение на python), является потокобезопасным (так что ни один strtok) и производительность которого не меньше на уровне python?

Изменить 1/Частичное решение?

Я попытался сделать это более справедливым сравнением, имея python reset список фиктивных файлов и добавляя к нему каждый раз, как это делает С++. Это все еще не совсем то, что делает код С++, но он немного ближе. В принципе, цикл теперь:

for line in sys.stdin:
    dummy = []
    dummy += line.split()
    count += 1

Производительность python теперь примерно такая же, как реализация 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

Я все еще удивляюсь, что даже если Python настолько оптимизирован для обработки строк (как предположил Matt Joiner), что эти реализации С++ не будут быстрее. Если у кого-нибудь есть идеи о том, как это сделать более оптимальным образом с использованием С++, поделитесь своим кодом. (Я думаю, что мой следующий шаг будет пытаться реализовать это в чистом C, хотя я не собираюсь компрометировать производительность программиста, чтобы повторно реализовать мой общий проект на C, так что это будет просто эксперимент для скорости разделения строк.)

Спасибо всем за вашу помощь.

Окончательное редактирование/решение:

Пожалуйста, см. одобренный Alf ответ. Поскольку python имеет дело со строками строго по ссылке, а строки STL часто копируются, производительность лучше с реализацией на основе python. Для сравнения, я скомпилировал и выполнил мои данные с помощью кода Alf, и вот производительность на том же компьютере, что и все остальные, по сути, идентичны наивной реализации 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

Моя единственная небольшая оставшаяся проблема касается количества кода, необходимого для выполнения С++ в этом случае.

Один из уроков здесь из этой проблемы и вчера проблема чтения строк stdin (связанная выше) заключается в том, что всегда нужно ориентироваться вместо того, чтобы делать наивные предположения относительно относительной "дефолтной" работы языков. Я ценю образование.

Еще раз спасибо за ваши предложения!

Ответы

Ответ 1

Как предполагают, строки Python являются ссылочными считанными неизменяемыми строками, поэтому в коде Python не копируются строки, а С++ std::string - изменяемый тип значения и копируется с наименьшей возможностью.

Если цель заключается в быстром расщеплении, то можно использовать операции подстроки постоянного времени, что означает только ссылку на части исходной строки, как в Python (и Java, и С# и hellip;).

В классе С++ std::string есть одна функция исправления: хотя она стандартная, поэтому ее можно использовать для безопасного и портативного переноса строк, где эффективность не является основным соображением. Но достаточно чата. Code - и на моей машине это, конечно, быстрее, чем Python, так как обработка строк Python реализована в 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

Отказ от ответственности: я надеюсь, что ошибок нет. Я не тестировал функциональность, но только проверял скорость. Но я думаю, даже если есть ошибка или два, исправление, которое не будет существенно влиять на скорость.

Ответ 2

Я не предлагаю никаких лучших решений (по крайней мере, по производительности), но некоторые дополнительные данные, которые могут быть интересными.

Использование 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 и script для генерации тестовых данных: https://github.com/tobbez/string-splitting.

Ответ 3

Я подозреваю, что это связано с тем, как 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 получает каждый раз, но не изменяется. Так что это не справедливое сравнение с С++. Вы должны попробовать изменить код Python на dummy = [], чтобы инициализировать его, а затем сделать dummy += line.split(). Можете ли вы сообщить об этом во время выполнения?

EDIT2. Чтобы сделать его еще более справедливым, вы можете изменить цикл while в коде на С++:

    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++;
    };

Ответ 5

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;
        }
    }
}

Ответ 6

Если вы примете реализацию 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

Ответ 7

Я подозреваю, что это связано с буферизацией на sys.stdin в Python, но без буферизации в реализации С++.

Подробнее об изменении размера буфера см. в этом сообщении, затем повторите сравнение: Установка меньшего размера буфера для sys.stdin?