Почему разделение строки в C ++ выполняется медленнее, чем в Python?

94

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

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

JJC
источник
2
Как вы компилировали программу на C ++? У вас включена оптимизация?
Interjay
2
@interjay: Это в последнем комментарии в его источнике: g++ -Wall -O3 -o split1 split_1.cpp@JJC: Как обстоят дела с вашим тестом, когда вы на самом деле используете dummyи, splineсоответственно, Python удаляет вызов, line.split()потому что у него нет побочных эффектов?
Эрик
2
Какие результаты вы получите, если уберете разделение и оставите только строки для чтения из стандартного ввода?
Interjay
2
Python написан на C. Это означает, что есть эффективный способ сделать это на C. Может быть, есть лучший способ разбить строку, чем использование STL?
ixe013
3
возможный дубликат Почему операции std :: string работают плохо?
Мэтт Джойнер

Ответы:

58

Предположительно, строки 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

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

Приветствия и hth. - Альф
источник
2
Да, строки Python являются объектами с подсчетом ссылок, поэтому Python копирует гораздо меньше. Они по-прежнему содержат под капотом строки C с завершающим нулем, но не пары (указатель, размер), как ваш код.
Фред Фу
13
Другими словами - для работы более высокого уровня, такой как манипуляции с текстом, придерживайтесь языка более высокого уровня, где усилия, чтобы сделать это эффективно, прилагались кумулятивно десятками разработчиков за десятки лет - или просто подготовьтесь к работе столько же, сколько и все эти разработчики за наличие чего-то сопоставимого на более низком уровне.
jsbueno
2
@JJC: для StringRef, вы можете std::stringочень легко скопировать подстроку в файл string( sr.begin(), sr.end() ).
Приветствия и hth. - Alf
3
Хотелось бы, чтобы строки CPython копировались меньше. Да, они являются неизменяемыми и подсчитываются по ссылкам, но str.split () выделяет новые строки для каждого элемента, используя PyString_FromStringAndSize()эти вызовы PyObject_MALLOC(). Таким образом, нет оптимизации с общим представлением, которое эксплуатирует неизменность строк в Python.
jfs
3
Сопровождающие: пожалуйста, не вводите ошибки, пытаясь исправить предполагаемые ошибки (особенно не в отношении cplusplus.com ). TIA.
Приветствия и hth. - Альф,
9

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

Использование 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 / разделение строк .

тоббез
источник
2
Я сделал пул-реквест ( github.com/tobbez/string-splitting/pull/2 ), который сделал тест немного более реалистичным за счет «использования» данных (подсчета количества слов и символов). С этим изменением все версии C / C ++ превзошли версии Python (за исключением версии, основанной на добавленном мною токенизаторе Boost), и реальная ценность методов, основанных на «строковом представлении» (таких как split6), сияет.
Дэйв Йохансен
Вы должны использовать memcpy, а не strcpy, если компилятор не замечает эту оптимизацию. strcpyобычно использует стратегию более медленного запуска, которая обеспечивает баланс между быстрым для коротких строк и нарастанием до полного SIMD для длинных строк. memcpyзнает размер сразу и не должен использовать какие-либо уловки SIMD для проверки конца строки неявной длины. (Ничего страшного для современных x86). Создание std::stringобъектов с помощью (char*, len)конструктора тоже могло бы быть быстрее, если бы вы могли это сделать saveptr-token. Очевидно, что быстрее всего было бы просто хранить char*токены: P
Питер Кордес
4

Я подозреваю, что это из-за того, как 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++;
    };
Vite Falcon
источник
Спасибо за идею. Я реализовал это, и эта реализация, к сожалению, на самом деле медленнее, чем исходный split1. Я также пробовал spline.reserve (16) перед циклом, но это не повлияло на скорость моего split1. В каждой строке всего три токена, и вектор очищается после каждой строки, поэтому я не ожидал, что это сильно поможет.
JJC
Я тоже пробовал твое редактирование. См. Обновленный вопрос. Производительность теперь на уровне split1.
JJC
Я пробовал ваш EDIT2. Производительность была немного хуже: $ / usr / bin / time cat test_lines_double | ./split7 33,39 real 0,01 user 0,49 sys C ++: просмотрел 20000000 строк за 33 секунды. Crunch speed: 606060
JJC
3

Я думаю, что следующий код лучше, используя некоторые функции 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));
}

Выбор тары:

  1. std::vector.

    Предполагая, что начальный размер выделенного внутреннего массива равен 1, а конечный размер равен N, вы будете выделять и освобождать для log2 (N) раз, и вы скопируете (2 ^ (log2 (N) + 1) - 1) = (2н - 1) раз. Как указано в разделе Является ли низкая производительность std :: vector из-за того, что realloc не вызывается логарифмическое количество раз? , это может иметь низкую производительность, когда размер вектора непредсказуем и может быть очень большим. Но если вы сможете оценить его размер, это не будет проблемой.

  2. std::list.

    Для каждого push_back время, которое он потребляет, является постоянным, но, вероятно, это займет больше времени, чем std :: vector для отдельного push_back. Использование пула памяти для каждого потока и специального распределителя может облегчить эту проблему.

  3. std::forward_list.

    То же, что и std :: list, но занимает меньше памяти на каждый элемент. Требуется класс-оболочка для работы из-за отсутствия API push_back.

  4. std::array.

    Если вы знаете предел роста, вы можете использовать std :: array. Конечно, вы не можете использовать его напрямую, так как у него нет API push_back. Но вы можете определить оболочку, и я думаю, что это самый быстрый способ здесь и может сэкономить немного памяти, если ваша оценка достаточно точна.

  5. std::deque.

    Эта опция позволяет обменять память на производительность. Не будет копии элемента (2 ^ (N + 1) - 1) раз, будет только выделение N раз и не будет освобождения. Кроме того, у вас будет постоянное время произвольного доступа и возможность добавлять новые элементы с обеих сторон.

