benchmarking getline - Perché leggere le righe da stdin è molto più lento in C++rispetto a Python?



iostream how (9)

Nel secondo esempio (con scanf ()) il motivo per cui questo è ancora più lento potrebbe essere perché scanf ("% s") analizza la stringa e cerca qualsiasi spazio char (spazio, tab, newline).

Inoltre, sì, CPython fa un po 'di cache per evitare letture di dischi rigidi.

Volevo confrontare le righe di lettura di input di stringa da stdin usando Python e C ++ ed ero scioccato nel vedere il mio codice C ++ eseguire un ordine di grandezza più lento del codice Python equivalente. Dato che il mio C ++ è arrugginito e non sono ancora un esperto Pythonista, ti prego di dirmi se sto facendo qualcosa di sbagliato o se sto fraintendendo qualcosa.

(Risposta TLDR: includi la frase: cin.sync_with_stdio(false) o usa invece fgets .

Risultati TLDR: scorri fino in fondo alla mia domanda e guarda la tabella.)

Codice C ++:

#include <iostream>
#include <time.h>

using namespace std;

int main() {
    string input_line;
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    while (cin) {
        getline(cin, input_line);
        if (!cin.eof())
            line_count++;
    };

    sec = (int) time(NULL) - start;
    cerr << "Read " << line_count << " lines in " << sec << " seconds.";
    if (sec > 0) {
        lps = line_count / sec;
        cerr << " LPS: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

// Compiled with:
// g++ -O3 -o readline_test_cpp foo.cpp

Equivalente Python:

#!/usr/bin/env python
import time
import sys

count = 0
start = time.time()

for line in  sys.stdin:
    count += 1

delta_sec = int(time.time() - start_time)
if delta_sec >= 0:
    lines_per_sec = int(round(count/delta_sec))
    print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
       lines_per_sec))

Ecco i miei risultati:

$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889

$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. LPS: 5570000

Dovrei notare che ho provato questo sia con Mac OS X v10.6.8 (Snow Leopard) e Linux 2.6.32 (Red Hat Linux 6.2). Il primo è un MacBook Pro, e quest'ultimo è un server molto robusto, non che questo sia troppo pertinente.

$ for i in {1..5}; do echo "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP:   Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in  1 seconds. LPS: 5570000

Piccolo punto di riferimento e riepilogo

Per completezza, ho pensato di aggiornare la velocità di lettura per lo stesso file sulla stessa scatola con il codice C ++ originale (sincronizzato). Ancora una volta, questo è per un file di linea 100M su un disco veloce. Ecco il confronto, con diverse soluzioni / approcci:

Implementation      Lines per second
python (default)           3,571,428
cin (default/naive)          819,672
cin (no sync)             12,500,000
fgets                     14,285,714
wc (not fair comparison)  54,644,808

Bene, vedo che nella tua seconda soluzione sei passato da cin a scanf , che è stato il primo suggerimento che stavo per farti (cin è sloooooooooooow). Ora, se si passa da scanf a fgets , si noterà un ulteriore aumento delle prestazioni: fgets è la funzione C ++ più veloce per l'input di stringhe.

A proposito, non sapevo cosa fosse la sincronizzazione, bello. Ma dovresti comunque provare fgets .


Di default, cin è sincronizzato con stdio, il che fa sì che eviti qualsiasi buffer di input. Se lo aggiungi alla parte superiore del tuo main, dovresti vedere prestazioni molto migliori:

std::ios_base::sync_with_stdio(false);

Normalmente, quando viene bufferizzato un flusso di input, anziché leggere un carattere alla volta, lo stream verrà letto in blocchi più grandi. Ciò riduce il numero di chiamate di sistema, che sono in genere relativamente costose. Tuttavia, poiché lo stdio e gli iostreams basati su FILE* spesso hanno implementazioni separate e quindi buffer separati, ciò potrebbe causare un problema se entrambi fossero utilizzati insieme. Per esempio:

int myvalue1;
cin >> myvalue1;
int myvalue2;
scanf("%d",&myvalue2);

