Perché la divisione di una stringa è più lenta in C ++ rispetto a Python?


93

Sto cercando di convertire del codice da Python a C ++ nel tentativo di guadagnare un po 'di velocità e affinare le mie arrugginite abilità C ++. Ieri sono rimasto scioccato quando un'implementazione ingenua della lettura di righe da stdin è stata molto più veloce in Python rispetto a C ++ (vedi questo ). Oggi, ho finalmente capito come dividere una stringa in C ++ con l'unione di delimitatori (semantica simile a split ()) di python, e ora sto sperimentando il deja vu! Il mio codice C ++ impiega molto più tempo per svolgere il lavoro (anche se non un ordine di grandezza in più, come nel caso della lezione di ieri).

Codice 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('')

Codice 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

Nota che ho provato due diverse implementazioni divise. Uno (split1) utilizza metodi di stringa per cercare token ed è in grado di unire più token e gestire numerosi token (proviene da qui ). Il secondo (split2) utilizza getline per leggere la stringa come un flusso, non unisce i delimitatori e supporta solo un singolo carattere delimitatore (quello è stato pubblicato da diversi utenti StackOverflow nelle risposte alle domande sulla divisione delle stringhe).

L'ho eseguito più volte in vari ordini. La mia macchina di prova è un Macbook Pro (2011, 8 GB, Quad Core), non che importi molto. Sto testando con un file di testo di 20 milioni di righe con tre colonne separate da spazi, ognuna simile a questa: "foo.bar 127.0.0.1 home.foo.bar"

Risultati:

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

Che cosa sto facendo di sbagliato? Esiste un modo migliore per eseguire la suddivisione delle stringhe in C ++ che non si basi su librerie esterne (ovvero nessun boost), supporti l'unione di sequenze di delimitatori (come la divisione di Python), sia thread-safe (quindi niente strtok) e le cui prestazioni siano almeno alla pari con il pitone?

Modifica 1 / Soluzione parziale ?:

Ho provato a renderlo un confronto più equo facendo in modo che Python resettasse l'elenco fittizio e lo aggiungesse ogni volta, come fa C ++. Questo non è ancora esattamente ciò che sta facendo il codice C ++, ma è un po 'più vicino. Fondamentalmente, il ciclo è ora:

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

Le prestazioni di python sono ora più o meno le stesse dell'implementazione C ++ di 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

Sono ancora sorpreso dal fatto che, anche se Python è così ottimizzato per l'elaborazione delle stringhe (come suggerito da Matt Joiner), queste implementazioni C ++ non sarebbero più veloci. Se qualcuno ha idee su come farlo in modo ottimale utilizzando C ++, condividi il tuo codice. (Penso che il mio prossimo passo sarà cercare di implementarlo in C puro, anche se non ho intenzione di compromettere la produttività del programmatore per reimplementare il mio progetto complessivo in C, quindi questo sarà solo un esperimento per la velocità di divisione delle stringhe.)

Grazie a tutti per il vostro aiuto.

Modifica / soluzione finale:

Si prega di vedere la risposta accettata di Alf. Poiché python si occupa delle stringhe esclusivamente per riferimento e le stringhe STL vengono spesso copiate, le prestazioni sono migliori con le implementazioni vanilla python. Per confronto, ho compilato ed eseguito i miei dati tramite il codice di Alf, ed ecco le prestazioni sulla stessa macchina di tutte le altre esecuzioni, essenzialmente identiche all'implementazione ingenua di python (sebbene più veloce dell'implementazione di python che reimposta / aggiunge l'elenco, come mostrato nella modifica sopra):

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

La mia unica piccola lamentela rimasta riguarda la quantità di codice necessaria per far funzionare C ++ in questo caso.

Una delle lezioni qui da questo problema e dal problema di lettura della riga standard di ieri (collegato sopra) è che si dovrebbe sempre fare il benchmark invece di fare supposizioni ingenue sulle prestazioni "predefinite" relative delle lingue. Apprezzo l'educazione.

Grazie ancora a tutti per i vostri suggerimenti!


2
Come hai compilato il programma C ++? Hai attivato le ottimizzazioni?
Interjay

2
@interjay: è nell'ultimo commento nella sua fonte: g++ -Wall -O3 -o split1 split_1.cpp@JJC: come se la cava il tuo benchmark quando usi effettivamente dummye splinerispettivamente, forse Python rimuove la chiamata a line.split()perché non ha effetti collaterali?
Eric,

2
Quali risultati ottieni se rimuovi la divisione e lasci solo le righe di lettura dallo stdin?
Interjay

2
Python è scritto in C. Significa che c'è un modo efficiente per farlo, in C. Forse c'è un modo migliore per dividere una stringa che usare STL?
ixe013