Согласно std :: deque-cppreference

С другой стороны, двухсторонние очереди обычно имеют большую минимальную стоимость памяти; двухсторонняя очередь, содержащая только один элемент, должна выделить свой полный внутренний массив (например, в 8 раз больше размера объекта в 64-битной libstdc ++; в 16 раз больше размера объекта или 4096 байт, в зависимости от того, что больше, в 64-битной libc ++)

или вы можете использовать комбинацию из них:

  1. std::vector< std::array<T, 2 ^ M> >

    Это похоже на std :: deque, разница только в том, что этот контейнер не поддерживает добавление элемента впереди. Но он по-прежнему работает быстрее из-за того, что он не будет копировать базовый массив std :: array для (2 ^ (N + 1) - 1) раз, он просто скопирует массив указателей для (2 ^ (N - M + 1) - 1) раз и выделяет новый массив только тогда, когда текущий заполнен и не нужно ничего освобождать. Кстати, вы можете получить постоянное время произвольного доступа.

  2. std::list< std::array<T, ...> >

    Значительно облегчить давление фрагментации памяти. Он будет выделять новый массив только тогда, когда текущий будет заполнен, и не нужно ничего копировать. Вам все равно придется заплатить цену за дополнительный указатель по сравнению с комбо 1.

  3. std::forward_list< std::array<T, ...> >

    То же, что и 2, но стоит столько же памяти, что и комбо 1.

ЦзяХао Сюй
источник
Если вы используете std :: vector с некоторым разумным начальным размером, например 128 или 256, общим количеством копий (при условии, что коэффициент роста равен 2), вы вообще избегаете любого копирования для размеров до этого предела. Затем вы можете уменьшить выделение, чтобы оно соответствовало количеству элементов, которые вы фактически использовали, так что это не страшно для небольших входов. Однако это не очень помогает с общим количеством копий для очень большого Nкорпуса. Жаль, что std :: vector нельзя использовать, reallocчтобы потенциально разрешить отображение большего количества страниц в конце текущего выделения , поэтому он примерно в 2 раза медленнее.
Питер Кордес
Так stringview::remove_prefixже дешево, как просто отслеживать текущую позицию в обычной строке? std::basic_string::findимеет необязательный второй аргумент, pos = 0позволяющий начать поиск со смещения.
Питер Кордес
@ Питер Кордес: Верно. Я проверил libcxx impl
JiaHao Xu
Я также проверил libstdc ++ impl , это то же самое.
JiaHao Xu
Ваш анализ производительности вектора отключен. Рассмотрим вектор, который имеет начальную емкость 1 при первой вставке и удваивается каждый раз, когда ему требуется новая емкость. Если вам нужно разместить 17 элементов, первое выделение дает место для 1, затем 2, затем 4, затем 8, затем 16, затем, наконец, 32. Это означает, что всего было 6 выделений ( log2(size - 1) + 2с использованием целочисленного журнала). Первое распределение переместило 0 строк, второе переместило 1, затем 2, затем 4, затем 8, затем, наконец, 16, всего 31 ход ( 2^(log2(size - 1) + 1) - 1)). Это O (n), а не O (2 ^ n). Это будет намного лучше std::list.
Дэвид Стоун
2

Вы делаете ошибочное предположение, что выбранная вами реализация C ++ обязательно быстрее, чем Python. Обработка строк в Python сильно оптимизирована. См. Этот вопрос для получения дополнительной информации: Почему операции std :: string работают плохо?

Мэтт Джойнер
источник
4
Я не делаю никаких заявлений об общей производительности языка, только о моем конкретном коде. Итак, здесь никаких предположений. Спасибо за хороший указатель на другой вопрос. Я не уверен, что вы говорите, что эта конкретная реализация на C ++ неоптимальна (ваше первое предложение) или что C ++ просто медленнее, чем Python в обработке строк (ваше второе предложение). Кроме того, если вы знаете, как быстро сделать то, что я пытаюсь сделать на C ++, поделитесь им для всеобщего блага. Спасибо. Чтобы прояснить, я люблю питон, но я не слепой фанат, поэтому я пытаюсь найти самый быстрый способ сделать это.
JJC
1
@JJC: Учитывая, что реализация Python быстрее, я бы сказал, что ваша неоптимальная. Имейте в виду, что языковые реализации могут срезать вам углы, но в конечном итоге выигрывает сложность алгоритмов и ручная оптимизация. В этом случае Python по умолчанию имеет преимущество.
Мэтт Джойнер
2

Если вы возьмете реализацию 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
Пол Бекингем
источник
1
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;
        }
    }
}
п. 'местоимения' м.
источник
Спасибо, нм! К сожалению, похоже, что это работает примерно с той же скоростью, что и исходная реализация (разделение 1) на моем наборе данных и машине: $ / usr / bin / time cat test_lines_double | ./split8 21,89 реальный 0,01 пользовательский 0,47 sys C ++: просмотрел 20000000 строк за 22 секунды. Crunch speed: 909090
JJC
На моей машине: split1 - 54s, split.py - 35s, split5 - 16s. Я понятия не имею.
п. 'местоимения' м.
Хм, ваши данные соответствуют указанному выше формату? Я полагаю, вы запускали каждый из них несколько раз, чтобы устранить временные эффекты, такие как начальное заполнение дискового кеша?
JJC
0

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

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

Алекс Коллинз
источник
1
Хммм ... Я не понимаю. Просто чтение строк (без разделения) выполняется быстрее в C ++, чем в Python (после включения строки cin.sync_with_stdio (false);). Это была проблема, о которой я говорил вчера.
JJC