Estoy tratando de convertir algo de código de Python a C ++ en un esfuerzo por ganar un poco de velocidad y agudizar mis oxidadas habilidades en C ++. Ayer me sorprendió cuando una implementación ingenua de leer líneas de stdin era mucho más rápida en Python que en C ++ (ver esto ). Hoy, finalmente descubrí cómo dividir una cadena en C ++ con delimitadores de fusión (semántica similar a split () de Python), ¡y ahora estoy experimentando un deja vu! Mi código C ++ tarda mucho más en hacer el trabajo (aunque no un orden de magnitud más, como fue el caso de la lección de ayer).
Código 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('')
Código 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
Tenga en cuenta que probé dos implementaciones divididas diferentes. Uno (split1) usa métodos de cadena para buscar tokens y es capaz de fusionar múltiples tokens, así como manejar numerosos tokens (viene de aquí ). El segundo (split2) usa getline para leer la cadena como una secuencia, no fusiona delimitadores y solo admite un único carácter delimitador (ese fue publicado por varios usuarios de StackOverflow en las respuestas a las preguntas de división de cadenas).
Ejecuté esto varias veces en varios órdenes. Mi máquina de prueba es una Macbook Pro (2011, 8GB, Quad Core), no es que importe mucho. Estoy probando con un archivo de texto de línea de 20M con tres columnas separadas por espacios que se parecen a esto: "foo.bar 127.0.0.1 home.foo.bar"
Resultados:
$ /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
¿Qué estoy haciendo mal? ¿Hay una mejor manera de dividir cadenas en C ++ que no dependa de bibliotecas externas (es decir, sin impulso), que admita la combinación de secuencias de delimitadores (como la división de Python), sea segura para subprocesos (por lo tanto, no strtok) y cuyo rendimiento sea al menos a la par con Python?
Editar 1 / ¿Solución parcial ?:
Intenté hacer una comparación más justa haciendo que Python restableciera la lista ficticia y la agregara cada vez, como lo hace C ++. Esto todavía no es exactamente lo que está haciendo el código C ++, pero está un poco más cerca. Básicamente, el ciclo es ahora:
for line in sys.stdin:
dummy = []
dummy += line.split()
count += 1
El rendimiento de Python ahora es aproximadamente el mismo que el de la implementación split1 de C ++.
/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
Todavía me sorprende que, incluso si Python está tan optimizado para el procesamiento de cadenas (como sugirió Matt Joiner), estas implementaciones de C ++ no serían más rápidas. Si alguien tiene ideas sobre cómo hacer esto de una manera más óptima usando C ++, por favor comparta su código. (Creo que mi próximo paso será intentar implementar esto en C puro, aunque no voy a sacrificar la productividad del programador para volver a implementar mi proyecto general en C, por lo que este será solo un experimento para la velocidad de división de cadenas).
Gracias a todos por vuestra ayuda.
Edición / solución final:
Consulte la respuesta aceptada de Alf. Dado que Python trata con cadenas estrictamente por referencia y las cadenas STL a menudo se copian, el rendimiento es mejor con las implementaciones vanilla de Python. A modo de comparación, compilé y ejecuté mis datos a través del código de Alf, y aquí está el rendimiento en la misma máquina que todas las demás ejecuciones, esencialmente idéntico a la implementación de Python ingenua (aunque más rápida que la implementación de Python que restablece / agrega la lista, como mostrado en la edición anterior):
$ /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
Mi única queja restante es la cantidad de código necesario para que C ++ funcione en este caso.
Una de las lecciones aquí de este problema y del problema de lectura de la línea stdin de ayer (vinculado arriba) es que siempre se debe comparar en lugar de hacer suposiciones ingenuas sobre el rendimiento relativo "predeterminado" de los idiomas. Agradezco la educación.
¡Gracias nuevamente a todos por sus sugerencias!
g++ -Wall -O3 -o split1 split_1.cpp
@JJC: ¿Cómo le va a su punto de referencia cuando realmente usadummy
yspline
respectivamente, tal vez Python elimine la llamada aline.split()
porque no tiene efectos secundarios?Respuestas:
Como conjetura, las cadenas de Python son cadenas inmutables contadas por referencia, de modo que no se copian cadenas en el código de Python, mientras que C ++
std::string
es un tipo de valor mutable y se copia en la menor oportunidad.Si el objetivo es una división rápida, entonces se usarían operaciones de subcadena de tiempo constante, lo que significa que solo se hace referencia a partes de la cadena original, como en Python (y Java, y C #…).
Sin
std::string
embargo, la clase C ++ tiene una característica redentora: es estándar , por lo que se puede usar para pasar cadenas de manera segura y portátil donde la eficiencia no es una consideración principal. Pero suficiente charla. Código, y en mi máquina esto es, por supuesto, más rápido que Python, ya que el manejo de cadenas de Python se implementa en C, que es un subconjunto de C ++ (he he):Descargo de responsabilidad: espero que no haya errores. No he probado la funcionalidad, solo verifiqué la velocidad. Pero creo que, incluso si hay un error o dos, corregir eso no afectará significativamente la velocidad.
fuente
StringRef
, puede copiar la subcadena a unstd::string
muy fácilmente, simplementestring( sr.begin(), sr.end() )
.PyString_FromStringAndSize()
esas llamadasPyObject_MALLOC()
. Entonces, no hay optimización con una representación compartida que explote que las cadenas son inmutables en Python.No estoy proporcionando mejores soluciones (al menos en cuanto al rendimiento), pero algunos datos adicionales que podrían ser interesantes.
Usando
strtok_r
(variante reentrante destrtok
):Además, utilizando cadenas de caracteres para los parámetros y
fgets
para la entrada:Y, en algunos casos, donde es aceptable destruir la cadena de entrada:
Los tiempos para estos son los siguientes (incluidos mis resultados para las otras variantes de la pregunta y la respuesta aceptada):
Como podemos ver, la solución de la respuesta aceptada sigue siendo la más rápida.
Para cualquiera que quiera hacer más pruebas, también puse un repositorio de Github con todos los programas de la pregunta, la respuesta aceptada, esta respuesta y, además, un Makefile y un script para generar datos de prueba: https: // github. com / tobbez / string-splitting .
fuente
memcpy
, nostrcpy
, en caso de que el compilador no advierta esa optimización.strcpy
Por lo general, utiliza una estrategia de inicio más lenta que logra un equilibrio entre rápido para cadenas cortas y aumento de SIMD completo para cadenas largas.memcpy
conoce el tamaño de inmediato y no tiene que usar ningún truco SIMD para verificar el final de una cadena de longitud implícita. (No es gran cosa en x86 moderno). La creación destd::string
objetos con el(char*, len)
constructor también podría ser más rápido, si puede aprovechar esosaveptr-token
. Obviamente, sería más rápido almacenarchar*
tokens: PSospecho que esto se debe a la forma en que
std::vector
se cambia el tamaño durante el proceso de una llamada a la función push_back (). Si intenta usarstd::list
ostd::vector::reserve()
para reservar suficiente espacio para las oraciones, debería obtener un rendimiento mucho mejor. O puede usar una combinación de ambos como se muestra a continuación para split1 ():EDITAR : La otra cosa obvia que veo es que la variable de Python
dummy
se asigna cada vez pero no se modifica. Por tanto, no es una comparación justa con C ++. Debería intentar modificar su código Pythondummy = []
para inicializarlo y luego hacerlodummy += line.split()
. ¿Puede informar el tiempo de ejecución después de esto?EDIT2 : Para hacerlo aún más justo, puede modificar el ciclo while en el código C ++ para que sea:
fuente
Creo que el siguiente código es mejor, usando algunas características de C ++ 17 y C ++ 14:
La elección del contenedor:
std::vector
.Suponiendo que el tamaño inicial de la matriz interna asignada es 1, y el tamaño final es N, asignará y desasignará para log2 (N) veces, y copiará (2 ^ (log2 (N) + 1) - 1) = (2N - 1) veces. Como se señaló en ¿El bajo rendimiento de std :: vector se debe a que no se llama a realloc un número logarítmico de veces? , esto puede tener un rendimiento deficiente cuando el tamaño del vector es impredecible y podría ser muy grande. Pero, si puede estimar el tamaño, esto será un problema menor.
std::list
.Para cada push_back, el tiempo que consume es una constante, pero probablemente lleve más tiempo que std :: vector en push_back individual. El uso de un grupo de memoria por subproceso y un asignador personalizado puede aliviar este problema.
std::forward_list
.Igual que std :: list, pero ocupa menos memoria por elemento. Requiere que funcione una clase contenedora debido a la falta de API push_back.
std::array
.Si puede conocer el límite de crecimiento, puede usar std :: array. Por supuesto, no puede usarlo directamente, ya que no tiene la API push_back. Pero puede definir un contenedor, y creo que es la forma más rápida aquí y puede ahorrar algo de memoria si su estimación es bastante precisa.
std::deque
.Esta opción le permite intercambiar memoria por rendimiento. No habrá (2 ^ (N + 1) - 1) veces la copia del elemento, solo N veces la asignación y no habrá desasignación. Además, tendrá un tiempo de acceso aleatorio constante y la capacidad de agregar nuevos elementos en ambos extremos.
Según std :: deque-cppreference
o puede usar una combinación de estos:
std::vector< std::array<T, 2 ^ M> >
Esto es similar a std :: deque, la diferencia es que este contenedor no admite agregar elementos al frente. Pero aún tiene un rendimiento más rápido, debido al hecho de que no copiará la matriz std :: subyacente durante (2 ^ (N + 1) - 1) veces, solo copiará la matriz de punteros para (2 ^ (N - M + 1) - 1) veces, y asignando una nueva matriz solo cuando la corriente está llena y no necesita desasignar nada. Por cierto, puede obtener un tiempo de acceso aleatorio constante.
std::list< std::array<T, ...> >
Alivia enormemente la presión de la fractura de la memoria. Solo asignará una nueva matriz cuando la actual esté llena y no necesita copiar nada. Aún tendrá que pagar el precio de un puntero adicional en comparación con el combo 1.
std::forward_list< std::array<T, ...> >
Igual que 2, pero cuesta la misma memoria que el combo 1.
fuente
N
caso muy grande . Es una lástima que std :: vector no pueda usarrealloc
para permitir potencialmente mapear más páginas al final de la asignación actual , por lo que es aproximadamente 2 veces más lento.stringview::remove_prefix
tan económico como realizar un seguimiento de su posición actual en una cadena normal?std::basic_string::find
tiene un segundo argumento opcionalpos = 0
que le permite comenzar a buscar desde un desplazamiento.log2(size - 1) + 2
utilizando el registro de números enteros). La primera asignación movió 0 cadenas, la segunda movió 1, luego 2, luego 4, luego 8, luego finalmente 16, para un total de 31 movimientos (2^(log2(size - 1) + 1) - 1)
). Esto es O (n), no O (2 ^ n). Esto superará en gran medidastd::list
.Está asumiendo erróneamente que la implementación de C ++ elegida es necesariamente más rápida que la de Python. El manejo de cadenas en Python está altamente optimizado. Consulte esta pregunta para obtener más información: ¿Por qué las operaciones std :: string funcionan mal?
fuente
Si toma la implementación de split1 y cambia la firma para que coincida más con la de split2, cambie esto:
a esto:
Obtiene una diferencia más dramática entre split1 y split2, y una comparación más justa:
fuente
fuente
Sospecho que esto está relacionado con el almacenamiento en búfer en sys.stdin en Python, pero sin almacenamiento en búfer en la implementación de C ++.
Consulte esta publicación para obtener detalles sobre cómo cambiar el tamaño del búfer, luego intente la comparación nuevamente: ¿ Establecer un tamaño de búfer más pequeño para sys.stdin?
fuente