C ++ 'da bir dizeyi bölmek neden Python'dan daha yavaş?


94

Biraz hız kazanmak ve paslı C ++ becerilerimi geliştirmek için bazı kodları Python'dan C ++ 'ya dönüştürmeye çalışıyorum. Stdin'den satırları okurken bir naif uygulama (bkz ++ çok daha hızlı C'nin Python iken Dün şok oldu bu ). Bugün nihayet C ++ 'da bir dizeyi sınırlayıcıları birleştirerek (python split () ile benzer anlambilim) nasıl böleceğimi buldum ve şimdi deja vu yaşıyorum! C ++ kodumun işi yapması çok daha uzun sürüyor (dünkü derste olduğu gibi bir büyüklük sırası olmasa da).

Python Kodu:

#!/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 ++ Kodu:

#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

İki farklı split uygulaması denediğime dikkat edin. Bir (split1) jeton aramak için dize yöntemleri kullanır ve birden belirteçleri yanı sıra sap sayısız belirteçleri (o geliyor birleştirme yapabiliyor burada ). İkincisi (split2), dizeyi bir akış olarak okumak için getline kullanır, sınırlayıcıları birleştirmez ve yalnızca tek bir sınırlayıcı karakteri destekler (bu, birkaç StackOverflow kullanıcısı tarafından dize bölme sorularının yanıtlarında yayınlanmıştır).

Bunu çeşitli siparişlerde defalarca çalıştırdım. Test makinem bir Macbook Pro (2011, 8GB, Dört Çekirdekli), o kadar önemli değil. Her biri şuna benzer görünen boşlukla ayrılmış üç sütuna sahip 20M satırlık bir metin dosyasıyla test ediyorum: "foo.bar 127.0.0.1 home.foo.bar"

Sonuçlar:

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

Neyi yanlış yapıyorum? C ++ 'da dize bölme yapmanın harici kitaplıklara dayanmayan (yani yükseltme yok), sınırlayıcı dizilerini birleştirmeyi destekleyen (python bölme gibi), iş parçacığı güvenli (yani strtok yok) ve performansı en az olan daha iyi bir yolu var mı python ile eşit mi?

Düzenleme 1 / Kısmi Çözüm ?:

Python'un kukla listeyi sıfırlamasını ve C ++ 'nın yaptığı gibi her seferinde buna eklemesini sağlayarak daha adil bir karşılaştırma yapmayı denedim. Bu hala C ++ kodunun yaptığı şey değil, ancak biraz daha yakın. Temel olarak, döngü şimdi:

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

Python'un performansı artık split1 C ++ uygulamasıyla hemen hemen aynı.

