เหตุใดการแยกสตริงใน C ++ จึงช้ากว่า Python


94

ฉันกำลังพยายามแปลงรหัสจาก Python เป็น C ++ เพื่อเพิ่มความเร็วและเพิ่มทักษะ C ++ ที่เป็นสนิมของฉัน เมื่อวานนี้ฉันรู้สึกตกใจเมื่อการใช้งานบรรทัดการอ่านอย่างไร้เดียงสาจาก stdin ใน Python เร็วกว่า C ++ มาก (ดูสิ่งนี้ ) วันนี้ในที่สุดฉันก็ค้นพบวิธีการแยกสตริงใน C ++ ด้วยการรวมตัวคั่น (ความหมายที่คล้ายกันกับการแยกของ python ()) และตอนนี้ฉันกำลังประสบกับ deja vu! รหัส 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

โปรดทราบว่าฉันได้ลองใช้การแยกสองแบบที่แตกต่างกัน หนึ่ง (แยก 1) ใช้วิธีการสตริงเพื่อค้นหาโทเค็นและสามารถรวมโทเค็นหลายตัวรวมทั้งจัดการโทเค็นจำนวนมากได้ (มาจากที่นี่ ) ตัวที่สอง (split2) ใช้ getline เพื่ออ่านสตริงเป็นสตรีมไม่รวมตัวคั่นและรองรับเฉพาะอักขระเดลิมิเตอร์ตัวเดียว (อันนี้โพสต์โดยผู้ใช้ StackOverflow หลายรายในคำตอบสำหรับคำถามการแยกสตริง)

ฉันทำงานนี้หลายครั้งในคำสั่งต่างๆ เครื่องทดสอบของฉันคือ Macbook Pro (2011, 8GB, Quad Core) ไม่ใช่ว่าจะสำคัญมาก ฉันกำลังทดสอบกับไฟล์ข้อความบรรทัด 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

ผมทำอะไรผิดหรือเปล่า? มีวิธีที่ดีกว่าไหมในการแยกสตริงใน C ++ ที่ไม่ต้องอาศัยไลบรารีภายนอก (เช่นไม่มีการเพิ่ม) รองรับการรวมลำดับของตัวคั่น (เช่นการแยกของ python) เธรดที่ปลอดภัย (ดังนั้นจึงไม่มี strtok) และมีประสิทธิภาพอย่างน้อยที่สุด พอ ๆ กับ python?

แก้ไข 1 / โซลูชันบางส่วน?:

ฉันพยายามทำให้เป็นการเปรียบเทียบที่ยุติธรรมยิ่งขึ้นโดยให้ python รีเซ็ตรายการดัมมี่และต่อท้ายทุกครั้งตามที่ C ++ ทำ นี่ยังไม่ตรงกับสิ่งที่โค้ด C ++ กำลังทำอยู่ แต่ใกล้กว่าเล็กน้อย โดยทั่วไปลูปตอนนี้คือ:

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

ขณะนี้ประสิทธิภาพของ python ใกล้เคียงกับการใช้งาน C ++ ของ Split1

/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 มักจะถูกคัดลอกประสิทธิภาพจะดีกว่าเมื่อใช้งาน vanilla 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 ++ เพื่อใช้ในกรณีนี้

หนึ่งในบทเรียนจากปัญหานี้และปัญหาการอ่านบรรทัด stdin ของเมื่อวาน (ลิงก์ด้านบน) คือควรใช้เกณฑ์มาตรฐานแทนที่จะตั้งสมมติฐานที่ไร้เดียงสาเกี่ยวกับประสิทธิภาพ "ค่าเริ่มต้น" ที่สัมพันธ์กันของภาษา ขอชื่นชมการศึกษา

ขอขอบคุณอีกครั้งสำหรับคำแนะนำของคุณ!


2
คุณรวบรวมโปรแกรม C ++ ได้อย่างไร? คุณเปิดการเพิ่มประสิทธิภาพไว้หรือไม่
interjay

