Tại sao việc chia nhỏ một chuỗi trong C ++ chậm hơn Python?


93

Tôi đang cố gắng chuyển đổi một số mã từ Python sang C ++ với nỗ lực để đạt được một chút tốc độ và rèn giũa các kỹ năng C ++ vốn có của mình. Hôm qua, tôi đã bị sốc khi việc triển khai các dòng đọc từ stdin trong Python nhanh hơn nhiều so với C ++ (xem phần này ). Hôm nay, cuối cùng tôi đã tìm ra cách tách một chuỗi trong C ++ bằng hợp nhất các dấu phân cách (ngữ nghĩa tương tự như split ()) của python và bây giờ tôi đang trải nghiệm deja vu! Mã C ++ của tôi mất nhiều thời gian hơn để thực hiện công việc (mặc dù không phải là một thứ tự lớn hơn, như trường hợp của bài học ngày hôm qua).

Mã 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('')

Mã 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

Lưu ý rằng tôi đã thử hai cách triển khai phân tách khác nhau. Một (split1) sử dụng các phương thức chuỗi để tìm kiếm mã thông báo và có thể hợp nhất nhiều mã thông báo cũng như xử lý nhiều mã thông báo (nó xuất phát từ đây ). Thứ hai (split2) sử dụng getline để đọc chuỗi dưới dạng một luồng, không hợp nhất các dấu phân cách và chỉ hỗ trợ một ký tự mê sảng duy nhất (ký tự đó đã được một số người dùng StackOverflow đăng trong câu trả lời cho các câu hỏi về tách chuỗi).

Tôi đã chạy điều này nhiều lần trong các đơn đặt hàng khác nhau. Máy thử nghiệm của tôi là Macbook Pro (2011, 8GB, Quad Core), không có vấn đề gì nhiều. Tôi đang thử nghiệm với tệp văn bản 20M dòng có ba cột được phân tách bằng dấu cách mà mỗi cột trông tương tự như sau: "foo.bar 127.0.0.1 home.foo.bar"

Các kết quả:

$ /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

Tôi đang làm gì sai? Có cách nào tốt hơn để thực hiện phân tách chuỗi trong C ++ mà không dựa vào các thư viện bên ngoài (tức là không tăng cường), hỗ trợ hợp nhất chuỗi các dấu phân cách (như phân tách của python), luồng an toàn (vì vậy không có strtok) và có hiệu suất ít nhất ngang hàng với trăn?

Chỉnh sửa 1 / Giải pháp một phần ?:

Tôi đã thử làm cho nó một so sánh công bằng hơn bằng cách yêu cầu python đặt lại danh sách giả và thêm vào đó mỗi lần, như C ++. Đây vẫn không phải là chính xác những gì mã C ++ đang làm, nhưng nó gần hơn một chút. Về cơ bản, vòng lặp bây giờ là:

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

Hiệu suất của python bây giờ giống như việc thực hiện 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

Tôi vẫn ngạc nhiên rằng, ngay cả khi Python được tối ưu hóa để xử lý chuỗi (như Matt Joiner đã đề xuất), thì các triển khai C ++ này sẽ không nhanh hơn. Nếu ai có ý tưởng về cách thực hiện việc này theo cách tối ưu hơn bằng C ++, vui lòng chia sẻ mã của bạn. (Tôi nghĩ rằng bước tiếp theo của tôi sẽ cố gắng thực hiện điều này bằng C thuần túy, mặc dù tôi sẽ không đánh đổi năng suất của lập trình viên để triển khai lại dự án tổng thể của mình trong C, vì vậy đây sẽ chỉ là một thử nghiệm cho tốc độ tách chuỗi.)

Cảm ơn tất cả sự giúp đỡ của bạn.

Chỉnh sửa / Giải pháp cuối cùng:

