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

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

using namespace std;

void split1(vector &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 &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 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), и чья производительность, по крайней мере, на уровне python?

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

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

91
задан Community 23 May 2017 в 12:02
поделиться