/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 dizge işleme için bu kadar optimize edilmiş olsa bile (Matt Joiner'ın önerdiği gibi) bu C ++ uygulamalarının daha hızlı olmayacağına hala şaşırıyorum. C ++ kullanarak bunu daha optimal bir şekilde nasıl yapacağınıza dair bir fikri olan varsa, lütfen kodunuzu paylaşın. (Sanırım bir sonraki adımım bunu saf C'de uygulamaya çalışmak olacak, ancak programcı verimliliğini C'deki genel projemi yeniden uygulamak için ödün vermeyeceğim, bu yüzden bu sadece dizi bölme hızı için bir deney olacak.)

Yardımlarınız için hepinize teşekkürler.

Son Düzenleme / Çözüm:

Lütfen Alf'ın kabul ettiği cevaba bakın. Python dizelerle kesinlikle referansla ilgilendiğinden ve STL dizeleri genellikle kopyalandığından, vanilya python uygulamalarıyla performans daha iyidir. Karşılaştırma için, verilerimi Alf'ın kodu aracılığıyla derledim ve çalıştırdım ve işte diğer tüm çalıştırmalarla aynı makinedeki performans, temelde saf python uygulamasıyla aynıdır (ancak listeyi sıfırlayan / ekleyen python uygulamasından daha hızlıdır. yukarıdaki düzenlemede gösterilmiştir):

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

Geriye kalan tek küçük sıkıntım, bu durumda C ++ 'nın gerçekleştirilmesi için gereken kod miktarı ile ilgilidir.

Bu sayıdan ve dünün standart satır okuma sorunundan (yukarıda bağlantılı) alınan derslerden biri, dillerin göreceli "varsayılan" performansı hakkında naif varsayımlar yapmak yerine her zaman kıyaslama yapılması gerektiğidir. Eğitimi takdir ediyorum.

Önerileriniz için hepinize tekrar teşekkürler!


2
C ++ programını nasıl derlediniz? Optimizasyonlarınızı etkinleştirdiniz mi?
interjay

2
@interjay: Kaynağındaki son yorumda: g++ -Wall -O3 -o split1 split_1.cpp@JJC: Karşılaştırma değerlendirmeniz gerçekten dummyve splinesırasıyla kullandığınızda nasıl ücret alıyor , belki de Python çağrıyı, line.split()yan etkisi olmadığı için kaldırıyor ?
Eric

2
Bölmeyi kaldırırsanız ve stdin'den yalnızca okuma satırları bırakırsanız ne gibi sonuçlar elde edersiniz?
interjay

2
Python C ile yazılmıştır. Bu, C dilinde bunu yapmanın etkili bir yolu olduğu anlamına gelir. Belki bir dizgeyi bölmenin STL kullanmaktan daha iyi bir yolu vardır?
ixe013

Yanıtlar:


58

Tahmin olarak, Python dizeleri referans sayılan değişmez dizelerdir, böylece Python kodunda hiçbir dizge kopyalanmazken, C ++ std::stringdeğiştirilebilir bir değer türüdür ve en küçük fırsatta kopyalanır.

Hedef hızlı bölme ise, o zaman sabit zamanlı alt dize işlemleri kullanılır; bu , Python'da (ve Java ve C #…) olduğu gibi yalnızca orijinal dizenin bölümlerine atıfta bulunmak anlamına gelir .

C ++ std::stringsınıfının bir kurtarma özelliği vardır, ancak standarttır , böylece verimliliğin ana husus olmadığı yerlerde dizeleri güvenli ve taşınabilir bir şekilde geçirmek için kullanılabilir. Ama yeterince sohbet. Kod - ve benim makinemde bu elbette Python'dan daha hızlı, çünkü Python'un dize işleme C ++ 'nın bir alt kümesi olan C de uygulanıyor (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

Feragatname: Umarım herhangi bir hata yoktur. İşlevselliği test etmedim, sadece hızı kontrol ettim. Ancak bir veya iki hata olsa bile, bunu düzeltmenin hızı önemli ölçüde etkilemeyeceğini düşünüyorum.


2
Evet, Python dizeleri referans sayılan nesnelerdir, bu nedenle Python çok daha az kopyalama yapar. Kodunuz gibi çiftler (işaretçi, boyut) olmasa da, başlık altında hala boş sonlu C dizeleri içerirler.
Fred Foo

13
Başka bir deyişle - metin işleme gibi daha yüksek seviyeli işler için, onlarca yıl boyunca onlarca geliştirici tarafından kümülatif olarak verimli bir şekilde yapılması için çaba sarf edilen daha yüksek seviyeli bir dile sadık kalın - ya da sadece tüm bu geliştiriciler kadar çalışmaya hazırlanın daha düşük seviyede karşılaştırılabilir bir şeye sahip olduğu için.
jsbueno

2
@JJC: için, StringRefalt dizeyi std::stringçok kolay bir şekilde kopyalayabilirsiniz string( sr.begin(), sr.end() ).
Şerefe ve hth. - Alf

3
CPython dizelerinin daha az kopyalanmasını dilerdim. Evet, referans olarak sayılırlar ve değişmezler ancak str.split () , PyString_FromStringAndSize()bu çağrıları kullanan her öğe için yeni dizeler ayırırPyObject_MALLOC() . Dolayısıyla, dizelerin Python'da değişmez olmasından yararlanan paylaşılan bir temsil ile optimizasyon yoktur.
jfs

3
Bakımcılar: Lütfen algılanan hataları düzeltmeye çalışarak hata yapmayın (özellikle cplusplus.com'a atıfta bulunarak ). TIA.
Şerefe ve hth. - Alf

9

Daha iyi çözümler sunmuyorum (en azından performans açısından), ancak ilginç olabilecek bazı ek veriler.

Kullanarak strtok_r(evresel varyantı 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);
}

Ek olarak parametreler ve fgetsgiriş için karakter dizilerini kullanma :

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

Ve bazı durumlarda, giriş dizesini yok etmenin kabul edilebilir olduğu durumlarda:

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

Bunların zamanlamaları aşağıdaki gibidir (sorudaki diğer varyantlar için sonuçlarım ve kabul edilen cevap dahil):

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

Gördüğümüz gibi, kabul edilen cevabın çözümü hala en hızlısı.

Daha fazla test yapmak isteyenler için, sorudan gelen tüm programları, kabul edilen cevabı, bu cevabı ve ayrıca bir Makefile ve test verilerini oluşturmak için bir komut dosyası: https: // github ile bir Github deposu da koyuyorum . com / tobbez / string-splitting .


2
Verileri "kullanarak" (kelime ve karakter sayısını sayarak) testi biraz daha gerçekçi hale getiren bir çekme isteği ( github.com/tobbez/string-splitting/pull/2 ) yaptım . Bu değişiklikle birlikte, tüm C / C ++ sürümleri Python sürümlerini (benim eklediğim Boost'un belirteçine dayalı olanı bekleyin) ve "dizi görünümü" tabanlı yöntemlerin (split6'nınki gibi) gerçek değerini geçti.
Dave Johansen

Sen kullanmalısınız memcpydeğil, strcpy, durumunda derleyici o optimizasyonu fark başarısız olur. strcpytipik olarak, kısa dizeler için hızlı ile uzun dizeler için tam SIMD'ye yükselme arasında bir denge sağlayan daha yavaş bir başlangıç ​​stratejisi kullanır. memcpyboyutu hemen bilir ve örtük uzunlukta bir dizinin sonunu kontrol etmek için herhangi bir SIMD numarası kullanmak zorunda değildir. (Modern x86 için önemli değil). Yapıcıyla std::stringnesneler oluşturmak (char*, len), bunu çıkarabilirseniz daha hızlı olabilir saveptr-token. Açıkça char*belirteçleri saklamak en hızlısı olacaktır : P
Peter Cordes

4

Bunun std::vectorbir push_back () işlev çağrısı işlemi sırasında yeniden boyutlandırılmasından kaynaklandığından şüpheleniyorum . Cümleler için yeterince yer ayırmaya std::listveya kullanmaya çalışırsanız std::vector::reserve(), çok daha iyi bir performans elde etmelisiniz. Veya split1 () için aşağıdaki gibi ikisinin bir kombinasyonunu kullanabilirsiniz:

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

DÜZENLEME : Anlıyorum diğer bariz şey Python değişken olmasıdır dummyalır atanan her seferinde ama değiştirilmemiş. Yani C ++ ile adil bir karşılaştırma değil. Python kodunuzu, dummy = []onu başlatmak için değiştirmeyi denemeli ve sonra yapmalısınız dummy += line.split(). Bundan sonra çalışma zamanını bildirebilir misiniz?

DÜZENLEME2 : Daha adil hale getirmek için C ++ kodundaki while döngüsünü şu şekilde değiştirebilirsin:

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

Fikir için teşekkürler. Bunu uyguladım ve bu uygulama maalesef orijinal split1'den daha yavaş. Döngüden önce spline.reserve (16) 'yı da denedim, ancak bunun split1'in hızına bir etkisi olmadı. Satır başına yalnızca üç jeton vardır ve vektör her satırdan sonra silinir, bu yüzden bunun çok yardımcı olmasını beklemiyordum.
JJC

Düzenlemenizi de denedim. Lütfen güncellenmiş soruya bakın. Performans artık split1 ile aynı seviyede.
JJC

EDIT2'nizi denedim. Performans biraz daha kötüydü: $ / usr / bin / time cat test_lines_double | ./split7 33.39 gerçek 0.01 kullanıcı 0.49 sys C ++: 33 saniyede 20000000 satır kesildi. Crunch speed: 606060
JJC

3

Bazı C ++ 17 ve C ++ 14 özelliklerini kullanarak aşağıdaki kodun daha iyi olduğunu düşünüyorum:

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

Konteyner seçimi:

  1. std::vector.

    Tahsis edilen dahili dizinin başlangıç ​​boyutunun 1 olduğunu ve nihai boyutun N olduğunu varsayarsak, log2 (N) kez ayıracak ve serbest bırakacaksınız ve (2 ^ (log2 (N) + 1) - 1) = (2N - 1) kez. 'De belirtildiği gibi std :: vector' un düşük performansı, logaritmik bir sayıda realloc çağrılmaması nedeniyle mi? , vektör boyutu tahmin edilemediğinde ve çok büyük olduğunda bu zayıf bir performansa sahip olabilir. Ancak, boyutunu tahmin edebilirseniz, bu daha az sorun olacaktır.

  2. std::list.

    Her geri itme için, tükettiği zaman sabittir, ancak tek tek geri itmede muhtemelen std :: vektörden daha fazla zaman alacaktır. İş parçacığı başına bellek havuzu ve özel ayırıcı kullanmak bu sorunu kolaylaştırabilir.

  3. std::forward_list.

    Std :: list ile aynıdır, ancak eleman başına daha az bellek kaplar. API push_back eksikliği nedeniyle çalışması için bir sarmalayıcı sınıfını gerekli kılın.

  4. std::array.

    Büyüme sınırını biliyorsan, std :: array kullanabilirsiniz. Çünkü API push_back'e sahip olmadığı için onu doğrudan kullanamazsınız. Ancak bir sarmalayıcı tanımlayabilirsiniz ve bence buradaki en hızlı yol budur ve tahminleriniz oldukça doğruysa hafızadan tasarruf edebilirsiniz.

  5. std::deque.

    Bu seçenek, performans için bellek ticareti yapmanızı sağlar. Elemanın (2 ^ (N + 1) - 1) kere kopyası olmayacak, sadece N kere tahsis olacak ve serbest bırakma olmayacak. Ayrıca, sabit rastgele erişim süresine ve her iki uca da yeni öğeler ekleyebileceksiniz.

Göre std :: deque-cppreference

Öte yandan, dekorlar tipik olarak büyük minimum bellek maliyetine sahiptir; Sadece bir öğeyi tutan bir deque, tüm dahili dizisini tahsis etmek zorundadır (örneğin, 64-bit libstdc ++ 'da nesne boyutunun 8 katı; 64-bit libc ++' da nesne boyutunun 16 katı veya 4096 bayt)

veya şunların kombinasyonunu kullanabilirsiniz:

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

    Bu std :: deque'e benzer, aradaki fark sadece bu konteynerin öne eleman eklemeyi desteklememesidir. Ancak, temeldeki std :: array'i (2 ^ (N + 1) - 1) kez kopyalamaması nedeniyle performansta hala daha hızlıdır, yalnızca (2 ^ için işaretçi dizisini kopyalar) (N - M + 1) - 1) kez ve yeni diziyi yalnızca akım dolu olduğunda ve hiçbir şeyin serbest bırakılmasına gerek olmadığında tahsis etmek. Bu arada, sürekli rastgele erişim süresi elde edebilirsiniz.

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

    Hafıza çerçeveleme baskısını büyük ölçüde hafifletir. Yeni diziyi yalnızca akım dolduğunda tahsis eder ve herhangi bir şeyi kopyalamasına gerek yoktur. Kombo 1'e göre düzenlenmiş ek bir işaretçinin fiyatını yine de ödemeniz gerekecek.

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

    2 ile aynı, ancak combo 1 ile aynı belleğe mal oluyor.


