Чому розбиття рядка повільніше на C ++, ніж Python?


93

Я намагаюсь перетворити якийсь код з Python на C ++, намагаючись набрати трохи швидкості і відточити свої іржаві навички C ++. Вчора я був шокований, коли наївна реалізація рядків читання з stdin була набагато швидшою в Python, ніж C ++ (див. Це ). Сьогодні я нарешті з'ясував, як розділити рядок на C ++ зі злиттям роздільників (схожа семантика на розкол пітона ()), і зараз я відчуваю дежавю! Мій код 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 Гб, 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

Що я роблю неправильно? Чи є кращий спосіб зробити розбиття рядків на 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, тому це буде просто експериментом для швидкості розбиття рядків.)

Дякуємо всім за вашу допомогу.

Остаточне редагування / рішення:

Будь ласка, дивіться прийняту відповідь Альфа. Оскільки python обробляє рядки строго за посиланням, а рядки STL часто копіюються, продуктивність кращої для реалізації ванільного 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 ++ для виконання у цьому випадку.

Одним із уроків цього випуску та вчорашнього питання читання строгого рядка (зв'язане вище) є те, що завжди слід орієнтуватися, а не робити наївні припущення щодо відносної «мови за умовчанням» у мовах. Я ціную освіту.

Ще раз дякую всім за ваші пропозиції!


2
Як ви склали програму C ++? Чи увімкнено оптимізацію?
interjay

2
@interjay: Це в останньому коментарі в його джерелі: g++ -Wall -O3 -o split1 split_1.cpp@JJC: Як ваш базовий тариф, коли ви фактично використовуєте, dummyі splineвідповідно, можливо, Python видаляє виклик, line.split()оскільки він не має побічних ефектів?
Ерік

2
Які результати ви отримуєте, якщо видалити розбиття та залишити лише stding рядки зі stdin?
interjay

2
Python написаний на C. Це означає, що існує ефективний спосіб зробити це, в C. Можливо, є кращий спосіб розділити рядок, ніж використання STL?
ixe013

Відповіді:


57

Як припущення, рядки Python - це посилання, що перераховуються незмінними рядками, так що жоден рядок не копіюється навколо в коді Python, тоді як C ++ std::string є типом значення, що змінюється, і копіюється при найменшій можливості.

Якщо мета швидко розщеплюється, тоді можна використовувати постійні операції з підрядкою за часом, що означає лише посилання на частини вихідної рядки, як у Python (і Java, і C #…).

Клас C ++ std::string, однак, має одну функцію викупу: він є стандартним , щоб його можна було безпечно та переносити струнами навколо, де ефективність не є головною увагою. Але чату вистачає. Код - і на моїй машині це, звичайно, швидше, ніж 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

Відмова: Я сподіваюся, що помилок немає. Я не перевіряв функціональність, а лише перевіряв швидкість. Але я думаю, навіть якщо є помилка або два, виправлення це не вплине суттєво на швидкість.


2
Так, рядки Python - це об'єкти, що рахуються з посиланням, тому Python виконує набагато менше копіювання. Вони все ще містять нульові закінчені рядки C під кришкою, однак, не (вказівник, розмір) пар, як ваш код.
Фред Фоо

13
Іншими словами - для роботи вищого рівня, як маніпуляції текстом, дотримуйтесь мови вищого рівня, де зусилля, щоб зробити це ефективно, були накопичені десятками розробників протягом десятків років - або просто готові працювати так само, як і всі ці розробники за те, щоб мати щось порівнянне на нижчому рівні.
jsbueno

2
@JJC: StringRefви можете скопіювати підрядку в std::stringдуже просто, просто string( sr.begin(), sr.end() ).
ура та хт. - Альф

3
Я б хотів, щоб рядки CPython були скопійовані менше. Так, вони посилаються на облік і незмінні, але str.split () виділяє нові рядки для кожного елемента, використовуючи PyString_FromStringAndSize()цей виклик PyObject_MALLOC(). Таким чином, немає оптимізації з спільним представленням, яке використовує те, що рядки незмінні в Python.
jfs

3
Обслуговувачі: не вводьте помилок, намагаючись виправити сприймані помилки (особливо не з посиланням на cplusplus.com ). ТІА.
ура та хт. - Альф

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. кому / 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++;
    };

Дякую за ідею. Я реалізував його, і ця реалізація насправді повільніше, ніж початковий спліт1, на жаль. Я також спробував spline.reserve (16) перед циклом, але це не вплинуло на швидкість мого split1. На рядок є лише три лексеми, і вектор очищається після кожного рядка, тому я не очікував, що це дуже допоможе.
JJC

Я також спробував твою редагування. Перегляньте оновлене запитання. Продуктивність зараз нарівні з split1.
JJC

Я спробував ваш EDIT2. Продуктивність була трохи гіршою: $ / usr / bin / time cat test_lines_double | ./split7 33.39 реальний 0.01 користувач 0.49 sys C ++: за 33 секунди побачив 20000000 рядків. Швидкість хрускіту: 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) = (2N - 1) разів. Як вказувалося в Чи є низька продуктивність std :: vector через те, що не називає realloc логарифмічною кількістю разів? , це може мати низьку продуктивність, коли розмір вектора непередбачуваний і може бути дуже великим. Але якщо ви зможете оцінити його розмір, це буде меншою проблемою.

  2. std::list.

    Для кожного push_back час, який він витрачає, є постійним, але це, ймовірно, займе більше часу, ніж std :: vector на окремих push_back. Використання пулу пам'яті на один потік та спеціального розподільника може полегшити цю проблему.

  3. std::forward_list.

    Те саме, що std :: list, але займає менше пам’яті на один елемент. Потрібен клас обгортки для роботи через відсутність push_back API.

  4. std::array.

    Якщо ви можете знати межу зростання, то ви можете використовувати std :: array. Зважаючи на це, ви не можете використовувати його безпосередньо, оскільки він не має API push_back. Але ви можете визначити обгортку, і я думаю, що це найшвидший спосіб, і ви можете зберегти пам'ять, якщо ваша оцінка досить точна.

  5. std::deque.

    Цей параметр дозволяє торгувати пам’яттю для продуктивності. Копія елемента не буде (2 ^ (N + 1) - 1) разів, лише N розподілу разів, і не буде розміщення. Також у вас буде постійний час випадкового доступу та можливість додавати нові елементи з обох кінців.

