C ++では文字列の分割がPythonよりも遅いのはなぜですか?


93

少し速度を上げて、さびたC ++のスキルを磨くために、一部のコードをPythonからC ++に変換しようとしています。昨日、標準入力から行を読み取る単純な実装がPythonではC ++よりもはるかに高速だったときにショックを受けました(これを参照)。今日、ようやくC ++で文字列をマージ区切り文字(pythonのsplit()と同様のセマンティクス)で分割する方法を見つけ、deja vuを体験しました!私のC ++コードは、作業を実行するのにはるかに長い時間がかかります(昨日のレッスンの場合のように、1桁以上ではありません)。

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

2つの異なる分割実装を試したことに注意してください。一つ(split1)は、トークンを検索するための文字列メソッドを使用し、複数のトークンだけでなく、ハンドルの多数のトークンを(それはから来てマージすることができ、ここで)。2番目(split2)は、getlineを使用して文字列をストリームとして読み取り、区切り文字をマージせず、1つの区切り文字(文字列分割の質問に対する回答で複数のStackOverflowユーザーによって投稿されたもの)のみをサポートします。

これをさまざまな順序で複数回実行しました。私のテストマシンはMacbook Pro(2011、8GB、クアッドコア)ですが、それほど重要ではありません。スペースで区切られた3つの列があり、それぞれが次のように見える20Mの行テキストファイルでテストしています: "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なし)で、少なくともパフォーマンスがC ++で文字列分割を行うより良い方法はありますかPythonと同等ですか?

Edit 1 / Partial Solution ?:

