2009-03-03 11 views
12

La semana pasada escribí algunas líneas de código en C# para iniciar un archivo de texto grande (300,000 líneas) en un diccionario. Le tomó diez minutos escribir y lo ejecutó en menos de un segundo.C++ gestión de memoria de cadena

Ahora estoy convirtiendo ese pedazo de código en C++ (porque lo necesito en un antiguo objeto de C++ COM). He pasado dos días hasta aquí. :-(Aunque la diferencia de productividad es impactante por sí misma, es el rendimiento que necesitaría algunos consejos.

Tarda siete segundos en cargarse, y lo que es peor: lleva exactamente ese tiempo liberar todo CStringWs después. esto no es aceptable, y debe encontrar una manera de aumentar el rendimiento.

¿hay alguna posibilidad de que pueda destinar esta cantidad de cadenas sin ver esta degradación performace tan horrible?

Mi conjetura en este momento es que tendré que rellenar todo el texto en una gran matriz y luego dejar que mi tabla hash apunte al comienzo de cada cadena dentro de esta matriz y soltar las cosas de CStringW.

Pero antes de eso, ¿algún consejo de ustedes expertos C++ por ahí?

EDIT: Mi respuesta a mí mismo se da a continuación. Me di cuenta de que esa es la ruta más rápida para mí, y también paso en lo que I considere en la dirección correcta - hacia un código más administrado.

+0

Realmente necesita mostrarnos el código que está utilizando ahora, su descripción es demasiado vaga como para dar su opinión. –

+0

Definitivamente debe proporcionar más información sobre su estructura de datos ... –

+1

Antes de estar familiarizado con .Net, la transición de C++ a C# - sin tener idea de cómo los marcos (en mi caso, el VCL) eran similares o diferentes - habría sido tan desafiante como a la inversa. No hace falta culpar a C++ por hacer su pregunta. – overslacked

Respuesta

11

Usted está caminando en los zapatos de Raymond Chen. Hizo exactamente lo mismo, escribiendo un diccionario chino en C++ no administrado. Rico Mariani también lo hizo, escribiéndolo en C#. El Sr. Mariani hizo una versión. El Sr. Chen escribió 6 versiones, tratando de igualar el rendimiento de la versión de Mariani. Más o menos reescribió fragmentos significativos de la biblioteca C/C++ en tiempo de ejecución para llegar allí.

El código administrado obtuvo mucho más respeto después de eso. El asignador GC es imposible de superar. Consulte this blog publicación para los enlaces. Este blog post también puede interesarle, es instructivo ver cómo la semántica del valor de STL es parte del problema.

+0

Bueno, para citar a Mariani "Entonces, sí, definitivamente puedes vencer al CLR". pero acepto que el rendimiento de CLR es muy impresionante. –

3

¿Qué clase de un recipiente que está almacenando sus cadenas en? Si es un std::vector de CStringW y si no tiene reserve -suficiente memoria de antemano, está destinado a recibir un golpe. A vector normalmente cambia el tamaño una vez que alcanza su límite (que no es muy alto) y luego copia la totalidad a la nueva ubicación de la memoria, que puede darte un gran golpe. A medida que su vector crece exponencialmente (es decir, si el tamaño inicial es 1, la próxima vez que asigna 2, 4 la próxima vez, el golpe se vuelve cada vez menos frecuente).

También ayuda a saber cuánto tiempo las cuerdas individuales son. (Por momentos :)

+0

Arreglo fijo ...¿Está asignando toda la memoria por adelantado o cambiando el tamaño de la matriz con cada cadena nueva? –

+0

No no no. Nunca lo realizo. Es una tabla hash y en las * raras * ocasiones en las que obtengo una clave hash duplicada se hacen algunas cosas adicionales, pero durante mi evaluación comparativa arrojo los duplicados solo para asegurarme de que no haya ningún problema de rendimiento. –

+0

Parece que tiene todo configurado perfectamente. ¿Has probado con std :: wstring? Además, es hora de tomar la ayuda de un generador de perfiles. – dirkgently

10

Yikes. Deshacerse de la CStrings ...

tratar un perfilador también. ¿Estás seguro de que no solo estás ejecutando código de depuración?

use std :: string en su lugar.

EDIT:

que acabo de hacer una simple prueba de comparaciones ctor y Dtor.

CStringW parece tomar entre 2 y 3 veces el tiempo para hacer un nuevo/eliminar.

itera 1000000 veces haciendo new/delete para cada tipo. Nada más, y una llamada GetTickCount() antes y después de cada ciclo. Consistentemente obtener el doble de tiempo para CStringW.

Eso no resuelve todo su problema, aunque sospecho.

EDIT: Tampoco creo que el uso de cadenas o CStringW sea el problema real, hay algo más que está causando el problema.

(mas por causa de fuerza mayor, uso STL de todos modos!)

Es necesario que el perfil it. Eso es un desastre

+0

Voy a intentar std :: string, estad atentos! :-) –