Hãy xem câu trả lời được chấp nhận của Alf. Vì python xử lý các chuỗi một cách nghiêm ngặt bằng tham chiếu và các chuỗi STL thường được sao chép, nên hiệu suất sẽ tốt hơn với các triển khai vani python. Để so sánh, tôi đã biên dịch và chạy dữ liệu của mình thông qua mã của Alf và đây là hiệu suất trên cùng một máy như tất cả các lần chạy khác, về cơ bản giống với triển khai python ngây thơ (mặc dù nhanh hơn triển khai python đặt lại / nối danh sách, như hiển thị trong chỉnh sửa ở trên):

$ /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

Điều duy nhất còn lại của tôi là về số lượng mã cần thiết để C ++ thực hiện trong trường hợp này.

Một trong những bài học ở đây từ vấn đề này và vấn đề đọc dòng stdin ngày hôm qua (được liên kết ở trên) là người ta nên luôn đánh giá chuẩn thay vì đưa ra các giả định ngây thơ về hiệu suất "mặc định" tương đối của các ngôn ngữ. Tôi đánh giá cao sự giáo dục.

Cảm ơn một lần nữa cho tất cả các đề xuất của bạn!


2
Bạn đã biên dịch chương trình C ++ như thế nào? Bạn đã bật tối ưu hóa chưa?
interjay

2
@interjay: Đó là trong nhận xét cuối cùng trong nguồn của anh ấy: g++ -Wall -O3 -o split1 split_1.cpp@JJC: Điểm chuẩn của bạn như thế nào khi bạn thực sự sử dụng dummysplinetương ứng, có thể Python loại bỏ lệnh gọi đến line.split()vì nó không có tác dụng phụ?
Eric

2
Bạn sẽ nhận được kết quả gì nếu bỏ phần tách và chỉ để lại những dòng đọc từ stdin?
interjay

2
Python được viết bằng C. Có nghĩa là có một cách hiệu quả để làm điều đó, bằng C. Có lẽ có cách nào tốt hơn để tách một chuỗi so với sử dụng STL?
ixe013

Câu trả lời:


57

Theo dự đoán, chuỗi Python là chuỗi bất biến được đếm tham chiếu, do đó không có chuỗi nào được sao chép xung quanh trong mã Python, trong khi C ++ std::stringlà loại giá trị có thể thay đổi và được sao chép ở cơ hội nhỏ nhất.

Nếu mục tiêu là phân tách nhanh, thì người ta sẽ sử dụng các phép toán chuỗi con thời gian không đổi, có nghĩa là chỉ tham chiếu đến các phần của chuỗi ban đầu, như trong Python (và Java, và C #…).

Tuy nhiên, std::stringlớp C ++ có một tính năng quy đổi: nó là tiêu chuẩn , để nó có thể được sử dụng để chuyển các chuỗi một cách an toàn và di động ở những nơi mà hiệu quả không phải là vấn đề chính. Nhưng đủ trò chuyện. Mã - và trên máy của tôi, điều này tất nhiên nhanh hơn Python, vì việc xử lý chuỗi của Python được thực hiện bằng C, là một tập con của C ++ (he he):

#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

Tuyên bố từ chối trách nhiệm: Tôi hy vọng không có bất kỳ lỗi nào. Tôi chưa kiểm tra chức năng mà chỉ kiểm tra tốc độ. Nhưng tôi nghĩ, ngay cả khi có một hoặc hai lỗi, việc sửa chữa đó sẽ không ảnh hưởng đáng kể đến tốc độ.


2
Có, các chuỗi Python là các đối tượng được đếm tham chiếu, vì vậy Python ít sao chép hơn nhiều. Tuy nhiên, chúng vẫn chứa các chuỗi C được kết thúc bằng null bên dưới, không phải các cặp (con trỏ, kích thước) như mã của bạn.
Fred Foo

13
Nói cách khác - đối với công việc ở cấp cao hơn, như thao tác văn bản, hãy sử dụng ngôn ngữ cấp cao hơn, nơi nỗ lực để thực hiện nó một cách hiệu quả đã được hàng chục nhà phát triển tích lũy trong hàng chục năm - hoặc chỉ cần chuẩn bị làm việc nhiều như tất cả các nhà phát triển đó vì có thứ gì đó có thể so sánh được ở cấp thấp hơn.
jsbueno 21/02/12

2
@JJC: đối với StringRef, bạn có thể sao chép chuỗi con vào một std::stringrất dễ dàng, chỉ string( sr.begin(), sr.end() ).
Chúc mừng và hth. - Alf

3
Tôi ước chuỗi CPython được sao chép ít hơn. Có, chúng là tham chiếu được đếm và không thay đổi nhưng str.split () phân bổ các chuỗi mới cho mỗi mục bằng cách sử dụng PyString_FromStringAndSize()lệnh gọi đó PyObject_MALLOC(). Vì vậy, không có sự tối ưu hóa nào với một đại diện được chia sẻ khai thác rằng các chuỗi là bất biến trong Python.
jfs

3
Người bảo trì: vui lòng không giới thiệu lỗi bằng cách cố gắng sửa các lỗi đã nhận thấy (đặc biệt là không liên quan đến cplusplus.com ). TIA.
Chúc mừng và hth. - Alf

9

Tôi không cung cấp bất kỳ giải pháp nào tốt hơn (ít nhất là về hiệu suất), nhưng một số dữ liệu bổ sung có thể thú vị.

Sử dụng strtok_r(biến thể tái xuất bản của 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);
}

