1565 votos

¿Por qué la lectura de las líneas de la entrada estándar mucho más lento en C que Python?

Yo quería comparar la lectura de las líneas de la cadena de entrada de stdin usando Python y C++, y se sorprendió al ver mi código de C++ ejecutar una orden de magnitud más lento que el equivalente de código Python. Desde mi C++ es oxidado y yo no soy todavía un experto Pythonista, por favor, dime si estoy haciendo algo mal o si yo estoy entendiendo algo.


(tl;dr respuesta: incluya la declaración: cin.sync_with_stdio(falso) o simplemente usar fgets lugar.

tl;dr resultados: desplácese a la parte inferior de mi pregunta y mirar en la tabla.)


Código 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

Python Equivalente:

#!/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))

Aquí están mis resultados:

$ 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

Gracias de antemano!

Edit: debo señalar que he probado este, tanto en OS-X (10.6.8) y Linux 2.6.32 (RHEL 6.2). El primero es un macbook pro, el segundo es un muy fornido servidor, no es que esto es muy pertinente.

Edit 2: (se elimina esta edición, como ya no se aplica)

$ 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

Edit 3:

Bueno, he intentado J.N. la sugerencia de tratar de tener python almacén de la línea que se lea: pero no hizo ninguna diferencia para python velocidad.

También traté de J.N. sugerencia de uso de scanf en una matriz de char en lugar de getline en un std::string. Bingo! Esto resultó en un rendimiento equivalente para python y c++. (3,333,333 LPS con mis datos de entrada, que por cierto son sólo líneas cortas de tres campos de cada uno, generalmente alrededor de 20 caracteres de ancho, aunque a veces más).

Código:

char input_a[512];
char input_b[32];
char input_c[512];
while(scanf("%s %s %s\n", input_a, input_b, input_c) != EOF) {             
    line_count++;
};

Velocidad:

$ cat test_lines | ./readline_test_cpp2 
Read 10000000 lines in 3 seconds. LPS: 3333333
$ cat test_lines | ./readline_test2.py 
Read 10000000 lines in 3 seconds. LPS: 3333333

(Sí, me corrí varias veces). Así que, supongo que será ahora el uso de scanf lugar de getline. Pero, todavía estoy curioso si se piensa la gente de este impacto en el rendimiento de std::string/getline es normal y razonable.

Edición 4 (era: Final Editar / Solución):

Añadiendo: cin.sync_with_stdio(false);

Inmediatamente por encima de mi original, mientras que el bucle por encima de los resultados en el código que se ejecuta más rápido que Python.

Nueva comparación de rendimiento (esto es en mi Macbook Pro 2011), con el código original, el original con la sincronización de movilidad, y el original de python, respectivamente, en un archivo con 20 líneas de texto. Sí, me corrí varias veces para eliminar el caché de disco confundir.

$ /usr/bin/time cat test_lines_double | ./readline_test_cpp
       33.30 real         0.04 user         0.74 sys
Read 20000001 lines in 33 seconds. LPS: 606060
$ /usr/bin/time cat test_lines_double | ./readline_test_cpp1b
        3.79 real         0.01 user         0.50 sys
Read 20000000 lines in 4 seconds. LPS: 5000000
$ /usr/bin/time cat test_lines_double | ./readline_test.py 
        6.88 real         0.01 user         0.38 sys
Read 20000000 lines in 6 seconds. LPS: 3333333

Gracias a @Vaughn Cato por su respuesta! Cualquier elaboración de la gente puede hacer o las buenas referencias que la gente puede señalar en cuanto a por qué esta sincronización ocurre, lo que significa que, cuando es útil, y cuando está bien para deshabilitar sería muy apreciado por la posteridad. :-)

Edición 5 / Solución Mejor:

Según lo sugerido por Gandalf El Gris por debajo, se es incluso más rápido que scanf o la desincronización cin enfoque. También me enteré de que scanf y recibe ambos son peligrosos y NO deben SER UTILIZADOS debido a su potencial de desbordamiento de búfer. Así, escribí esta iteración usar fgets, la alternativa más segura para el que recibe. Aquí están las líneas pertinentes para que mis compañeros de noobs:

char input_line[MAX_LINE];
char *result;

//<snip>

while((result = fgets(input_line, MAX_LINE, stdin )) != NULL)    
    line_count++;
if (ferror(stdin))
    perror("Error reading stdin.");

Ahora, aquí están los resultados por medio de un archivo (a 100 metros de las líneas; ~3.4 GB) en un servidor rápido con disco muy rápido, la comparación de los python, sin sincronización cin, y el fgets enfoques, así como la comparación con el wc utilidad. [El scanf versión segfaulted y no me siento como para la solución de problemas.]:

$ /usr/bin/time cat temp_big_file | readline_test.py 
0.03user 2.04system 0:28.06elapsed 7%CPU (0avgtext+0avgdata 2464maxresident)k
0inputs+0outputs (0major+182minor)pagefaults 0swaps
Read 100000000 lines in 28 seconds. LPS: 3571428

$ /usr/bin/time cat temp_big_file | readline_test_unsync_cin 
0.03user 1.64system 0:08.10elapsed 20%CPU (0avgtext+0avgdata 2464maxresident)k
0inputs+0outputs (0major+182minor)pagefaults 0swaps
Read 100000000 lines in 8 seconds. LPS: 12500000

$ /usr/bin/time cat temp_big_file | readline_test_fgets 
0.00user 0.93system 0:07.01elapsed 13%CPU (0avgtext+0avgdata 2448maxresident)k
0inputs+0outputs (0major+181minor)pagefaults 0swaps
Read 100000000 lines in 7 seconds. LPS: 14285714

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU (0avgtext+0avgdata 2464maxresident)k
0inputs+0outputs (0major+182minor)pagefaults 0swaps
100000000


Recap (lines per second):
python:         3,571,428 
cin (no sync): 12,500,000
fgets:         14,285,714
wc:            54,644,808

Como se puede ver, fgets es mejor, pero todavía bastante lejos de wc rendimiento; estoy bastante seguro de que esto es debido al hecho de que el wc examina cada personaje sin ninguna memoria de la copia. Sospecho que, en este punto, en otras partes del código se convertirá en el cuello de la botella, así que no creo que la optimización de ese nivel, sería incluso la pena, incluso si es posible (ya que, después de todo, yo realmente se necesita para almacenar la lectura de las líneas en la memoria).

También tenga en cuenta que una pequeña desventaja con el uso de un char * buffer y fgets vs sin sincronización cin a la cadena, es que el último puede leer las líneas de la cualquier longitud, mientras que el primero requiere de la limitación de entrada a algún número finito. En la práctica, esto probablemente no es un problema para la lectura de la mayoría de la línea de entrada basados en archivos, como el buffer puede ser ajustada a un valor muy grande que no sería superado por el de entrada válido.

Este ha sido educativos. Gracias a todos por sus comentarios y sugerencias.

Edit 6:

Como se sugiere por J.F. Sebastian en los comentarios de abajo, el de GNU wc usa la utilidad del llano C read() (dentro de la caja fuerte-read.c wrapper) para leer fragmentos (de 16k bytes) en un tiempo y un recuento de las nuevas líneas. Aquí está una Python equivalente basado en su código (sólo mostrando el correspondiente fragmento que sustituye a la de python bucle for:

BUFFER_SIZE = 16384 
count = sum(chunk.count('\n') for chunk in iter(partial(sys.stdin.read, BUFFER_SIZE), ''))

El rendimiento de esta versión es bastante rápido (aunque todavía un poco más lento que el raw c wc utilidad, de curso:

$ /usr/bin/time cat temp_big_file | readline_test3.py 
0.01user 1.16system 0:04.74elapsed 24%CPU (0avgtext+0avgdata 2448maxresident)k
0inputs+0outputs (0major+181minor)pagefaults 0swaps
Read 100000000 lines in 4.7275 seconds. LPS: 21152829

De nuevo, es un poco tonto para mí comparar C++ fgets/cin y el primer código de python, por un lado, wc-l y este último fragmento de código python en el otro, como los dos últimos, en realidad no guarde el leer las líneas, sino simplemente el recuento de saltos de línea. Aún así, es interesante explorar todas las diferentes implementaciones y pensar acerca de las implicaciones de rendimiento. Gracias de nuevo!

Edit 7: Pequeña referencia adición y de recapitulación

(Hola HN lectores!)

La integridad, pensé en la actualización de la velocidad de lectura para el mismo archivo en el mismo cuadro con el original (sincronizados) código C++. De nuevo, esto es para un radio de 100 metros de la línea de archivo en un disco rápido. He aquí la tabla completa ahora:

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

También, ver a mi pregunta acerca de la división de líneas de C++ vs Python... una velocidad similar historia, donde el enfoque ingenuo es más lento en C++!

Edit: para mayor claridad, eliminado el pequeño error en el código original que no estaba relacionado con la pregunta. Por último, pequeños retoques a espacio en blanco y la salida de cadenas de caracteres para hacer la comparación más fácil/más clara.

1390voto

Vaughn Cato Puntos 30511

De forma predeterminada, cin está sincronizado con stdio, lo que provoca que para evitar cualquier entrada de búfer. Si sumamos esto a la parte superior de su principal, usted debe ver mucho mejor rendimiento:

cin.sync_with_stdio(false);

Normalmente, cuando una secuencia de entrada se almacenan en la memoria, en lugar de leer un carácter a la vez, la secuencia se puede leer en trozos más grandes. Esto reduce el número de llamadas al sistema, que suelen ser relativamente caro. Sin embargo, desde el ARCHIVO* basado en stdio y iostreams a menudo tienen implementaciones independientes y, por tanto, independiente de búferes, esto podría conducir a un problema si los dos se utilizan juntos. Por ejemplo:

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

Si más entrada fue leído por cin de lo que realmente se necesita, entonces, el segundo valor entero no estaría disponible para la función scanf, que tiene su propia e independiente de búfer. Esto podría conducir a resultados inesperados.

Para evitar esto, por defecto, las secuencias están sincronizados con stdio. Una manera común para lograr esto, es tener cin leer cada uno de los caracteres de uno en un tiempo como sea necesario el uso de stdio funciones. Por desgracia, este presenta una gran sobrecarga. Para cantidades pequeñas de entrada, esto no es un gran problema, pero cuando usted está leyendo millones de líneas, la penalización de rendimiento es considerable.

Afortunadamente, la biblioteca de diseñadores decidió que también debe ser capaz de deshabilitar esta característica para mejorar el rendimiento si sabía lo que estaba haciendo, de modo que siempre que la sync_with_stdio método.

136voto

2mia Puntos 365

Sólo por curiosidad he echado un vistazo a lo que ocurre bajo el capó, y yo he usado dtruss / strace en cada prueba.

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
 

Pitón

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

80voto

karunski Puntos 2067

Reproduje el resultado original de mi equipo con g en un Mac.

La adición de las siguientes declaraciones a la versión C justo antes de la while bucle lo trae en línea con el Python versión:

 std::ios_base::sync_with_stdio(false);
char buffer[1048576];
std::cin.rdbuf()->pubsetbuf(buffer, sizeof(buffer));
 

sync_with_stdio mejorar la velocidad a 2 segundos, y el establecimiento de un búfer mayor baja a 1 segundo.

12voto

Gregg Puntos 73

Por cierto, la razón por la cuenta de la línea para la versión C es uno mayor que el recuento para la versión de Python es que la bandera EF sólo se establece cuando se hace un intento de leer más allá de EF. De modo que el bucle correcta sería:

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

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

8voto

sarnold Puntos 62720

Puedo reproducir sus resultados en mi sistema. Yo alimentados con una 351 mb archivo binario para ambos programas y la versión de Python divide por cero porque se ejecuta tan rápidamente y el C++ versión tarda 12 segundos en ejecutarse.

Me sacó la velocidad media aritmética y corrió las pruebas un par de veces:

cat promedio de 0.055 segundos (más de ocho carreras) para volcar el archivo /dev/null.

La versión de Python toma un promedio de .484 segundos y 0.03 ssd (más de ocho carreras) para contar las líneas. He aquí un representante de salida de /usr/bin/time, lo que es suficiente para mostrar la memoria utilizada (20800 max residente kilobytes) y e / s de disco (0major == todo lo que se lee de la memoria caché).

0.48user 0.08system 0:00.56elapsed 98%CPU (0avgtext+0avgdata 20800maxresident)k
0inputs+0outputs (0major+1604minor)pagefaults 0swaps

El C++ versión lleva un promedio de 12.32 segundos y 0.23 ssd (más de ocho carreras) para contar las líneas. Un representante de salida de /usr/bin/time sólo muestra 4672 max residente kilobytes y otra vez, 0major muestra todo lo que se lee de la memoria caché:

12.34user 0.09system 0:12.45elapsed 99%CPU (0avgtext+0avgdata 4672maxresident)k
0inputs+8outputs (0major+349minor)pagefaults 0swaps

Tengo más memoria libre que yo sé qué hacer con:

$ free -m
             total       used       free     shared    buffers     cached
Mem:          5979       4413       1566          0        226       2594
-/+ buffers/cache:       1591       4387
Swap:         6347          1       6346

Como resumen rápido, el 4387 de la free columna -/+ buffers/cache de la línea indica que tengo aproximadamente cuatro gigabytes de memoria "libre" para el núcleo de cualquier tiempo que le plazca. La presión de la memoria no es un problema.

La versión de Python creó 54898 líneas en strace -o /tmp/python /tmp/readlines.py < /input/file.

El C++ versión creada 89802 líneas en strace -o /tmp/cpp /tmp/readlines < /input/file.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X