Eğer std :: vektörü, 128 veya 256 gibi makul bir başlangıç ​​boyutuyla kullanırsanız (büyüme faktörü 2 olduğu varsayılarak), bu sınıra kadar olan boyutlar için herhangi bir kopyalamadan kaçınmış olursunuz. Daha sonra, gerçekte kullandığınız öğelerin sayısına uyacak şekilde atamayı küçültebilirsiniz, böylece küçük girdiler için korkunç değildir. NYine de bu, çok büyük kasa için toplam kopya sayısına pek yardımcı olmuyor . Çok kötü std :: vector, reallocpotansiyel olarak mevcut ayırmanın sonunda daha fazla sayfanın eşlenmesini sağlamak için kullanılamaz , bu yüzden yaklaşık 2 kat daha yavaştır.
Peter Cordes

stringview::remove_prefixsadece normal dizede geçerli konumunuza durum takibi ucuza olarak? bir ofsetten aramaya başlamanıza izin std::basic_string::findveren isteğe bağlı bir 2. argüman vardır pos = 0.
Peter Cordes

@ Peter Cordes Bu doğru. Libcxx impl'yi
JiaHao Xu

Ayrıca aynı olan libstdc ++ impl'yi de kontrol ettim .
JiaHao Xu

Vektörün performans analiziniz kapalı. İlk eklediğinizde başlangıç ​​kapasitesi 1 olan ve her yeni kapasiteye ihtiyaç duyduğunda iki katına çıkan bir vektör düşünün. 17 öğe koymanız gerekirse, ilk ayırma 1, sonra 2, sonra 4, sonra 8, sonra 16 ve son olarak 32 için yer açar. Bu, toplamda 6 tahsis olduğu anlamına gelir ( log2(size - 1) + 2tamsayı günlüğü kullanılarak). İlk ayırma 0 dizeyi taşıdı, ikincisi 1'i, ardından 2'yi, sonra 4'ü, ardından 8'i, ardından son olarak 16'yı toplam 31 hareket için ( 2^(log2(size - 1) + 1) - 1)) taşıdı . Bu O (n), O (2 ^ n) değil. Bu çok daha iyi performans gösterecek std::list.
David Stone