C ++のように、Pythonにダミーリストをリセットして毎回追加させることで、より公平な比較を試みました。これはまだ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が文字列処理用に非常に最適化されていても(Matt Joinerが示唆したように)、これらのC ++実装が高速にならないことにはまだ驚きます。C ++を使用してより最適な方法でこれを行う方法について誰かがアイデアを持っている場合は、コードを共有してください。(私の次のステップはこれを純粋な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

私の唯一残っている不満は、この場合にC ++を実行するために必要なコードの量に関するものです。

この問題と昨日の標準入力の読み取りの問題(上記にリンク)からの教訓の1つは、言語の相対的な「デフォルト」のパフォーマンスについて単純な仮定をするのではなく、常にベンチマークを行う必要があるということです。教育に感謝します。

あなたの提案をありがとうございました!


2
C ++プログラムをどのようにコンパイルしましたか?最適化をオンにしていますか?
interjay

2
@interjay::それは彼の元での最後のコメントにありますg++ -Wall -O3 -o split1 split_1.cpp@JJC:あなたが実際に使用するときにどのようにあなたのベンチマークの運賃が行うdummyspline、それぞれ、多分Pythonはへの呼び出しを削除しline.split()、それが何の副作用を持っていないので?
エリック

2
分割を削除し、標準入力からの読み取り行のみを残すと、どのような結果が得られますか?
interjay

2
PythonはCで書かれています。これは、Cでそれを行うための効率的な方法があることを意味します。おそらく、STLを使用するよりも文字列を分割するより良い方法がありますか?
ixe013

回答:


57

推測として、Python文字列は参照カウントされた不変文字列であるため、Pythonコードでは文字列はコピーされませんstd::stringが、C ++ は可変値型であり、最小の機会でコピーされます。

目標が高速分割の場合、定数時間の部分文字列演算を使用します。これは、Python(およびJava、C#…)のように、元の文字列の一部のみを参照することを意味します。

std::stringただし、C ++ クラスには1つの償還機能があります。これは標準であるため、効率が主な考慮事項ではない場所で文字列を安全かつ移植性のある方法で渡すために使用できます。しかし、十分なチャット。コード-そして私のマシンでは、これはもちろん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

免責事項:バグがないことを願っています。機能はテストしていませんが、速度のみを確認しています。でも、バグが1つか2つあったとしても、それを修正しても速度に大きな影響はないと思います。


2
はい、Python文字列は参照カウントオブジェクトなので、Pythonはコピーをはるかに少なくします。ただし、コードのように(ポインター、サイズ)のペアではなく、内部にはnullで終了するC文字列が含まれています。
フレッドフー

13
言い換えると、テキスト操作などの高レベルの作業については、高レベルの言語にこだわり、それを効率的に実行するための努力は、何十年にもわたって何十人もの開発者によって累積されてきました。下位レベルで同等のものがあるため。
jsbueno

2
@JJC:のStringRef場合、部分文字列をstd::string非常に簡単にコピーできますstring( sr.begin(), sr.end() )
乾杯とhth。-アルフ

3
CPythonの文字列のコピーを減らしたいです。はい、それらは参照カウントされ不変ですが、str.split()はPyString_FromStringAndSize()その呼び出しを使用して各アイテムに新しい文字列を割り当てますPyObject_MALLOC()。したがって、Pythonでは文字列が不変であることを利用する共有表現による最適化はありません。
jfs

3
メンテナ:(特にcplusplus.comを参照せずに認識されたバグを修正しようとしてバグを導入しないでください。TIA。
乾杯とhth。-2015

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

ご覧のように、受け入れられた回答からの解決策は、依然として最速です。

さらにテストを実行したい人のために、質問からのすべてのプログラム、承認された回答、この回答、さらにテストファイルを生成するためのMakefileとスクリプトを含むGithubリポジトリを作成しました:https:// github。 com / tobbez / string-splitting


2
私はプルリクエスト(github.com/tobbez/string-splitting/pull/2)を実行しました。これは、データ(単語と文字の数を数える)を "使用"することで、テストをもう少し現実的にします。この変更により、すべてのC / C ++バージョンがPythonバージョン(私が追加したBoostのトークナイザーに基づくものを期待)に勝り、「string view」ベースのメソッド(split6のような)の真の価値が輝きました。
Dave Johansen、2015

コンパイラが最適化に気付かない場合はmemcpy、ではなくを使用してくださいstrcpystrcpy通常、より遅い起動方法を使用して、短い文字列の高速と長い文字列の完全なSIMDまでのバランスをとります。 memcpyはサイズをすぐに認識し、暗黙の長さの文字列の終わりをチェックするためにSIMDトリックを使用する必要はありません。(最近のx86では大した問題ではありません)。コンストラクターを使用してstd::stringオブジェクトを作成すると、(char*, len)それをから取得できる場合も、より高速になる可能性がありsaveptr-tokenます。明らかにchar*トークンを保存するだけが最速です:P
Peter Cordes

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

編集:私が目にするもう1つの明白なことは、Python変数dummyが毎回割り当てられますが、変更されないことです。したがって、C ++に対する公平な比較ではありません。あなたはdummy = []それを初期化するためにあなたのPythonコードを修正してみて、それからそうすべきですdummy += line.split()。この後のランタイムを報告できますか?

EDIT2:さらに公平にするために、C ++コードの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++;
    };

アイデアをありがとう。私はそれを実装しましたが、この実装は実際には元のsplit1よりも低速です。ループの前にspline.reserve(16)も試しましたが、split1の速度に影響はありませんでした。1行にトークンは3つしかなく、ベクトルは各行の後にクリアされるため、それがあまり役立つとは思っていませんでした。
JJC 2012

私もあなたの編集を試みました。更新された質問をご覧ください。パフォーマンスは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のパフォーマンスが低下しますか?、これは、ベクトルのサイズが予測不能で非常に大きくなる可能性がある場合、パフォーマンスが低下する可能性があります。しかし、そのサイズを見積もることができれば、これはそれほど問題にはなりません。

  2. std::list

    すべてのpush_backで、消費される時間は一定ですが、おそらく、個々のpush_backでのstd :: vectorよりも時間がかかります。スレッドごとのメモリプールとカスタムアロケータを使用すると、この問題を緩和できます。

  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によると

一方、dequeは通常、最小のメモリコストが大きくなります。要素を1つだけ保持する両端キューは、その完全な内部配列を割り当てる必要があります(64ビットlibstdc ++ではオブジェクトサイズの8倍、64ビットlibc ++ではオブジェクトサイズの16倍または4096バイトのいずれか大きい方)

または、これらの組み合わせを使用できます。

  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と同じメモリを使用します。


128や256のような妥当な初期サイズ、合計コピー数(成長係数を2と想定)でstd :: vectorを使用する場合、その制限までのサイズのコピーはまったく回避できます。次に、実際に使用した要素の数に合わせて割り当てを縮小することができるため、小さな入力ではひどくなりません。Nただし、これは非常に大きなケースのコピーの総数ではあまり役に立ちません。それは残念だのstd ::ベクトルを使用することはできませんrealloc潜在的に現在の割り当ての終わりに多くのページをマッピングできるように、それが遅く2倍程度ですので、。
Peter Cordes

あるstringview::remove_prefixだけで、通常の文字列にあなたの現在の位置を追跡するように安いと? std::basic_string::findオプションの2番目の引数pos = 0があり、オフセットから検索を開始できます。
Peter Cordes

@ピーターコルドそれは正しいです。libcxx
実装

libstdc ++ implもチェックしました。これは同じです。
JiaHao Xu

ベクトルのパフォーマンスの分析はオフです。最初に挿入したときに初期容量が1で、新しい容量が必要になるたびに2倍になるベクトルを考えます。17アイテムを配置する必要がある場合、最初の割り当てで1、2、4、8、16、最後に32のスペースが確保されます。つまりlog2(size - 1) + 2、合計6つの割り当て(整数ログを使用)があったことになります。最初の割り当てで0個の文字列が移動し、2番目の割り当てで1個、2個、4個、8個、最後に16個と移動し、合計31個の移動(2^(log2(size - 1) + 1) - 1))が行われました。これはO(n)であり、O(2 ^ n)ではありません。これは非常に優れていstd::listます。
David Stone、

2

選択したC ++実装がPythonの実装よりも必然的に高速であるという誤った仮定をしていることになります。Pythonの文字列処理は高度に最適化されています。詳細については、この質問を参照してください。なぜstd :: string操作のパフォーマンスが悪いのですか?


4
言語の全体的なパフォーマンスについては主張せず、私の特定のコードについてのみ主張します。したがって、ここでは仮定はありません。他の質問への良いポインタをありがとう。C ++でのこの特定の実装が最適ではない(最初の文)と言っているのか、C ++が文字列処理でPythonよりも遅い(2番目の文)と言っているのかはわかりません。また、私がC ++でやろうとしていることをすばやく実行する方法を知っている場合は、皆のためにそれを共有してください。ありがとう。明確にするために、私はpythonが大好きですが、私は盲目的なファンではありません。そのため、これを行うための最速の方法を学ぼうとしています。
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;
        }
    }
}

nmありがとう!残念ながら、これは私のデータセットとマシンの元の(スプリット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。何も思いつきません。
n。「代名詞」m。

うーん、あなたのデータは上記のフォーマットと一致していますか?最初のディスクキャッシュの作成などの一時的な影響を排除するために、それぞれを数回実行したと思いますか?
JJC

0

これはPythonのsys.stdinでのバッファリングに関連していると思われますが、C ++実装ではバッファリングがありません。

バッファサイズを変更する方法の詳細については、この投稿を参照してから、比較を再試行してください 。sys.stdinのバッファサイズを小さく設定しますか?


1
うーん...私はついていません。(分割なしで)行を読み取るだけの方が、C ++ではPythonよりも高速です(cin.sync_with_stdio(false);行を含めた後)。それが、私が昨日抱えていた問題でした。
JJC
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.