Risposte:


57

Come ipotesi, le stringhe Python sono stringhe immutabili con conteggio di riferimento, in modo che nessuna stringa venga copiata nel codice Python, mentre C ++ std::stringè un tipo di valore mutabile e viene copiato alla minima opportunità.

Se l'obiettivo è la suddivisione rapida, si utilizzerebbero operazioni di sottostringa a tempo costante, il che significa fare riferimento solo a parti della stringa originale, come in Python (e Java e C #…).

La std::stringclasse C ++ ha una caratteristica di riscatto, però: è standard , in modo che possa essere utilizzata per passare stringhe in modo sicuro e portabile in giro dove l'efficienza non è una considerazione principale. Ma basta chattare. Codice - e sulla mia macchina questo è ovviamente più veloce di Python, poiché la gestione delle stringhe di Python è implementata in C che è un sottoinsieme di 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

Disclaimer: spero che non ci siano bug. Non ho testato la funzionalità, ma ho solo controllato la velocità. Ma penso che, anche se ci sono uno o due bug, la correzione non influirà in modo significativo sulla velocità.


2
Sì, le stringhe Python sono oggetti conteggiati di riferimento, quindi Python copia molto meno. Contengono ancora stringhe C con terminazione null sotto il cofano, tuttavia, non coppie (puntatore, dimensione) come il tuo codice.
Fred Foo

13
In altre parole, per il lavoro di livello superiore, come la manipolazione del testo, attenersi a un linguaggio di livello superiore, dove lo sforzo per farlo in modo efficiente è stato messo cumulativamente da decine di sviluppatori nel corso di decine di anni o semplicemente prepararsi a lavorare tanto quanto tutti quegli sviluppatori per avere qualcosa di paragonabile a un livello inferiore.
jsbueno

2
@ JJC: per il StringRef, puoi copiare la sottostringa in un std::stringmolto facilmente, solo string( sr.begin(), sr.end() ).
Saluti e salute. - Alf

3
Vorrei che le stringhe CPython venissero copiate di meno. Sì, sono conteggiati nei riferimenti e immutabili, ma str.split () alloca nuove stringhe per ogni elemento utilizzando PyString_FromStringAndSize()quelle chiamate PyObject_MALLOC(). Quindi non c'è ottimizzazione con una rappresentazione condivisa che sfrutti il ​​fatto che le stringhe sono immutabili in Python.
jfs

3
Manutentori: per favore non introdurre bug cercando di correggere bug percepiti (specialmente non con riferimento a cplusplus.com ). TIA.
Saluti e salute. - Alf

9

Non sto fornendo soluzioni migliori (almeno dal punto di vista delle prestazioni), ma alcuni dati aggiuntivi che potrebbero essere interessanti.

Utilizzando strtok_r(variante rientrante di 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);
}

Inoltre, utilizzando stringhe di caratteri per i parametri e fgetsper l'input:

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

E, in alcuni casi, dove è accettabile distruggere la stringa di input:

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

I tempi per questi sono i seguenti (compresi i miei risultati per le altre varianti della domanda e la risposta accettata):

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

Come possiamo vedere, la soluzione dalla risposta accettata è ancora più veloce.

Per chiunque volesse fare ulteriori test, ho anche creato un repository Github con tutti i programmi dalla domanda, la risposta accettata, questa risposta e in aggiunta un Makefile e uno script per generare dati di test: https: // github. com / tobbez / string-splitting .


2
Ho fatto una richiesta pull ( github.com/tobbez/string-splitting/pull/2 ) che rende il test un po 'più realistico "utilizzando" i dati (contando il numero di parole e caratteri). Con questa modifica, tutte le versioni C / C ++ battono le versioni di Python (aspettatevi quella basata sul tokenizer di Boost che ho aggiunto) e il valore reale dei metodi basati sulla "visualizzazione delle stringhe" (come quello di split6) risplende.
Dave Johansen

Dovresti usare memcpy, no strcpy, nel caso in cui il compilatore non riesca a notare quell'ottimizzazione. strcpytipicamente utilizza una strategia di avvio più lenta che trova un equilibrio tra veloce per stringhe brevi e rampa fino a SIMD completo per stringhe lunghe. memcpyconosce subito la dimensione e non deve usare alcun trucco SIMD per verificare la fine di una stringa di lunghezza implicita. (Non è un grosso problema su x86 moderno). Anche la creazione di std::stringoggetti con il (char*, len)costruttore potrebbe essere più veloce, se riesci a ottenerla saveptr-token. Ovviamente sarebbe più veloce memorizzare solo i char*gettoni: P
Peter Cordes

4