2

Seçtiğiniz C ++ uygulamasının Python'dan daha hızlı olması gerektiğine dair yanlış bir varsayımda bulunuyorsunuz. Python'da dize işleme oldukça optimize edilmiştir. Daha fazlası için şu soruya bakın: std :: string işlemleri neden kötü çalışıyor?


4
Genel dil performansı hakkında herhangi bir iddiada bulunmuyorum, sadece kendi kodum hakkında. Yani, burada varsayım yok. Diğer soruya iyi bir işaretçi için teşekkürler. C ++ 'daki bu özel uygulamanın yetersiz (ilk cümleniz) olduğunu mu yoksa C ++' nın dize işlemede Python'dan (ikinci cümleniz) daha yavaş olduğunu mu söylediğinizden emin değilim. Ayrıca, C ++ ile yapmaya çalıştığım şeyi hızlı bir şekilde yapmanın bir yolunu biliyorsanız, lütfen bunu herkesin yararı için paylaşın. Teşekkürler. Sadece açıklığa kavuşturmak için, python'u seviyorum, ama kör bir hayran değilim, bu yüzden bunu yapmanın en hızlı yolunu öğrenmeye çalışıyorum.
JJC

1
@JJC: Python'un uygulamasının daha hızlı olduğu göz önüne alındığında, sizinki yetersiz diyebilirim. Unutmayın ki dil uygulamaları sizin için köşeleri kısabilir ancak sonuçta algoritmik karmaşıklık ve el optimizasyonları kazanır. Bu durumda, Python varsayılan olarak bu kullanım durumu için üstünlüğe sahiptir.
Matt Joiner