Відповідно до std :: deque-cppreference

З іншого боку, деки зазвичай мають великі мінімальні витрати на пам'ять; deque, що містить лише один елемент, повинен виділити свій повний внутрішній масив (наприклад, у 8 разів більший розмір об'єкта на 64-бітному libstdc ++; в 16 разів більший розмір об'єкта або 4096 байт, залежно від того, що більше, на 64-розрядний libc ++)

або ви можете використовувати комбо з цих:

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

    Це схоже на std :: deque, різниця полягає лише в тому, що цей контейнер не підтримує додавання елемента спереду. Але вона все ще швидша в продуктивності, через те, що вона не буде копіювати базовий std :: масив протягом (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 , що те саме.
Цзяхао Сю

Ваш аналіз продуктивності вектора вимкнено. Розглянемо вектор, який має початкову ємність 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 ++: за 22 секунди побачив 20000000 рядків. Швидкість хрускіту: 909090
JJC

На моїй машині: split1 - 54s, split.py - 35s, split5 - 16s. Я поняття не маю.
п. 'займенники' m.

Хм, чи відповідають ваші дані формату, який я зазначив вище? Я припускаю, що ви бігали кожні кілька разів, щоб усунути минущі ефекти, такі як початкова популяція кеш-пам'яті?
JJC

0

Я підозрюю, що це пов'язано з буферизацією на sys.stdin в Python, але не буферизацією в реалізації C ++.

Дивіться цю публікацію, щоб дізнатися, як змінити розмір буфера, а потім спробуйте порівняння ще раз: Встановити менший розмір буфера для sys.stdin?


1
Гммм ... я не дотримуюся. Просто читання рядків (без розбиття) швидше в C ++, ніж Python (після включення рядка cin.sync_with_stdio (помилкове); Це питання, про яке я мав вчора, на яке згадувалося вище.
JJC
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.