Sospetto che ciò sia dovuto al modo in cui std::vectorviene ridimensionato durante il processo di una chiamata alla funzione push_back (). Se provi a usare std::listo std::vector::reserve()a riservare spazio sufficiente per le frasi, dovresti ottenere prestazioni molto migliori. Oppure puoi usare una combinazione di entrambi come sotto per 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());
}

EDIT : L'altra cosa ovvia che vedo è che la variabile Python dummyviene assegnata ogni volta ma non modificata. Quindi non è un confronto equo con C ++. Dovresti provare a modificare il tuo codice Python dummy = []per inizializzarlo e poi farlo dummy += line.split(). Puoi segnalare il tempo di esecuzione dopo questo?

EDIT2 : per renderlo ancora più equo, puoi modificare il ciclo while nel codice C ++ in modo che sia:

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

Grazie per l'idea. L'ho implementato e questa implementazione è in realtà più lenta dell'originale split1, sfortunatamente. Ho anche provato spline.reserve (16) prima del loop, ma questo non ha avuto alcun impatto sulla velocità del mio split1. Ci sono solo tre gettoni per riga e il vettore viene cancellato dopo ogni riga, quindi non mi aspettavo che aiutasse molto.
JJC

Ho provato anche la tua modifica. Si prega di consultare la domanda aggiornata. Le prestazioni sono ora alla pari con split1.
JJC

Ho provato il tuo EDIT2. Le prestazioni sono state un po 'peggiori: $ / usr / bin / time cat test_lines_double | ./split7 33.39 real 0.01 user 0.49 sys C ++: ha visto 20000000 linee in 33 secondi. Velocità scricchiolio: 606060
JJC

3

Penso che il codice seguente sia migliore, utilizzando alcune funzionalità di C ++ 17 e 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));
}

La scelta del contenitore:

  1. std::vector.

    Supponendo che la dimensione iniziale dell'array interno allocato sia 1 e la dimensione finale sia N, allocherai e deallocherai per log2 (N) volte e copierai (2 ^ (log2 (N) + 1) - 1) = (2N - 1) volte. Come sottolineato in Le scarse prestazioni di std :: vector sono dovute alla mancata chiamata di realloc un numero logaritmico di volte? , questo può avere prestazioni scadenti quando la dimensione del vettore è imprevedibile e potrebbe essere molto grande. Ma se puoi stimarne le dimensioni, questo sarà un problema minore.

  2. std::list.

    Per ogni push_back, il tempo impiegato è una costante, ma probabilmente ci vorrà più tempo di std :: vector su singoli push_back. L'utilizzo di un pool di memoria per thread e di un allocatore personalizzato può alleviare questo problema.

  3. std::forward_list.

    Uguale a std :: list, ma occupa meno memoria per elemento. Richiede che una classe wrapper funzioni a causa della mancanza di API push_back.

  4. std::array.

    Se puoi conoscere il limite di crescita, puoi usare std :: array. Ovviamente non puoi usarlo direttamente, poiché non ha l'API push_back. Ma puoi definire un wrapper, e penso che sia il modo più veloce qui e puoi risparmiare un po 'di memoria se la tua stima è abbastanza accurata.

  5. std::deque.

    Questa opzione ti consente di scambiare la memoria con le prestazioni. Non ci saranno (2 ^ (N + 1) - 1) volte la copia dell'elemento, solo N volte l'allocazione e nessuna deallocazione. Inoltre, avrai un tempo di accesso casuale costante e la possibilità di aggiungere nuovi elementi ad entrambe le estremità.

Secondo std :: deque-cppreference

D'altra parte, i deques hanno tipicamente un grande costo minimo di memoria; un deque che contiene un solo elemento deve allocare il suo array interno completo (ad esempio 8 volte la dimensione dell'oggetto su libstdc ++ a 64 bit; 16 volte la dimensione dell'oggetto o 4096 byte, a seconda di quale sia maggiore, su libc ++ a 64 bit)

oppure puoi usare una combinazione di questi:

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

    Questo è simile a std :: deque, la differenza è che questo contenitore non supporta l'aggiunta di elementi in primo piano. Ma è ancora più veloce nelle prestazioni, perché non copia lo std :: array sottostante per (2 ^ (N + 1) - 1) volte, ma copia semplicemente l'array di puntatori per (2 ^ (N - M + 1) - 1) volte e allocare un nuovo array solo quando la corrente è piena e non è necessario deallocare nulla. A proposito, puoi ottenere un tempo di accesso casuale costante.

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

    Allevia notevolmente la pressione del framento della memoria. Assegnerà un nuovo array solo quando la corrente è piena e non è necessario copiare nulla. Dovrai comunque pagare il prezzo per un puntatore aggiuntivo rispetto alla combo 1.

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

    Uguale a 2, ma costa la stessa memoria della combo 1.