+0

CString no es tan malo ... – peterchen

+1

CString es malo. Primero, como acabo de confirmar, es más lento que std :: string. No es portátil. Es un truco de MFC, junto con todos los demás elementos no deseados de MFC. No me malinterpreten, los he usado durante años, pero mientras menos MFC usemos, mejor ... – Tim

1

Al trabajar con clases de cadenas, siempre debe tener en cuenta las operaciones innecesarias, por ejemplo, no utilizar constructores, concatenaciones y tales operaciones con demasiada frecuencia, especialmente evitarlas en bucles. Supongo que hay algún motivo de codificación de caracteres por el que utilizas CStringW, por lo que probablemente no puedas usar algo diferente, esta sería otra forma de optimizar tu código.

4

Si es un diccionario de solo lectura, lo siguiente debería funcionar para usted.

Use fseek/ftell functionality, to find the size of the text file. 

Allocate a chunk of memory of that size + 1 to hold it. 

fread the entire text file, into your memory chunk. 

Iterate though the chunk. 

    push_back into a vector<const char *> the starting address of each line. 

    search for the line terminator using strchr. 

    when you find it, deposit a NUL, which turns it into a string. 
    the next character is the start of the next line 

until you do not find a line terminator. 

Insert a final NUL character. 

Ahora puede utilizar el vector, para obtener el puntero, que le permitirá el acceso el valor correspondiente.

Cuando haya terminado con su diccionario, desasigne la memoria, deje que el vector muera cuando salga del ámbito.

[EDITAR] Esto puede ser un poco más complicado en la plataforma dos, ya que el terminador de línea es CRLF.

En ese caso, use strstr para encontrarlo, e incremente en 2 para encontrar el comienzo de la siguiente línea.

+0

Esto funcionará bien y es rápido. No creo que sea necesario, está haciendo algo malo en su código que probablemente pueda arreglarse y tenga un código legible y simple. – Tim

+0

Lo que lo está matando es el costo de construir/destruir con asignaciones/desasignaciones de memoria. – EvilTeach

+0

O (si Win32 admite el equivalente) mmap el archivo MAP_PRIVATE (copia en escritura), que guarda la copia. –

12

Esto suena muy parecido a la versión de Raymond Chen vs Rico Mariani's C++ vs C# chino/inglés. Le tomó varias iteraciones a Raymond para vencer a C#.

Quizás haya ideas allí que podrían ayudar.

http://blogs.msdn.com/ricom/archive/2005/05/10/performance-quiz-6-chinese-english-dictionary-reader.aspx

+0

+1 para el enlace interesante – EvilTeach

+0

Sí, ¡no sabes qué tan correcto eres! :-) Pero una cosa extra más complicada para mí es que quiero leer ASCII, UTF8 y Unicode de una vez, así que debo hacer una traducción de los datos del archivo a los datos que almaceno. –

+1

@danbystrom, ASCII se puede considerar como un subconjunto de UTF-8. UTF-8 es un formato de codificación Unicode. Si tiene sus archivos en UTF-8 puede usar cualquier carácter definido en Unicode y el texto ASCII existente no necesita recodificación. – CMircea

2