Se cin più input è stato letto da quello effettivamente necessario, allora il secondo valore intero non sarebbe disponibile per la funzione scanf , che ha il proprio buffer indipendente. Ciò porterebbe a risultati inaspettati.

Per evitare questo, per impostazione predefinita, i flussi sono sincronizzati con stdio . Un modo comune per raggiungere questo obiettivo è quello di far leggere a cin ogni personaggio uno alla volta secondo necessità utilizzando le funzioni stdio . Sfortunatamente, questo introduce un sacco di spese generali. Per piccole quantità di input, questo non è un grosso problema, ma quando si leggono milioni di righe, la penalizzazione delle prestazioni è significativa.

Fortunatamente, i progettisti della biblioteca hanno deciso che dovresti essere in grado di disabilitare questa funzione per ottenere prestazioni migliori se sapessi cosa stavi facendo, quindi hanno fornito il metodo sync_with_stdio .


Il seguente codice è stato più veloce per me rispetto all'altro codice pubblicato finora: (Visual Studio 2013, file a 64 bit, 500 MB con lunghezza della linea uniformemente in [0, 1000)).

const int buffer_size = 500 * 1024;  // Too large/small buffer is not good.
std::vector<char> buffer(buffer_size);
int size;
while ((size = fread(buffer.data(), sizeof(char), buffer_size, stdin)) > 0) {
    line_count += count_if(buffer.begin(), buffer.begin() + size, [](char ch) { return ch == '\n'; });
}

Batte tutti i miei tentativi Python di più di un fattore 2.


A proposito, il motivo per cui il conteggio delle righe per la versione C ++ è maggiore del conteggio per la versione di Python è che il flag di eof viene impostato solo quando si tenta di leggere oltre eof. Quindi il ciclo corretto sarebbe:

while (cin) {
    getline(cin, input_line);

    if (!cin.eof())
        line_count++;
};

Un primo elemento di una risposta: <iostream> è lento. Accidenti, lento. Ottengo un enorme incremento delle prestazioni con scanf come nel seguito, ma è ancora due volte più lento di Python.

#include <iostream>
#include <time.h>
#include <cstdio>

using namespace std;

int main() {
    char buffer[10000];
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    int read = 1;
    while(read > 0) {
        read = scanf("%s", buffer);
        line_count++;
    };
    sec = (int) time(NULL) - start;
    line_count--;
    cerr << "Saw " << line_count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}

getline , gli operatori di streaming, scanf , possono essere utili se non ti interessa il tempo di caricamento dei file o se stai caricando piccoli file di testo. Ma se la performance è qualcosa a cui tieni, in realtà devi solo bufferizzare l'intero file in memoria (supponendo che si adatti).

Ecco un esempio:

//open file in binary mode
std::fstream file( filename, std::ios::in|::std::ios::binary );
if( !file ) return NULL;

//read the size...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

//read into memory buffer, then close it.
char *filebuf = new char[length+1];
file.read(filebuf, length);
filebuf[length] = '\0'; //make it null-terminated
file.close();

Se lo desideri, puoi avvolgere un flusso attorno a quel buffer per un accesso più comodo come questo:

std::istrstream header(&filebuf[0], length);

Inoltre, se si ha il controllo del file, è consigliabile utilizzare un formato di dati binario piatto anziché testo. È più affidabile leggere e scrivere perché non devi affrontare tutte le ambiguità degli spazi bianchi. È anche più piccolo e molto più veloce da analizzare.


Solo per curiosità ho dato un'occhiata a quello che succede sotto il cofano, e ho usato dtruss/strace su ogni test.

C ++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

syscalls sudo dtruss -c ./a.out < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            6
pread                                           8
mprotect                                       17
mmap                                           22
stat64                                         30
read_nocancel                               25958

Pitone

./a.py < in
Read 6512402 lines in 1 seconds. LPS: 6512402

syscalls sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29

Può essere vecchio C ++ e ottimizzazioni. Nel mio computer ho ottenuto quasi la stessa velocità:

un loop: 1.577ms due loop: 1.507ms

Corro VS2015 su processore E5-1620 da 3,5 Ghz con RAM da 16 GB





python c++ benchmarking iostream getline