2
@interjay: อยู่ในความคิดเห็นสุดท้ายในแหล่งที่มาของเขา: g++ -Wall -O3 -o split1 split_1.cpp@JJC: ค่าโดยสารมาตรฐานของคุณเป็นอย่างไรเมื่อคุณใช้งานจริงdummyและsplineตามลำดับ Python อาจลบการโทรออกline.split()เนื่องจากไม่มีผลข้างเคียง?
Eric

2
คุณจะได้ผลลัพธ์อะไรหากคุณลบการแยกและปล่อยให้อ่านเฉพาะบรรทัดจาก stdin?
interjay

2
Python เขียนด้วย C หมายความว่ามีวิธีที่มีประสิทธิภาพในการทำใน C อาจมีวิธีที่ดีกว่าในการแยกสตริงมากกว่าการใช้ STL?
ixe013

คำตอบ:


58

ตามที่คาดเดาสตริง Python เป็นการอ้างอิงที่นับสตริงที่ไม่เปลี่ยนรูปดังนั้นจึงไม่มีการคัดลอกสตริงในโค้ด Python ในขณะที่ C ++ std::stringเป็นประเภทค่าที่ไม่แน่นอนและจะถูกคัดลอกในโอกาสที่น้อยที่สุด

ถ้าเป้าหมายคือการแยกอย่างรวดเร็วจากนั้นหนึ่งจะใช้เวลาคง substring การดำเนินงานซึ่งหมายถึงเฉพาะการอ้างอิงไปยังส่วนของสายเดิมเช่นเดียวกับในหลาม (และ 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

คำเตือน: ฉันหวังว่าจะไม่มีข้อบกพร่องใด ๆ ฉันยังไม่ได้ทดสอบการทำงาน แต่ตรวจสอบความเร็วเท่านั้น แต่ฉันคิดว่าแม้ว่าจะมีจุดบกพร่องหรือสองข้อ แต่การแก้ไขนั้นจะไม่ส่งผลกระทบต่อความเร็วมากนัก


2
ใช่สตริง Python เป็นวัตถุที่นับอ้างอิงดังนั้น Python จึงคัดลอกน้อยกว่ามาก พวกเขายังคงมีสตริง C ที่สิ้นสุดด้วย null ภายใต้ประทุนแม้ว่าจะไม่ใช่คู่ (ตัวชี้ขนาด) เหมือนรหัสของคุณ
Fred Foo

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 - Alf

9

ฉันไม่ได้ให้โซลูชันที่ดีกว่านี้ (อย่างน้อยก็มีประสิทธิภาพ) แต่มีข้อมูลเพิ่มเติมบางอย่างที่น่าสนใจ

การใช้strtok_r(reentrant variant of 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 repo พร้อมกับโปรแกรมทั้งหมดจากคำถามคำตอบที่ยอมรับคำตอบนี้และนอกจากนี้ Makefile และสคริปต์สำหรับสร้างข้อมูลทดสอบ: https: // github co.th


2
ฉันส่งคำขอดึง ( github.com/tobbez/string-splitting/pull/2 ) ซึ่งทำให้การทดสอบมีความสมจริงมากขึ้นโดย "ใช้" ข้อมูล (การนับจำนวนคำและอักขระ) ด้วยการเปลี่ยนแปลงนี้เวอร์ชัน C / C ++ ทั้งหมดจะเอาชนะเวอร์ชัน Python ได้ (คาดว่าจะเป็นเวอร์ชันที่อิงตามโทเค็นไนเซอร์ของ Boost ที่ฉันเพิ่ม) และมูลค่าที่แท้จริงของวิธีการตาม "มุมมองสตริง" (เช่นเดียวกับการแยก 6)
Dave Johansen

คุณควรใช้memcpyไม่ใช่strcpyในกรณีที่คอมไพลเลอร์ไม่สังเกตเห็นการเพิ่มประสิทธิภาพนั้น strcpyโดยทั่วไปจะใช้กลยุทธ์การเริ่มต้นที่ช้าลงซึ่งจะสร้างความสมดุลระหว่างการเร็วสำหรับสตริงสั้นกับการเพิ่มขึ้นเป็น 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());
}