Cargue la cadena en un solo búfer, analice el texto para reemplazar los saltos de línea con terminadores de cadena ('\ 0') y use los punteros en ese búfer para agregarlo al conjunto.

Alternativamente, p. Ej. si tiene que hacer una conversión ANSI/UNICODE durante la carga, use un asignador de fragmentos, que sacrifica la eliminación de elementos individuales.

class ChunkAlloc 
{ 
    std::vector<BYTE> m_data; 
    size_t m_fill; 
    public: 
    ChunkAlloc(size_t chunkSize) : m_data(size), m_fill(0) {} 
    void * Alloc(size_t size) 
    { 
     if (m_data.size() - m_fill < size) 
     { 
      // normally, you'd reserve a new chunk here 
      return 0; 
     } 
     void * result = &(m_data[m_fill]); 
     m_fill += size; 
     return m_fill; 
    } 
} 
// all allocations from chuunk are freed when chung is destroyed. 

No sería cortar que juntos en diez minutos, pero 30 minutos y algunas pruebas suena bien :)

+0

Sí, sí sí. ¡Eso es lo que he descubierto durante la noche también! :-) –

+0

:) Ver Raymond vs Rico (http://blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx) es muy similar a su proyecto, con resultados claros: con C++ usted puede resuelva su problema más rápido que la aplicación C# incluso carga, pero le cuesta. Mucho. – peterchen

3

Gracias a todos ustedes por sus valiosos comentarios. ¡Votaciones para ti! :-)

Debo admitir que no estaba preparado para esto en absoluto, que C# vencería a la basura del viejo C++ de esta manera. Por favor, no lo interprete como una ofensa a C++, sino como un administrador de memoria asombrosamente bueno que se encuentra dentro de .NET Framework.

¡Decidí dar un paso atrás y luchar en esta batalla en la Arena InterOp! Es decir, mantendré mi código C# y dejaré que mi antiguo código C++ hable con el código C# sobre una interfaz COM.

Muchas preguntas se les hizo sobre mi código y voy a tratar de responder a algunas de ellas:

  • El compilador fue Visual Studio 2008 y no, yo no estaba corriendo una versión de depuración.

  • El archivo se leyó con un lector de archivos UTF8 que descargué de un empleado de Microsoft que lo publicó en su sitio. Devolvió CStringW y aproximadamente el 30% del tiempo se dedicó allí solo a leer el archivo.

  • El contenedor donde almacené las cadenas era solo un vector de tamaño fijo de punteros a CStringW y nunca se redimensionó.

EDIT: Estoy convencido de que las sugerencias que me dieron habrían hecho funcionar, y que probablemente podría vencer el código C# si he invertido bastante tiempo en ella. Por otro lado, hacerlo no proporcionaría ningún valor para el cliente y la única razón para hacerlo sería simplemente para demostrar que podría hacerse ...

+0

gracias por la sinopsis. – Tim

3

El problema no está en el CString, sino en que está asignando una gran cantidad de objetos pequeños; el asignador de memoria predeterminado no está optimizado para esto.

Escriba su propio asignador: asigne una gran cantidad de memoria y luego avance un puntero al asignar. Esto es lo que realmente hace el asignador de .NET. Cuando esté listo, elimine todo el buffer.

Creo que hubo muestra de escritura de encargo nueva/Eliminar operadores (más) A partir del C++

+0

Aunque nunca intenté usar eso, los contenedores de la Biblioteca Estándar de C++ aceptan un Allocator como parámetro, para sintonizar/optimizar las creaciones de los elementos. Si necesita. –

0

No es de extrañar que la gestión de la memoria de CLR es mejor que el montón de trucos viejos y sucios MFC se basa en: es al menos dos veces más joven que MFC, y está basado en la agrupación. Cuando tuve que trabajar en un proyecto similar con matrices de cadenas y WinAPI/MFC, acabo de usar std :: basic_string instanciado con TCHAR de WinAPI y mi propio asignador basado en Loki :: SmallObjAllocator. También puede echar un vistazo a boost :: pool en este caso (si desea que tenga una "sensación estándar" o tenga que usar una versión del compilador de VC++ anterior a la 7.1).