2

Split1 uygulamasını alır ve imzayı split2'ninkine daha yakın olacak şekilde değiştirirseniz, bunu değiştirerek:

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

buna:

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

Bölme1 ve bölme2 arasında daha dramatik bir fark ve daha adil bir karşılaştırma elde edersiniz:

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

Teşekkürler nm! Ne yazık ki, bu, veri setim ve makinemdeki orijinal (bölünmüş 1) uygulama ile yaklaşık aynı hızda çalışıyor gibi görünüyor: $ / usr / bin / time cat test_lines_double | ./split8 21.89 gerçek 0.01 kullanıcı 0.47 sys C ++: 22 saniyede 20000000 satır kesildi. Crunch speed: 909090
JJC

Makinemde: split1 - 54s, split.py - 35s, split5 - 16s. Hiç bir fikrim yok.
n. zamirler 'm.

Hmm, verileriniz yukarıda belirttiğim formatla eşleşiyor mu? İlk disk önbelleği popülasyonu gibi geçici etkileri ortadan kaldırmak için her birini birkaç kez çalıştırdığınızı varsayıyorum.
JJC

0

Bunun Python'da sys.stdin'de arabelleğe alma ile ilgili olduğundan şüpheleniyorum, ancak C ++ uygulamasında arabelleğe alma yok.

Arabellek boyutunu nasıl değiştireceğinizle ilgili ayrıntılar için bu gönderiye bakın , ardından karşılaştırmayı tekrar deneyin: sys.stdin için daha küçük arabellek boyutu ayarlamak?


1
Hmmm ... Ben takip etmiyorum. Sadece satırları okumak (bölme olmadan) C ++ 'da Python'dan daha hızlıdır (cin.sync_with_stdio (false); satırını dahil ettikten sonra). Yukarıda değindiğim, dün yaşadığım sorun buydu.
JJC
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.