पायथन की तुलना में C ++ में स्ट्रिंग धीमे को विभाजित क्यों किया जाता है?


93

मैं थोड़ी गति हासिल करने और अपने जंग खाए C ++ कौशल को तेज करने के प्रयास में कुछ कोड को पायथन से C ++ में बदलने की कोशिश कर रहा हूं। मैं कल हैरान जब stdin से लाइनों को पढ़ने के एक अनुभवहीन कार्यान्वयन बहुत तेजी से सी से अजगर में था ++ (देखें था इस )। आज, मुझे अंत में पता चला कि सी + + में विलय करने वाले सीमांकक के साथ एक स्ट्रिंग को कैसे विभाजित किया जाता है (अजगर के समान () के समान शब्दार्थ), और अब मैं deja vu का अनुभव कर रहा हूं! मेरा C ++ कोड काम करने में अधिक समय लेता है (हालांकि अधिक परिमाण का क्रम नहीं है, जैसा कि कल के पाठ के लिए था)।

पायथन कोड:

#!/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

ध्यान दें कि मैंने दो अलग-अलग विभाजन कार्यान्वयन की कोशिश की। एक (स्प्लिट 1) टोकन खोजने के लिए स्ट्रिंग विधियों का उपयोग करता है और कई टोकन को मर्ज करने में सक्षम है और साथ ही कई टोकन संभालता है (यह यहां से आता है )। दूसरी (स्प्लिट 2) स्ट्रिंग को स्ट्रीम के रूप में पढ़ने के लिए गेटलाइन का उपयोग करता है, सीमांकक का विलय नहीं करता है, और केवल एक एकल परिधि चरित्र का समर्थन करता है (जो कि स्ट्रिंग विभाजन के सवालों के जवाब में कई StackOverflow उपयोगकर्ताओं द्वारा पोस्ट किया गया था)।

मैंने इसे कई बार विभिन्न आदेशों में चलाया। मेरी टेस्ट मशीन मैकबुक प्रो (2011, 8 जीबी, क्वाड कोर) है, न कि यह बहुत मायने रखती है। मैं 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

मैं क्या गलत कर रहा हूं? क्या सी ++ में स्ट्रिंग विभाजन करने का एक बेहतर तरीका है जो बाहरी पुस्तकालयों (यानी कोई बढ़ावा नहीं) पर निर्भर नहीं करता है, सीमांकक के विलय के अनुक्रम का समर्थन करता है (जैसे कि अजगर का विभाजन), धागा सुरक्षित है (इसलिए कोई स्ट्रैटोक), और जिसका प्रदर्शन कम से कम है अजगर के साथ बराबर पर?

संपादित करें 1 / आंशिक समाधान ?:

मैंने कोशिश की कि अजगर की डमी लिस्ट को रीसेट करके इसे और अधिक निष्पक्ष बनाया जाए और हर बार इसे जोड़ा जाए, जैसा कि C ++ करता है। यह अभी भी ठीक वैसा नहीं है जैसा कि C ++ कोड कर रहा है, लेकिन यह थोड़ा करीब है। असल में, लूप अब है:

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

अजगर का प्रदर्शन अब विभाजन 1 सी ++ कार्यान्वयन के समान है।

/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

मुझे अभी भी आश्चर्य है कि, भले ही पायथन स्ट्रिंग प्रक्रिया के लिए अनुकूलित है (जैसा कि मैट जॉइनर ने सुझाव दिया है), कि ये सी ++ कार्यान्वयन तेजी से नहीं होंगे। यदि किसी के पास C ++ का उपयोग करके अधिक इष्टतम तरीके से यह करने के बारे में विचार हैं, तो कृपया अपना कोड साझा करें। (मुझे लगता है कि मेरा अगला कदम शुद्ध सी में इसे लागू करने की कोशिश कर रहा होगा, हालांकि मैं सी में अपनी समग्र परियोजना को फिर से लागू करने के लिए प्रोग्रामर उत्पादकता से व्यापार करने नहीं जा रहा हूं, इसलिए यह केवल स्ट्रिंग विभाजन की गति के लिए एक प्रयोग होगा।)

मदद के लिए आप सबका शुक्रिया।

अंतिम संपादन / समाधान:

कृपया अल्फ का स्वीकृत उत्तर देखें। चूंकि अजगर संदर्भ के साथ सख्ती से स्ट्रिंग्स के साथ व्यवहार करता है और एसटीएल स्ट्रिंग्स को अक्सर कॉपी किया जाता है, इसलिए प्रदर्शन वेनिला पायथन कार्यान्वयन के साथ बेहतर होता है। तुलना के लिए, मैंने अल्फ कोड के माध्यम से अपना डेटा संकलित और चलाया, और यहां उसी मशीन पर प्रदर्शन किया गया है, जो अन्य सभी रन के रूप में है, अनिवार्य रूप से भोले अजगर के कार्यान्वयन के समान है (हालांकि अजगर कार्यान्वयन की तुलना में तेज़ है जो सूची को रीसेट / अपेंड करता है) उपरोक्त संपादन में दिखाया गया है):

$ /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 ++ प्रोग्राम को कैसे संकलित किया? क्या आपके पास अनुकूलन चालू हैं?
इंटरजेड

2
@interjay: यह उनके स्रोत में अंतिम टिप्पणी है: g++ -Wall -O3 -o split1 split_1.cpp@JJC: जब आप वास्तव में dummyऔर splineक्रमशः उपयोग करते हैं, तो आपका बेंचमार्क किराया कैसे करता है, हो सकता है कि पायथन कॉल को हटा दे line.split()क्योंकि इसका कोई दुष्प्रभाव नहीं है?
एरिक

2
यदि आप बंटवारे को हटाते हैं, तो आपको क्या परिणाम मिलते हैं और स्टड से केवल पढ़ने की लाइनें छोड़ दें?
अंतरजाल

2
पायथन सी में लिखा है। इसका मतलब है कि इसे करने का एक कुशल तरीका है, सी में। शायद एसटीएल का उपयोग करने की तुलना में एक स्ट्रिंग को विभाजित करने का एक बेहतर तरीका है?
ixe013

जवाबों:


57

एक अनुमान के रूप में, पायथन स्ट्रिंग्स को संदर्भित अपरिवर्तनीय स्ट्रिंग्स के रूप में गिना जाता है, ताकि पायथन कोड में चारों ओर किसी भी तार की नकल न की जाए, जबकि C ++ std::stringएक उत्परिवर्ती मान प्रकार है, और इसे सबसे छोटे अवसर पर कॉपी किया जाता है।