Se usi std :: vector con una dimensione iniziale ragionevole, come 128 o 256, il totale delle copie (assumendo un fattore di crescita di 2), eviti qualsiasi copia per dimensioni fino a quel limite. È quindi possibile ridurre l'allocazione per adattarla al numero di elementi effettivamente utilizzati, quindi non è terribile per piccoli input. NTuttavia, questo non aiuta molto con il numero totale di copie per il caso molto grande . È un peccatorealloc che std :: vector non possa essere utilizzato per consentire potenzialmente la mappatura di più pagine alla fine dell'allocazione corrente , quindi è circa 2 volte più lento.
Peter Cordes

È stringview::remove_prefixeconomico come tenere traccia della tua posizione attuale in una stringa normale? std::basic_string::findha un secondo argomento opzionale pos = 0per farti iniziare a cercare da un offset.
Peter Cordes

@ Peter Cordes È corretto. Ho controllato libcxx
impl

Ho anche controllato libstdc ++ impl , che è lo stesso.
JiaHao Xu

La tua analisi delle prestazioni del vettore è disattivata. Si consideri un vettore che ha una capacità iniziale di 1 quando si inserisce per la prima volta e che raddoppia ogni volta che necessita di nuova capacità. Se è necessario inserire 17 elementi, la prima allocazione fa spazio per 1, quindi 2, quindi 4, quindi 8, quindi 16, infine 32. Ciò significa che c'erano 6 allocazioni in totale ( log2(size - 1) + 2, utilizzando il log intero). La prima allocazione ha spostato 0 stringhe, la seconda 1, poi 2, poi 4, poi 8, infine 16, per un totale di 31 mosse ( 2^(log2(size - 1) + 1) - 1)). Questo è O (n), non O (2 ^ n). Questo supererà notevolmente std::list.
David Stone,

2

Stai assumendo erroneamente che l'implementazione C ++ scelta sia necessariamente più veloce di quella di Python. La gestione delle stringhe in Python è altamente ottimizzata. Vedi questa domanda per ulteriori informazioni: Perché le operazioni std :: string funzionano male?


4
Non sto facendo affermazioni sulle prestazioni complessive del linguaggio, solo sul mio codice particolare. Quindi, nessuna ipotesi qui. Grazie per il buon suggerimento all'altra domanda. Non sono sicuro che tu stia dicendo che questa particolare implementazione in C ++ non è ottimale (la tua prima frase) o che C ++ è solo più lento di Python nell'elaborazione delle stringhe (la tua seconda frase). Inoltre, se conosci un modo veloce per fare quello che sto cercando di fare in C ++, condividilo a vantaggio di tutti. Grazie. Giusto per chiarire, adoro Python, ma non sono un fanboy cieco, motivo per cui sto cercando di imparare il modo più veloce per farlo.
JJC

1
@ JJC: Dato che l'implementazione di Python è più veloce, direi che la tua non è ottimale. Tieni presente che le implementazioni del linguaggio possono tagliare gli angoli per te, ma alla fine la complessità algoritmica e l'ottimizzazione della mano vincono. In questo caso, Python ha il sopravvento per questo caso d'uso per impostazione predefinita.
Matt Joiner

2

Se prendi l'implementazione di split1 e modifichi la firma in modo che corrisponda maggiormente a quella di split2, modificando questo:

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

a questa:

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

Ottieni una differenza più drammatica tra split1 e split2 e un confronto più equo:

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

Grazie nm! Sfortunatamente, sembra funzionare all'incirca alla stessa velocità dell'implementazione originale (split 1) sul mio set di dati e sulla mia macchina: $ / usr / bin / time cat test_lines_double | ./split8 21,89 reale 0,01 utente 0,47 sys C ++: ha visto 20000000 linee in 22 secondi. Velocità scricchiolio: 909090
JJC

Sulla mia macchina: split1 - 54s, split.py - 35s, split5 - 16s. Non ne ho idea.
n. 'pronomi' m.

Hmm, i tuoi dati corrispondono al formato che ho notato sopra? Presumo che tu abbia eseguito ciascuno più volte per eliminare effetti temporanei come il popolamento iniziale della cache del disco?
JJC

0

Sospetto che questo sia correlato al buffering su sys.stdin in Python, ma nessun buffering nell'implementazione C ++.

Vedi questo post per i dettagli su come modificare la dimensione del buffer, quindi prova di nuovo il confronto: Impostare una dimensione del buffer più piccola per sys.stdin?


1
Hmmm ... non seguo. La sola lettura delle righe (senza la divisione) è più veloce in C ++ rispetto a Python (dopo aver incluso la riga cin.sync_with_stdio (false);). Questo era il problema che ho avuto ieri, a cui ho fatto riferimento sopra.
JJC
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.