Ngoài ra, sử dụng các chuỗi ký tự cho các tham số và fgetscho đầu vào:

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

Và, trong một số trường hợp, việc hủy chuỗi đầu vào được chấp nhận:

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

Thời gian cho những điều này như sau (bao gồm kết quả của tôi cho các biến thể khác từ câu hỏi và câu trả lời được chấp nhận):

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

Như chúng ta có thể thấy, giải pháp từ câu trả lời được chấp nhận vẫn là nhanh nhất.

Đối với bất kỳ ai muốn thực hiện thêm các bài kiểm tra khác, tôi cũng tạo một repo Github với tất cả các chương trình từ câu hỏi, câu trả lời được chấp nhận, câu trả lời này và thêm vào đó là Makefile và script để tạo dữ liệu kiểm tra: https: // github. com / tobbez / chuỗi-tách .


2
Tôi đã thực hiện một yêu cầu kéo ( github.com/tobbez/string-splitting/pull/2 ) để làm cho thử nghiệm thực tế hơn một chút bằng cách "sử dụng" dữ liệu (đếm số từ và ký tự). Với sự thay đổi này, tất cả các phiên bản C / C ++ đều đánh bại các phiên bản Python (mong đợi phiên bản dựa trên tokenizer của Boost mà tôi đã thêm) và giá trị thực của các phương thức dựa trên "chế độ xem chuỗi" (như của split6) tỏa sáng.
Dave Johansen

Bạn nên sử dụng memcpy, không strcpy, trong trường hợp trình biên dịch không nhận thấy rằng tối ưu hóa. strcpythường sử dụng chiến lược khởi động chậm hơn nhằm tạo ra sự cân bằng giữa tốc độ nhanh đối với các chuỗi ngắn và tăng lên đến SIMD đầy đủ đối với các chuỗi dài. memcpybiết kích thước ngay lập tức và không phải sử dụng bất kỳ thủ thuật SIMD nào để kiểm tra phần cuối của một chuỗi có độ dài ngầm định. (Không phải là vấn đề lớn trên x86 hiện đại). Việc tạo std::stringcác đối tượng với hàm (char*, len)tạo cũng có thể nhanh hơn, nếu bạn có thể giải quyết được điều đó saveptr-token. Rõ ràng là sẽ nhanh nhất nếu chỉ lưu trữ char*mã thông báo: P
Peter Cordes

4