यदि लक्ष्य तेजी से विभाजित हो रहा है, तो कोई निरंतर समय विकल्प संचालन का उपयोग करेगा, जिसका अर्थ केवल मूल स्ट्रिंग के कुछ हिस्सों का उल्लेख है, जैसा कि पायथन (और जावा, और सी #…) में है।

C ++ std::stringवर्ग में एक रिडीम करने की सुविधा है, हालांकि: यह मानक है , ताकि इसका उपयोग सुरक्षित रूप से और आंशिक रूप से स्ट्रिंग्स पास करने के लिए किया जा सके जहां दक्षता मुख्य विचार नहीं है। लेकिन पर्याप्त चैट। कोड - और मेरी मशीन पर यह निश्चित रूप से पायथन से अधिक तेज है, क्योंकि पायथन के स्ट्रिंग हैंडलिंग को 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
हां, पायथन स्ट्रिंग्स संदर्भित ऑब्जेक्ट्स हैं, इसलिए पायथन बहुत कम कॉपी करता है। वे अभी भी हुड के नीचे अशक्त-सी सी स्ट्रिंग हैं, हालांकि, आपके कोड की तरह (सूचक, आकार) जोड़े नहीं हैं।
फ्रेड फू

13
दूसरे शब्दों में - उच्च स्तर के काम के लिए, जैसे पाठ हेरफेर, एक उच्च स्तर की भाषा से चिपके रहते हैं, जहां इसे कुशलता से करने का प्रयास दसियों वर्षों में दसियों डेवलपर्स द्वारा संचयी रूप से किया गया है - या बस उन सभी डेवलपर्स के रूप में काम करने के लिए तैयार रहें निचले स्तर में कुछ तुलनीय होने के लिए।
18

2
@JJC: के लिए StringRef, आप सबटाइटरिंग को std::stringबहुत आसानी से कॉपी कर सकते हैं, बस string( sr.begin(), sr.end() )
चीयर्स एंड हीथ। - अल्फ

3
काश सीपीथॉन के तार कम कॉपी होते। हां, वे संदर्भित और अपरिवर्तनीय हैं, लेकिन str.split ()PyString_FromStringAndSize() उस कॉल का उपयोग करके प्रत्येक आइटम के लिए नए तार आवंटित करता है PyObject_MALLOC()। इसलिए एक साझा प्रतिनिधित्व के साथ कोई अनुकूलन नहीं है जो शोषण करता है कि पायथन में तार अपरिवर्तनीय हैं।
jfs

3
अनुरक्षक: कृपया कथित बग्स को ठीक करने का प्रयास करके कीड़े का परिचय न दें (विशेष रूप से cplusplus.com के संदर्भ में नहीं )। TIA।
चीयर्स एंड हीथ। - अल्फ

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 रेपो भी रखा: https: // github। com / टोबेज़ / स्ट्रिंग-विभाजन


2
मैंने एक पुल अनुरोध ( github.com/tobbez/string-splitting/pull/2 ) किया, जो डेटा का उपयोग करके "शब्दों और वर्णों की संख्या" की गणना करके परीक्षण को थोड़ा अधिक यथार्थवादी बनाता है। इस परिवर्तन के साथ, सभी C / C ++ संस्करणों ने पायथन संस्करणों (बूस्ट के टोकेनाइज़र के आधार पर एक के लिए उम्मीद है कि मैंने जोड़ा) और "स्ट्रिंग व्यू" आधारित विधियों (जैसे कि स्प्लिट 6) का वास्तविक मूल्य चमकता है।
डेव जोहान्सन

यदि कंपाइलर उस अनुकूलन को नोटिस करने में विफल रहता है memcpy, तो आपको उपयोग करना चाहिए , नहीं strcpystrcpyआम तौर पर एक धीमी स्टार्टअप रणनीति का उपयोग करता है जो छोटे तारों के लिए उपवास के बीच संतुलन बनाता है। लंबे स्ट्रिंग्स के लिए पूर्ण SIMD तक रैंप। memcpyआकार का तुरंत पता चलता है, और किसी अंतर्निहित लंबाई वाले स्ट्रिंग के अंत की जांच करने के लिए किसी भी SIMD ट्रिक्स का उपयोग नहीं करना पड़ता है। (आधुनिक x86 पर कोई बड़ी बात नहीं)। कंस्ट्रक्टर के std::stringसाथ ऑब्जेक्ट बनाना और (char*, len)भी तेज़ हो सकता है, अगर आप इसे बाहर निकाल सकते हैं saveptr-token। स्पष्ट रूप से यह सिर्फ char*टोकन स्टोर करने के लिए सबसे तेज़ होगा : P
पीटर कॉर्डेस

4

मुझे संदेह है कि यह std::vectorएक पुश_बैक () फ़ंक्शन कॉल की प्रक्रिया के दौरान आकार बदलने के कारण होता है। यदि आप वाक्यों के लिए पर्याप्त स्थान का उपयोग std::listया std::vector::reserve()आरक्षित करने का प्रयास करते हैं , तो आपको बहुत बेहतर प्रदर्शन प्राप्त करना चाहिए। या आप नीचे दिए गए विभाजन 1 () के लिए दोनों के संयोजन का उपयोग कर सकते हैं:

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

संपादित करें : दूसरी स्पष्ट चीज जो मैं देख रहा हूं वह यह है कि पायथन चर को हर बार असाइन कियाdummy जाता है लेकिन संशोधित नहीं किया जाता है। इसलिए यह C ++ के खिलाफ उचित तुलना नहीं है। आपको इसे प्रारंभ करने और फिर करने के लिए अपने पायथन कोड को संशोधित करने का प्रयास करना चाहिए । क्या आप इसके बाद रनटाइम रिपोर्ट कर सकते हैं?dummy = []dummy += line.split()

EDIT2 : इसे और भी उचित बनाने के लिए आप 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) की भी कोशिश की, लेकिन इसका मेरे स्प्लिट 1 की गति पर कोई प्रभाव नहीं पड़ा। प्रति पंक्ति में केवल तीन टोकन हैं, और प्रत्येक पंक्ति के बाद वेक्टर को साफ किया जाता है, इसलिए मुझे उम्मीद नहीं थी कि बहुत मदद मिलेगी।
JJC