แก้ไข : สิ่งที่ชัดเจนอีกอย่างที่ฉันเห็นคือตัวแปร Python dummyได้รับการกำหนดทุกครั้ง แต่ไม่มีการแก้ไข ดังนั้นจึงไม่ใช่การเปรียบเทียบที่ยุติธรรมกับ C ++ คุณควรพยายามปรับเปลี่ยนรหัสหลามของคุณจะสามารถเริ่มต้นมันแล้วทำdummy = [] dummy += line.split()คุณสามารถรายงานรันไทม์หลังจากนี้ได้หรือไม่?

แก้ไข 2 : เพื่อให้ยุติธรรมยิ่งขึ้นคุณสามารถแก้ไข while loop ในโค้ด 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++;
    };

ขอบคุณสำหรับความคิด ฉันใช้งานได้จริงและการใช้งานนี้ช้ากว่า Split1 ดั้งเดิม แต่น่าเสียดาย ฉันยังลอง spline.reserve (16) ก่อนลูป แต่สิ่งนี้ไม่มีผลต่อความเร็วของ Split1 ของฉัน มีเพียงสามโทเค็นต่อบรรทัดและเวกเตอร์จะถูกล้างหลังจากแต่ละบรรทัดดังนั้นฉันไม่ได้คาดหวังว่าจะช่วยได้มากนัก
JJC

ฉันลองแก้ไขของคุณแล้วเช่นกัน โปรดดูคำถามที่อัปเดต ขณะนี้ประสิทธิภาพเทียบเท่ากับ Split1
JJC

ฉันลอง EDIT2 ของคุณแล้ว ประสิทธิภาพแย่ลงเล็กน้อย: $ / usr / bin / time cat test_lines_double | ./split7 33.39 จริง 0.01 ผู้ใช้ 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) = (2N - 1) ครั้ง ตามที่ระบุไว้ในstd :: vector มีประสิทธิภาพต่ำเนื่องจากไม่เรียก realloc จำนวนครั้งลอการิทึมหรือไม่ ซึ่งอาจมีประสิทธิภาพต่ำเมื่อขนาดของเวกเตอร์ไม่สามารถคาดเดาได้และอาจมีขนาดใหญ่มาก แต่ถ้าคุณสามารถประมาณขนาดของมันได้ก็จะเป็นปัญหาน้อยลง

  2. std::list.

    สำหรับ push_back ทุกครั้งเวลาที่ใช้จะเป็นค่าคงที่ แต่อาจใช้เวลามากกว่า std :: vector ใน push_back แต่ละรายการ การใช้พูลหน่วยความจำต่อเธรดและตัวจัดสรรแบบกำหนดเองสามารถบรรเทาปัญหานี้ได้

  3. std::forward_list.

    เหมือนกับ std :: list แต่ใช้หน่วยความจำน้อยกว่าต่อองค์ประกอบ กำหนดให้คลาส Wrapper ทำงานเนื่องจากไม่มี API push_back

  4. std::array.

    หากคุณสามารถทราบขีด จำกัด ของการเติบโตคุณสามารถใช้ std :: array ด้วยเหตุนี้คุณไม่สามารถใช้โดยตรงได้เนื่องจากไม่มี API push_back แต่คุณสามารถกำหนด wrapper ได้และฉันคิดว่ามันเป็นวิธีที่เร็วที่สุดที่นี่และสามารถบันทึกหน่วยความจำได้หากการประมาณของคุณค่อนข้างแม่นยำ

  5. std::deque.

    ตัวเลือกนี้ช่วยให้คุณสามารถแลกเปลี่ยนหน่วยความจำเพื่อประสิทธิภาพ จะไม่มีสำเนา (2 ^ (N + 1) - 1) เท่าของการจัดสรรเพียง N เท่าและไม่มีการจัดสรร นอกจากนี้คุณจะมีเวลาในการเข้าถึงแบบสุ่มคงที่และความสามารถในการเพิ่มองค์ประกอบใหม่ที่ปลายทั้งสองด้าน