Tôi nghi ngờ rằng điều này là do cách std::vectorđược thay đổi kích thước trong quá trình gọi hàm push_back (). Nếu bạn cố gắng sử dụng std::listhoặc std::vector::reserve()để dành đủ không gian cho các câu, bạn sẽ đạt được hiệu suất tốt hơn nhiều. Hoặc bạn có thể sử dụng kết hợp cả hai như dưới đây cho 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());
}

CHỈNH SỬA : Điều hiển nhiên khác mà tôi thấy là biến Python dummyđược gán mỗi lần nhưng không được sửa đổi. Vì vậy, nó không phải là một so sánh công bằng với C ++. Bạn nên thử sửa đổi mã Python của mình dummy = []để khởi tạo nó và sau đó thực hiện dummy += line.split(). Bạn có thể báo cáo thời gian chạy sau này không?

EDIT2 : Để làm cho nó công bằng hơn nữa, bạn có thể sửa đổi vòng lặp while trong mã C ++ thành:

    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++;
    };

Cảm ơn vì ý tưởng. Tôi đã triển khai nó và việc triển khai này thực sự chậm hơn so với split1 ban đầu, thật không may. Tôi cũng đã thử spline.reserve (16) trước vòng lặp, nhưng điều này không ảnh hưởng đến tốc độ của split1. Chỉ có ba mã thông báo trên mỗi dòng và vectơ bị xóa sau mỗi dòng, vì vậy tôi không mong đợi điều đó sẽ giúp được nhiều.
JJC

Tôi cũng đã thử chỉnh sửa của bạn. Vui lòng xem câu hỏi cập nhật. Hiệu suất hiện đã ngang bằng với split1.
JJC 21/02/12

Tôi đã thử EDIT2 của bạn. Hiệu suất kém hơn một chút: $ / usr / bin / time cat test_lines_double | ./split7 33,39 người dùng thực 0,01 0,49 sys C ++: Thấy 20000000 dòng trong 33 giây. Tốc độ vò: 606060
JJC

3

Tôi nghĩ mã sau đây tốt hơn, sử dụng một số tính năng C ++ 17 và 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));
}

Sự lựa chọn của thùng chứa:

  1. std::vector.

    Giả sử kích thước ban đầu của mảng nội bộ được phân bổ là 1 và kích thước cuối cùng là N, bạn sẽ phân bổ và phân bổ cho log2 (N) lần và bạn sẽ sao chép (2 ^ (log2 (N) + 1) - 1) = (2N - 1) lần. Như đã chỉ ra trong Có phải hiệu suất kém của vectơ std :: do không gọi realloc là số lôgarit không? , điều này có thể có hiệu suất kém khi kích thước của vector không thể đoán trước và có thể rất lớn. Nhưng, nếu bạn có thể ước tính kích thước của nó, điều này sẽ ít trở ngại hơn.

  2. std::list.

    Đối với mỗi push_back, thời gian mà nó tiêu thụ là một hằng số, nhưng có thể sẽ mất nhiều thời gian hơn so với std :: vector trên push_back riêng lẻ. Sử dụng nhóm bộ nhớ cho mỗi luồng và bộ phân bổ tùy chỉnh có thể giải quyết vấn đề này.

  3. std::forward_list.

    Tương tự như std :: list, nhưng chiếm ít bộ nhớ hơn cho mỗi phần tử. Yêu cầu lớp trình bao bọc hoạt động do thiếu API push_back.

  4. std::array.

    Nếu bạn có thể biết giới hạn của sự tăng trưởng, thì bạn có thể sử dụng std :: array. Tất nhiên, bạn không thể sử dụng nó trực tiếp vì nó không có API push_back. Nhưng bạn có thể xác định một trình bao bọc, và tôi nghĩ đó là cách nhanh nhất ở đây và có thể tiết kiệm một số bộ nhớ nếu ước tính của bạn khá chính xác.

  5. std::deque.

    Tùy chọn này cho phép bạn đánh đổi bộ nhớ để lấy hiệu suất. Sẽ không có (2 ^ (N + 1) - 1) lần bản sao của phần tử, chỉ phân bổ N lần và không có sự phân bổ. Ngoài ra, bạn sẽ có thời gian truy cập ngẫu nhiên liên tục và khả năng thêm các phần tử mới ở cả hai đầu.