मैंने आपके संपादन की भी कोशिश की। कृपया अद्यतन प्रश्न देखें। विभाजन 1 के साथ अब प्रदर्शन बराबर है।
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 है, और अंतिम आकार एन है, आप लॉग 2 (एन) बार के लिए आवंटित और डीललोकेट करेंगे, और आप कॉपी करेंगे (2 ^ (लॉग 2 (एन) + 1) - 1) = (2N - 1) बार। जैसा कि अंदर बताया गया है एसटीडी का खराब प्रदर्शन :: वेक्टर वास्तविक समय पर एक लघुगणक संख्या को न कहने के कारण है? , यह एक खराब प्रदर्शन हो सकता है जब वेक्टर का आकार अप्रत्याशित है और बहुत बड़ा हो सकता है। लेकिन, यदि आप इसके आकार का अनुमान लगा सकते हैं, तो यह कम समस्या होगी।

  2. std::list

    प्रत्येक पुश_बैक के लिए, इसका उपभोग किया गया समय एक स्थिर होता है, लेकिन संभवतः यह व्यक्तिगत पुश_बैक पर std :: वेक्टर से अधिक समय लेगा। प्रति-थ्रेड मेमोरी पूल और एक कस्टम एलोकेटर का उपयोग करके इस समस्या को कम किया जा सकता है।

  3. std::forward_list

    एसटीडी :: सूची के समान, लेकिन प्रति तत्व कम मेमोरी पर कब्जा। एपीआई पुश_बैक की कमी के कारण काम करने के लिए एक रैपर क्लास की आवश्यकता होती है।

  4. std::array

    यदि आप विकास की सीमा को जान सकते हैं, तो आप std :: array का उपयोग कर सकते हैं। कारण, आप इसे सीधे उपयोग नहीं कर सकते, क्योंकि इसमें API push_back नहीं है। लेकिन आप एक आवरण को परिभाषित कर सकते हैं, और मुझे लगता है कि यह यहां सबसे तेज़ तरीका है और कुछ स्मृति को बचा सकता है यदि आपका अनुमान काफी सटीक है।

  5. std::deque

    यह विकल्प आपको प्रदर्शन के लिए मेमोरी का व्यापार करने की अनुमति देता है। तत्व की कोई (2 ^ (N + 1) - 1) बार कॉपी नहीं होगी, बस N बार आवंटन, और कोई सौदा नहीं होगा। इसके अलावा, आपके पास निरंतर रैंडम एक्सेस समय होगा, और दोनों सिरों पर नए तत्वों को जोड़ने की क्षमता होगी।

Std :: deque-cppreference के अनुसार

दूसरी ओर, देवताओं में आम तौर पर बड़ी न्यूनतम स्मृति लागत होती है; केवल एक तत्व को रखने वाले एक छल को अपने पूर्ण आंतरिक सरणी को आवंटित करना पड़ता है (उदाहरण के लिए, वस्तु का आकार 64-बिट libddc ++ पर 8 गुना; 16 गुना ऑब्जेक्ट आकार या 4096 बाइट्स, जो भी बड़ा हो, 64-बिट libc ++ पर)

या आप इनमें से कॉम्बो का उपयोग कर सकते हैं:

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

    यह std :: deque के समान है, फर्क सिर्फ इतना है कि यह कंटेनर सामने वाले तत्व को जोड़ने के लिए समर्थन नहीं करता है। लेकिन यह प्रदर्शन में अभी भी तेज है, इस तथ्य के कारण कि यह अंतर्निहित std की प्रतिलिपि नहीं बनायेगा :: सरणी के लिए सरणी (2 ^ (N + 1) - 1) बार, यह सिर्फ सूचक सरणी की प्रतिलिपि करेगा (2 ^) (एन - एम + 1) - 1) बार, और नए सरणी का आवंटन केवल तभी होता है जब करंट भर जाता है और किसी चीज को डीलिट करने की आवश्यकता नहीं होती है। वैसे, आप लगातार यादृच्छिक एक्सेस समय प्राप्त कर सकते हैं।

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

    मेमोरी फ्रस्टेशन के दबाव को बहुत कम करें। यह केवल नई सरणी आवंटित करेगा जब वर्तमान भरा हुआ है, और कुछ भी कॉपी करने की आवश्यकता नहीं है। आपको अभी भी कॉम्बो 1 के लिए एक अतिरिक्त पॉइंटर के लिए कीमत चुकानी होगी।

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

    2 के रूप में ही, लेकिन कॉम्बो 1 के रूप में एक ही स्मृति लागत।


यदि आप std :: वेक्टर का उपयोग कुछ उचित प्रारंभिक आकार के साथ करते हैं, जैसे कि 128 या 256, कुल प्रतियां (2 का ग्रोथ फैक्टर मानकर), तो आप उस सीमा तक आकार के लिए किसी भी नकल से बचते हैं। फिर आप उन एलिमेंट को फिट करने के लिए आवंटन को सिकोड़ सकते हैं जो आपने वास्तव में उपयोग किए थे इसलिए यह छोटे इनपुट के लिए भयानक नहीं है। यह बहुत बड़े Nमामले के लिए प्रतियां की कुल संख्या के साथ ज्यादा मदद नहीं करता है , हालांकि। यह बहुत बुरा एसटीडी है: वेक्टर reallocवर्तमान आवंटन के अंत में संभावित रूप से अधिक पृष्ठों की मैपिंग की अनुमति नहीं दे सकता है , इसलिए यह 2x धीमी है।
पीटर कॉर्डेस

