Quería comparar la lectura de líneas de entrada de cadena desde stdin usando Python y C ++ y me sorprendió ver que mi código C ++ se ejecutaba un orden de magnitud más lento que el código Python equivalente. Como mi C ++ está oxidado y todavía no soy un Pythonista experto, por favor dígame si estoy haciendo algo mal o si estoy malinterpretando algo.
(Respuesta TLDR: incluya la declaración: cin.sync_with_stdio(false)
o simplemente use fgets
en su lugar.
Resultados de TLDR: desplácese hasta el final de mi pregunta y mire 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
Equivalente a 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))
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
Debo señalar que probé esto tanto en Mac OS X v10.6.8 (Snow Leopard) como en Linux 2.6.32 (Red Hat Linux 6.2). El primero es un MacBook Pro, y el segundo es un servidor muy robusto, no es que esto sea demasiado 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
Pequeño anexo de referencia y resumen
Para completar, pensé que actualizaría la velocidad de lectura para el mismo archivo en el mismo cuadro con el código C ++ original (sincronizado). Nuevamente, esto es para un archivo de línea de 100M en un disco rápido. Aquí está la comparación, con varias soluciones / enfoques:
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
<iostream>
rendimiento apesta. No es la primera vez que sucede. 2) Python es lo suficientemente inteligente como para no copiar los datos en el bucle for porque no lo usa. Podrías volver a probar tratando de usarscanf
y achar[]
. Alternativamente, podría intentar reescribir el bucle para que se haga algo con la cadena (por ejemplo, mantenga la quinta letra y concatene en un resultado).cin.eof()
! Ponga lagetline
llamada en la declaración 'if`.wc -l
es rápido porque lee la transmisión más de una línea a la vez (puede ser unafread(stdin)/memchr('\n')
combinación). Los resultados de Python están en el mismo orden de magnitud, por ejemplo,wc-l.py
Respuestas:
De forma predeterminada,
cin
se sincroniza con stdio, lo que hace que evite cualquier almacenamiento en búfer de entrada. Si agrega esto a la parte superior de su página principal, debería ver un rendimiento mucho mejor:Normalmente, cuando una secuencia de entrada está almacenada, en lugar de leer un carácter a la vez, la secuencia se leerá en fragmentos más grandes. Esto reduce la cantidad de llamadas al sistema, que generalmente son relativamente caras. Sin embargo, dado que las implementaciones
FILE*
basadasstdio
yaiostreams
menudo tienen implementaciones separadas y, por lo tanto, memorias intermedias separadas, esto podría generar un problema si ambas se usaran juntas. Por ejemplo:Si se leyera más entrada
cin
de la que realmente necesitaba, entonces el segundo valor entero no estaría disponible para lascanf
función, que tiene su propio búfer independiente. Esto llevaría a resultados inesperados.Para evitar esto, de forma predeterminada, las secuencias se sincronizan con
stdio
. Una forma común de lograr esto es habercin
leído cada carácter uno a la vez según sea necesario usandostdio
funciones. Desafortunadamente, esto introduce muchos gastos generales. Para pequeñas cantidades de entrada, este no es un gran problema, pero cuando está leyendo millones de líneas, la penalización de rendimiento es significativa.Afortunadamente, los diseñadores de la biblioteca decidieron que también debería poder desactivar esta función para obtener un mejor rendimiento si supiera lo que estaba haciendo, por lo que proporcionaron el
sync_with_stdio
método.fuente
fscanf
llamada, porque eso simplemente no hace tanto trabajo como Python. Python debe asignar memoria para la cadena, posiblemente varias veces, ya que la asignación existente se considera inadecuada, exactamente como el enfoque de C ++std::string
. Es casi seguro que esta tarea está vinculada a E / S y hay demasiados FUD sobre el costo de crearstd::string
objetos en C ++ o usarlos<iostream>
en sí mismos.sync_with_stdio()
es una función miembro estática, y una llamada a esta función en cualquier objeto de flujo (por ejemplocin
) activa o desactiva la sincronización para todos los objetos estándar de iostream.Solo por curiosidad, he echado un vistazo a lo que sucede debajo del capó, y he usado dtruss / strace en cada prueba.
C ++
llamadas al sistema
sudo dtruss -c ./a.out < in
Pitón
llamadas al sistema
sudo dtruss -c ./a.py < in
fuente
Estoy unos años atrás aquí, pero:
En 'Editar 4/5/6' de la publicación original, está utilizando la construcción:
Esto está mal en un par de formas diferentes:
En realidad, estás cronometrando la ejecución
cat
, no tu punto de referencia. El uso de la CPU 'usuario' y 'sys' que se muestrantime
son aquellos decat
, no su programa de referencia. Peor aún, el tiempo "real" tampoco es necesariamente preciso. Dependiendo de la implementación decat
y de las tuberías en su sistema operativo local, es posible quecat
escriba un búfer gigante final y salga mucho antes de que el proceso del lector termine su trabajo.El uso de
cat
es innecesario y de hecho contraproducente; Estás agregando partes móviles. Si estuvieras en un sistema suficientemente antiguo (es decir, con una sola CPU y, en ciertas generaciones de computadoras, E / S más rápido que la CPU), el solo hecho de que secat
estuviera ejecutando podría colorear sustancialmente los resultados. También está sujeto a lo quecat
pueda hacer el almacenamiento en búfer de entrada y salida y otros procesos . (Esto probablemente le otorgaría un premio de 'Uso inútil del gato' si fuera Randal Schwartz.Una mejor construcción sería:
En esta declaración, es el shell el que abre big_file, pasándolo a su programa (bueno, en realidad al
time
que luego ejecuta su programa como un subproceso) como un descriptor de archivo ya abierto. El 100% de la lectura del archivo es estrictamente responsabilidad del programa que está intentando comparar. Esto le brinda una lectura real de su rendimiento sin complicaciones espurias.Mencionaré dos 'arreglos' posibles, pero realmente incorrectos, que también podrían considerarse (pero los 'numero' de manera diferente ya que no son cosas que estaban mal en la publicación original):
R. Puede "arreglar" esto cronometrando solo su programa:
B. o cronometrando toda la tubería:
Estos son incorrectos por las mismas razones que el n. ° 2: todavía se usan
cat
innecesariamente. Los menciono por algunas razones:son más "naturales" para las personas que no se sienten completamente cómodas con las funciones de redirección de E / S del shell POSIX
puede haber casos en los que
cat
se necesita (por ejemplo: el archivo para ser leído requiere algún tipo de privilegio de acceso, y usted no desea conceder ese privilegio al programa a partir de referencias:sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output
)en la práctica , en máquinas modernas, lo agregado
cat
en la tubería probablemente no tenga consecuencias reales.Pero digo lo último con algunas dudas. Si examinamos el último resultado en 'Editar 5' -
- esto afirma que
cat
consumió el 74% de la CPU durante la prueba; y de hecho 1.34 / 1.83 es aproximadamente el 74%. Quizás una serie de:¡Hubiera tomado solo los .49 segundos restantes! Probablemente no:
cat
aquí tuvo que pagar lasread()
llamadas al sistema (o equivalentes) que transfirieron el archivo desde el 'disco' (en realidad la memoria caché del búfer), así como las escrituras de tubería para entregarlaswc
. La prueba correcta aún habría tenido que hacer esasread()
llamadas; solo se habrían guardado las llamadas de escritura a canalización y lectura de canalización, y deberían ser bastante baratas.Aún así, predigo que sería capaz de medir la diferencia entre
cat file | wc -l
ywc -l < file
y encontrar una diferencia notable (porcentaje de 2 dígitos). Cada una de las pruebas más lentas habrá pagado una penalización similar en tiempo absoluto; que sin embargo equivaldría a una fracción menor de su tiempo total más grande.De hecho, hice algunas pruebas rápidas con un archivo de basura de 1.5 gigabytes, en un sistema Linux 3.13 (Ubuntu 14.04), obteniendo estos resultados (estos son en realidad 'el mejor de 3' resultados; después de preparar el caché, por supuesto):
Observe que los dos resultados de la canalización afirman haber tomado más tiempo de CPU (usuario + sys) que el tiempo real del reloj de pared. Esto se debe a que estoy usando el comando 'time' incorporado del shell (bash), que conoce la canalización; y estoy en una máquina de múltiples núcleos donde los procesos separados en una tubería pueden usar núcleos separados, acumulando tiempo de CPU más rápido que en tiempo real. Al usar
/usr/bin/time
, veo un tiempo de CPU menor que el tiempo real, lo que muestra que solo puede cronometrar el elemento de tubería único que se le pasó en su línea de comando. Además, la salida del shell da milisegundos, mientras que/usr/bin/time
solo da centésimas de segundo.Entonces, en el nivel de eficiencia de
wc -l
,cat
hace una gran diferencia: 409/283 = 1.453 o 45.3% más en tiempo real, y 775/280 = 2.768, ¡o se usa un sorprendente 177% más de CPU! En mi cuadro de prueba aleatorio, estaba allí en el momento.Debo agregar que hay al menos otra diferencia significativa entre estos estilos de prueba, y no puedo decir si es un beneficio o una falla; tienes que decidir esto tú mismo:
Cuando ejecuta
cat big_file | /usr/bin/time my_program
, su programa recibe información de una tubería, exactamente al ritmo enviado porcat
, y en fragmentos no más grandes que los escritos porcat
.Cuando ejecuta
/usr/bin/time my_program < big_file
, su programa recibe un descriptor de archivo abierto para el archivo real. Su programa, o en muchos casos las bibliotecas de E / S del idioma en el que se escribió, puede tomar diferentes medidas cuando se le presenta un descriptor de archivo que hace referencia a un archivo normal. Puede usarsemmap(2)
para asignar el archivo de entrada a su espacio de direcciones, en lugar de usarread(2)
llamadas explícitas del sistema. Estas diferencias podrían tener un efecto mucho mayor en sus resultados de referencia que el pequeño costo de ejecutar elcat
binario.Por supuesto, es un resultado de referencia interesante si el mismo programa se desempeña de manera significativamente diferente entre los dos casos. Muestra que, de hecho, el programa o sus bibliotecas de E / S están haciendo algo interesante, como usar
mmap()
. Entonces, en la práctica, podría ser bueno ejecutar los puntos de referencia en ambos sentidos; quizás descontando elcat
resultado por algún pequeño factor para "perdonar" el costo de ejecutarsecat
.fuente
$ < big_file time my_program
$ time < big_file my_program
Esto debería funcionar en cualquier shell POSIX (es decir, no `csh `y no estoy seguro acerca de exótica como` rc`:)time
está midiendo toda la tubería en lugar del primer programa.time seq 2 | while read; do sleep 1; done
imprime 2 segundos,/usr/bin/time seq 2 | while read; do sleep 1; done
imprime 0 segundos.Reproduje el resultado original en mi computadora usando g ++ en una Mac.
Agregar las siguientes declaraciones a la versión de C ++ justo antes de que el
while
bucle lo alinee con la versión de Python :sync_with_stdio mejoró la velocidad a 2 segundos, y establecer un búfer más grande lo redujo a 1 segundo.
fuente
getline
, operadores de transmisiónscanf
, puede ser conveniente si no le importa el tiempo de carga de archivos o si está cargando archivos de texto pequeños. Pero, si el rendimiento es algo que le interesa, realmente debería almacenar todo el archivo en la memoria (suponiendo que se ajuste).Aquí hay un ejemplo:
Si lo desea, puede ajustar una secuencia alrededor de ese búfer para un acceso más conveniente como este:
Además, si tiene el control del archivo, considere usar un formato de datos binarios planos en lugar de texto. Es más confiable leer y escribir porque no tiene que lidiar con todas las ambigüedades del espacio en blanco. También es más pequeño y mucho más rápido de analizar.
fuente
El siguiente código fue más rápido para mí que el otro código publicado aquí hasta ahora: (Visual Studio 2013, 64 bits, archivo de 500 MB con longitud de línea uniforme en [0, 1000)).
Supera todos mis intentos de Python en más de un factor 2.
fuente
read
syscalls sin búfer en un búfer de longitud estáticoBUFSIZE
o mediante lasmmap
syscalls correspondientes equivalentes , y luego se desplaza a través de ese búfer contando nuevas líneas a lafor (char *cp = buf; *cp; cp++) count += *cp == "\n"
. SinBUFSIZE
embargo, tendrá que ajustar su sistema, que stdio ya habrá hecho por usted. Pero esefor
bucle debería compilarse en instrucciones de lenguaje de ensamblador increíblemente rápidas para el hardware de su caja.Por cierto, la razón por la cual el recuento de líneas para la versión C ++ es uno mayor que el recuento para la versión de Python es que el indicador eof solo se establece cuando se intenta leer más allá de eof. Entonces el bucle correcto sería:
fuente
while (getline(cin, input_line)) line_count++;
++line_count;
y noline_count++;
.long
, y el compilador es bastante capaz de decir que no se usa el resultado del incremento. Si no genera un código idéntico para el postincremento y el preincremento, está roto.++line_count;
lugar deline_count++;
no dolería :)while
, ¿verdad? ¿Importaría si hubiera algún tipo de error y quisieras asegurarte de queline_count
fuera correcto? Solo estoy adivinando pero no entiendo por qué importaría.En su segundo ejemplo (con scanf ()), la razón por la que esto es aún más lento podría deberse a que scanf ("% s") analiza la cadena y busca cualquier carácter de espacio (espacio, tabulación, nueva línea).
Además, sí, CPython almacena en caché para evitar lecturas de disco duro.
fuente
Un primer elemento de una respuesta:
<iostream>
es lento. Maldita sea lento. Obtuve un gran aumento de rendimientoscanf
como en el siguiente, pero aún es dos veces más lento que Python.fuente
Bueno, veo que en tu segunda solución cambiaste
cin
ascanf
, que fue la primera sugerencia que te haría (cin es sloooooooooooow). Ahora, si cambia descanf
afgets
, vería otro aumento en el rendimiento:fgets
es la función C ++ más rápida para la entrada de cadenas.Por cierto, no sabía sobre esa cosa de sincronización, agradable. Pero igual deberías intentarlo
fgets
.fuente
fgets
que será incorrecto (en términos de recuento de líneas y en términos de división de líneas en bucles si realmente necesita usarlas) para líneas suficientemente grandes, sin verificaciones adicionales para líneas incompletas (e intentar compensarlo implica asignar buffers innecesariamente grandes , dondestd::getline
maneja la reasignación para que coincida con la entrada real sin problemas). Lo rápido y lo incorrecto es fácil, pero casi siempre vale la pena usarlo "un poco más lento, pero correcto", lo que te apagasync_with_stdio
.