Theo std :: deque-cppreference

Mặt khác, deques thường có chi phí bộ nhớ tối thiểu lớn; deque chỉ giữ một phần tử phải cấp phát toàn bộ mảng bên trong của nó (ví dụ: 8 lần kích thước đối tượng trên 64-bit libstdc ++; 16 lần kích thước đối tượng hoặc 4096 byte, tùy theo cái nào lớn hơn, trên libc ++ 64-bit)

hoặc bạn có thể sử dụng kết hợp sau:

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

    Điều này tương tự như std :: deque, sự khác biệt chỉ là vùng chứa này không hỗ trợ thêm phần tử ở phía trước. Nhưng nó vẫn nhanh hơn về hiệu suất, do thực tế là nó sẽ không sao chép mảng std :: cơ bản cho (2 ^ (N + 1) - 1) lần, nó sẽ chỉ sao chép mảng con trỏ cho (2 ^ (N - M + 1) - 1) lần và chỉ cấp phát mảng mới khi dòng điện đầy và không cần phân bổ bất cứ thứ gì. Nhân tiện, bạn có thể nhận được thời gian truy cập ngẫu nhiên không đổi.

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

    Giảm đáng kể áp lực của việc phân chia bộ nhớ. Nó sẽ chỉ cấp phát mảng mới khi hiện tại đã đầy và không cần sao chép bất cứ thứ gì. Bạn vẫn sẽ phải trả giá cho một con trỏ bổ sung được kết hợp với combo 1.

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

    Giống như 2, nhưng có cùng bộ nhớ với combo 1.


Nếu bạn sử dụng vectơ std :: với một số kích thước ban đầu hợp lý, như 128 hoặc 256, tổng số bản sao (giả sử hệ số tăng trưởng là 2), bạn sẽ tránh được bất kỳ sự sao chép nào đối với kích thước lên đến giới hạn đó. Sau đó, bạn có thể thu nhỏ phân bổ để phù hợp với số lượng phần tử mà bạn thực sự đã sử dụng để không quá khủng khiếp đối với các đầu vào nhỏ. Tuy nhiên, điều này không giúp ích nhiều cho tổng số bản sao của hộp đựng rất lớn N. Nó quá tệ vì std :: vector không thể sử dụng reallocđể có thể cho phép ánh xạ nhiều trang hơn vào cuối phân bổ hiện tại , vì vậy nó sẽ chậm hơn khoảng 2 lần.
Peter Cordes

stringview::remove_prefixrẻ như chỉ theo dõi vị trí hiện tại của bạn trong một chuỗi bình thường? std::basic_string::findcó đối số thứ 2 tùy chọn pos = 0để cho phép bạn bắt đầu tìm kiếm từ một điểm bù.
Peter Cordes

@ Peter Cordes Điều đó chính xác. Tôi đã kiểm tra libcxx impl
JiaHao Xu

Tôi cũng đã kiểm tra libstdc ++ impl , cũng giống như vậy.
JiaHao Xu

Phân tích của bạn về hiệu suất của vectơ bị tắt. Hãy xem xét một vectơ có dung lượng ban đầu là 1 khi bạn chèn lần đầu tiên và nó tăng gấp đôi mỗi khi cần dung lượng mới. Nếu bạn cần đặt 17 mục, lần phân bổ đầu tiên sẽ nhường chỗ cho 1, sau đó là 2, sau đó là 4, rồi 8, sau đó là 16, rồi cuối cùng là 32. Điều này có nghĩa là có tổng cộng 6 lần phân bổ ( log2(size - 1) + 2, sử dụng nhật ký số nguyên). Lần phân bổ đầu tiên di chuyển 0 chuỗi, lần thứ hai di chuyển 1, sau đó 2, rồi 4, rồi 8, rồi cuối cùng là 16, với tổng số 31 lần di chuyển ( 2^(log2(size - 1) + 1) - 1)). Đây là O (n), không phải O (2 ^ n). Điều này sẽ vượt trội hơn rất nhiều std::list.
David Stone,