क्या stringview::remove_prefixकेवल एक सामान्य स्ट्रिंग में अपनी वर्तमान स्थिति का ध्यान रखना सस्ता है? std::basic_string::findआपके पास एक pos = 0ऑफसेट से खोज शुरू करने के लिए एक वैकल्पिक 2 arg है।
पीटर कॉर्डेस

@ पीटर कॉर्डेस यह सही है। मैंने libcxx प्रत्यारोपण की
जियाहो जू

मैंने libstdc ++ इम्प्लांट भी चेक किया , जो समान है।
जियाहो जू

वेक्टर के प्रदर्शन का आपका विश्लेषण बंद है। एक वेक्टर पर विचार करें जिसकी शुरुआती क्षमता 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 ++ कार्यान्वयन आवश्यक रूप से पायथन से अधिक तेज है। पायथन में स्ट्रिंग हैंडलिंग अत्यधिक अनुकूलित है। इस प्रश्न को और देखें: std :: string operation खराब प्रदर्शन क्यों करते हैं?


4
मैं समग्र भाषा प्रदर्शन के बारे में कोई दावा नहीं कर रहा हूं, केवल मेरे विशेष कोड के बारे में। इसलिए, यहाँ कोई धारणा नहीं है। अन्य प्रश्न के लिए अच्छे पॉइंटर के लिए धन्यवाद। मुझे यकीन नहीं है कि यदि आप कह रहे हैं कि C ++ में यह विशेष कार्यान्वयन सबप्टिमल (आपका पहला वाक्य) है या यह कि C ++ स्ट्रिंग प्रोसेसिंग (आपके दूसरे वाक्य) में पायथन की तुलना में धीमा है। इसके अलावा, यदि आप C ++ में जो करने की कोशिश कर रहे हैं, उसे करने का एक तेज़ तरीका जानते हैं, तो कृपया इसे सभी के लाभ के लिए साझा करें। धन्यवाद। बस स्पष्ट करने के लिए, मैं अजगर से प्यार करता हूं, लेकिन मैं कोई अंधा नहीं हूं, जो यही कारण है कि मैं ऐसा करने का सबसे तेज़ तरीका सीखने की कोशिश कर रहा हूं।
जेजेसी

1
@JJC: यह देखते हुए कि पायथन का कार्यान्वयन तेज है, मैं कहूंगा कि आपका उप-विषयक है। ध्यान रखें कि भाषा कार्यान्वयन आपके लिए कोनों को काट सकता है, लेकिन अंततः एल्गोरिथम जटिलता और हाथ अनुकूलन अनुकूलन जीतते हैं। इस मामले में, डिफ़ॉल्ट रूप से इस उपयोग के मामले में पायथन का ऊपरी हाथ है।
मैट जॉइनर

2

यदि आप विभाजित 1 क्रियान्वयन को लेते हैं और हस्ताक्षर को अधिक बदलकर विभाजित करते हैं, तो इसे बदलकर, विभाजन 2 से अधिक निकटता से जोड़ सकते हैं:

void split1(vector<string> &tokens, const string &str, const string &delimiters = " ")

इसके लिए:

void split1(vector<string> &tokens, const string &str, const char delimiters = ' ')

आपको स्प्लिट 1 और स्प्लिट 2 और एक निष्पक्ष तुलना के बीच अधिक नाटकीय अंतर मिलता है:

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

मेरी मशीन पर: स्प्लिट १ - ५४, स्प्लिटडोम - ३५ स, स्प्लिट ५ - १६ से। मुझे पता नहीं है।
एन। 'सर्वनाम' मी।

हम्म, क्या आपका डेटा मेरे द्वारा बताए गए प्रारूप से मेल खाता है? मुझे लगता है कि आप प्रारंभिक डिस्क कैश जनसंख्या जैसे क्षणिक प्रभाव को खत्म करने के लिए हर बार दौड़े?
JJC

0

मुझे संदेह है कि यह पायथन में sys.stdin पर बफरिंग से संबंधित है, लेकिन C ++ कार्यान्वयन में कोई बफरिंग नहीं है।

बफ़र आकार को बदलने के तरीके के बारे में जानकारी के लिए यह पोस्ट देखें, फिर तुलना करने का प्रयास करें: sys.stdin के लिए छोटे बफ़र आकार की स्थापना?


1
हम्म् ... मैं अनुसरण नहीं करता। सिर्फ पठन (विभाजन के बिना) पायथन की तुलना में C ++ में तेज है (बाद में cin.sync_with_stdio (झूठी); लाइन शामिल)। यह वह मुद्दा था जो मैंने कल, ऊपर संदर्भित किया था।
JJC
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.