ตามมาตรฐาน :: deque-cppreference

ในทางกลับกัน deques มักมีต้นทุนหน่วยความจำน้อยที่สุด deque ที่ถือองค์ประกอบเพียงตัวเดียวจะต้องจัดสรรอาร์เรย์ภายในแบบเต็ม (เช่นขนาดวัตถุ 8 เท่าบน libstdc ++ 64 บิต; 16 เท่าของขนาดวัตถุหรือ 4096 ไบต์แล้วแต่จำนวนใดจะใหญ่กว่าบน libc ++ 64 บิต)

หรือคุณสามารถใช้คำสั่งผสมเหล่านี้:

  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 เท่า
Peter Cordes

คือstringview::remove_prefixถูกเป็นเพียงการติดตามตำแหน่งปัจจุบันของคุณในสตริงปกติ? std::basic_string::findมีอาร์กิวเมนต์ที่ 2 ซึ่งเป็นทางเลือกpos = 0เพื่อให้คุณเริ่มค้นหาจากออฟเซ็ต
Peter Cordes

@ Peter Cordes นั่นถูกต้อง ฉันตรวจสอบlibcxx im
JiaHao Xu

ฉันยังตรวจสอบlibstdc ++ imซึ่งเหมือนกัน
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มาก
David Stone

2

คุณกำลังเข้าใจผิดว่าการใช้งาน C ++ ที่คุณเลือกนั้นจำเป็นต้องเร็วกว่า Python การจัดการสตริงใน Python ได้รับการปรับให้เหมาะสมอย่างมาก ดูคำถามนี้เพิ่มเติม: เหตุใดการดำเนินการ std :: string จึงทำงานได้ไม่ดี?


4
ฉันไม่ได้อ้างสิทธิ์ใด ๆ เกี่ยวกับประสิทธิภาพภาษาโดยรวม แต่เกี่ยวกับรหัสเฉพาะของฉัน ดังนั้นไม่มีสมมติฐานที่นี่ ขอบคุณสำหรับตัวชี้ที่ดีสำหรับคำถามอื่น ๆ ฉันไม่แน่ใจว่าคุณกำลังบอกว่าการนำไปใช้งานเฉพาะใน C ++ นี้ต่ำกว่า (ประโยคแรกของคุณ) หรือ C ++ ช้ากว่า Python ในการประมวลผลสตริง (ประโยคที่สองของคุณ) นอกจากนี้หากคุณรู้วิธีที่รวดเร็วในการทำสิ่งที่ฉันพยายามทำใน C ++ โปรดแบ่งปันเพื่อประโยชน์ของทุกคน ขอบคุณ. ฉันรัก python แต่ฉันไม่ใช่แฟนบอยตาบอดนั่นคือเหตุผลที่ฉันพยายามเรียนรู้วิธีที่เร็วที่สุดในการทำสิ่งนี้
JJC

1
@JJC: เนื่องจากการใช้งาน Python เร็วขึ้นฉันจะบอกว่าคุณเป็นคนไม่ดี โปรดทราบว่าการปรับใช้ภาษาสามารถลดทอนมุมของคุณได้ แต่ในที่สุดความซับซ้อนของอัลกอริทึมและการเพิ่มประสิทธิภาพด้วยมือก็จะชนะ ในกรณีนี้ Python มีความสำคัญสำหรับกรณีการใช้งานนี้โดยค่าเริ่มต้น
Matt Joiner

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 ++: เห็น 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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.