2

Bạn đang giả định sai lầm rằng việc triển khai C ++ mà bạn đã chọn nhất thiết phải nhanh hơn Python. Xử lý chuỗi trong Python được tối ưu hóa cao. Xem câu hỏi này để biết thêm: Tại sao các hoạt động chuỗi std :: hoạt động kém?


4
Tôi không đưa ra bất kỳ tuyên bố nào về hiệu suất ngôn ngữ tổng thể, chỉ về mã cụ thể của tôi. Vì vậy, không có giả định nào ở đây. Cảm ơn bạn đã chỉ dẫn tốt cho câu hỏi khác. Tôi không chắc liệu bạn đang nói rằng việc triển khai cụ thể này trong C ++ là chưa tối ưu (câu đầu tiên của bạn) hay C ++ chỉ chậm hơn Python trong xử lý chuỗi (câu thứ hai của bạn). Ngoài ra, nếu bạn biết một cách nhanh chóng để làm những gì tôi đang cố gắng làm trong C ++, hãy chia sẻ nó vì lợi ích của mọi người. Cảm ơn. Chỉ cần nói rõ, tôi yêu python, nhưng tôi không phải là fanboy mù quáng, đó là lý do tại sao tôi đang cố gắng học cách nhanh nhất để làm điều này.
JJC

1
@JJC: Cho rằng việc triển khai Python nhanh hơn, tôi muốn nói rằng của bạn là chưa tối ưu. Hãy nhớ rằng việc triển khai ngôn ngữ có thể làm giảm bớt khó khăn cho bạn, nhưng cuối cùng thì độ phức tạp của thuật toán và tối ưu hóa thủ công sẽ chiến thắng. Trong trường hợp này, Python có ưu thế hơn đối với trường hợp sử dụng này theo mặc định.
Matt Joiner

2

Nếu bạn sử dụng triển khai split1 và thay đổi chữ ký để phù hợp hơn với chữ ký của split2, bằng cách thay đổi điều này:

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

đến điều này:

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

Bạn nhận được sự khác biệt ấn tượng hơn giữa split1 và split2 và so sánh công bằng hơn:

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

Cảm ơn nm! Thật không may, điều này dường như chạy ở cùng tốc độ với triển khai ban đầu (chia 1) trên tập dữ liệu và máy của tôi: $ / usr / bin / time cat test_lines_double | ./split8 21,89 người dùng thực 0,01 0,47 sys C ++: Thấy 20000000 dòng trong 22 giây. Tốc độ vò: 909090
JJC

Trên máy của tôi: split1 - 54 giây, split.py - 35 giây, split5 - 16 giây. Tôi không có ý kiến.
n. 'đại từ' m.

Rất tiếc, dữ liệu của bạn có khớp với định dạng tôi đã lưu ý ở trên không? Tôi giả sử bạn đã chạy mỗi lần một vài lần để loại bỏ các hiệu ứng thoáng qua như dân số bộ nhớ cache đĩa ban đầu?
JJC

0

Tôi nghi ngờ rằng điều này có liên quan đến bộ đệm trên sys.stdin trong Python, nhưng không có bộ đệm trong triển khai C ++.

Xem bài đăng này để biết chi tiết về cách thay đổi kích thước bộ đệm, sau đó thử so sánh lại: Đặt kích thước bộ đệm nhỏ hơn cho sys.stdin?


1
Hmmm ... Tôi không theo dõi. Chỉ đọc các dòng (không có phần tách) trong C ++ nhanh hơn Python (sau khi bao gồm dòng cin.sync_with_stdio (false);). Đó là vấn đề tôi gặp phải ngày hôm qua, đã tham khảo ở trên.
JJC
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.