2012-05-21 10 views
15

Uso una implementación de montón personalizada en uno de mis proyectos. Consta de dos partes principales:Heap optimizado para (aunque sin limitarse a) uso de un único subproceso

  1. Montón de tamaño de bloque fijo. Es decir. un montón que asigna bloques de un tamaño específico solamente. Asigna bloques de memoria más grandes (ya sea páginas de memoria virtual o de otro montón), y luego los divide en unidades de asignación atómica.

    Realiza asignación/liberación rápida (en O (1)) y no hay gastos generales de uso de memoria, sin tomar en cuenta las cosas impuestas por el montón externo.

  2. Global de uso general montón. Consiste en cubos de los montones anteriores (tamaño fijo). WRT el tamaño de asignación solicitado elige el segmento adecuado y realiza la asignación a través de él.

    Dado que toda la aplicación es (en gran medida) de múltiples subprocesos: el montón global bloquea el contenedor correspondiente durante su funcionamiento.

    Nota: a diferencia de los montones tradicionales, este montón requiere el tamaño de asignación no solo para la asignación, sino también para la liberación. Esto permite identificar el depósito apropiado sin búsquedas ni sobrecarga adicional de memoria (como guardar el tamaño de bloque que precede al bloque asignado). Aunque algo menos conveniente, esto está bien en mi caso. Además, dado que la "configuración del depósito" es conocida en tiempo de compilación (implementada a través del vudú de plantillas C++), el depósito apropiado se determina en tiempo de compilación.

Hasta ahora todo se ve (y funciona) bien.

Recientemente trabajé en un algoritmo que realiza operaciones de almacenamiento en gran cantidad y, naturalmente, se ve afectado significativamente por el rendimiento del montón. La creación de perfiles reveló que su rendimiento se ve considerablemente afectado por el bloqueo . Es decir, el almacenamiento en sí funciona muy rápido (la asignación típica implica solo algunas instrucciones de eliminación de referencias de memoria), pero como toda la aplicación es de subprocesos múltiples, el depósito apropiado está protegido por la sección crítica, que se basa en instrucciones interbloqueadas, que son mucho más pesados

Lo he arreglado mientras tanto dando a este algoritmo su propio montón dedicado, que no está protegido por una sección crítica. Pero esto impone varios problemas/restricciones en el nivel de código. Tal como la necesidad de pasar la información de contexto en el interior de la pila donde sea que sea necesario. También se puede usar TLS para evitar esto, pero esto puede causar algunos problemas con el reingreso en mi caso específico.

Esto me hace preguntarme: ¿Existe una técnica conocida para optimizar el montón (pero no se limita a) el uso de un único subproceso?

EDIT:

agradecimiento especial a @Voo por sugerir retirar tcmalloc del google.

Parece que funciona de manera similar a lo que hice más o menos (al menos para objetos pequeños). Pero además resuelven el problema exacto que tengo, al mantener por subproceso caché.

Yo también pensé en esta dirección, pero pensé en mantener por montones. Entonces liberar un bloque de memoria asignado desde el montón que pertenece a otro subproceso es algo complicado: uno debe insertarlo en una especie de cola bloqueada, y ese otro subproceso debe notificarse, y liberar las asignaciones pendientes de forma asincrónica. La desasignación asíncrona puede causar problemas: si ese hilo está ocupado por algún motivo (por ejemplo, realiza cálculos agresivos), no se produce una desasignación de memoria. Además en el escenario de subprocesos múltiples, el costo de la desasignación es significativamente mayor.

OTOH la idea del almacenamiento en caché parece mucho más simple y más eficiente. Trataré de resolverlo.

Muchas gracias.

P.S .:

De hecho tcmalloc de Google es grande. Creo que se implementó de manera muy similar a lo que hice (al menos parte de tamaño fijo).

Pero, para ser pedante, hay una cuestión en la que mi montón es superior. De acuerdo con los documentos, tcmalloc impone una sobrecarga de aproximadamente 1% (asintóticamente), mientras que mi sobrecarga es 0.0061%. Es 4/64K para ser exacto.

:)

+0

Esto recuerda a mi las pruebas que he hecho hace años. El mecanismo estándar "pobre" comúnmente utilizado ocupa más de 100 veces una buena implementación "personalizada". –

+1

Me encantaría ver lo que ha hecho si es más rápido que el asignador de memoria estándar. Como la mayoría de las implementaciones estándar ya hacen lo que afirma hacer (y mucho más). Encuentro que el reclamo O (1) es curioso, especialmente cuando usted dice que no tiene gastos generales (estoy seguro de que ganará un centavo cuando lo haga su patente). –

+1

La idea de cubo completo es básicamente tcmalloc de Google (aunque dado que es un asignador general, debe decidir dinámicamente qué depósito usar). tcmalloc utiliza el almacenamiento local de subprocesos para evitar exactamente su problema y solo rara vez asigna desde el montón general y por lo tanto evita bloqueos. – Voo

Respuesta

10

Un pensamiento es mantener un asignador de memoria para cada subproceso. Asigne bloques de memoria bastante gruesos a cada asignador desde un conjunto de memoria global. Diseña tu algoritmo para asignar bloques gruesos desde direcciones de memoria adyacentes (más sobre esto más adelante).

Cuando el asignador para un subproceso dado tiene poca memoria, solicita más memoria del grupo de memoria global. Esta operación requiere un bloqueo, pero debe ocurrir con mucha menos frecuencia que en su caso actual. Cuando el asignador para un subproceso dado libera su último byte, devuelve toda la memoria para ese asignador al grupo de memoria global (supongamos que el subproceso finaliza).

Este enfoque tenderá a agotar la memoria antes que su enfoque actual (la memoria se puede reservar para un hilo que nunca lo necesita). La medida en que eso es un problema depende del perfil de creación/duración/destrucción de subprocesos de su (s) aplicación (es). Puede mitigar eso a expensas de una complejidad adicional, p. al introducir una señal de que un asignador de memoria para un subproceso dado está fuera de memoria, y el grupo global está exhausto, a lo que otros asignadores de memoria pueden responder al liberar algo de memoria.

Una ventaja de este esquema es que tenderá a eliminar false sharing, ya que la memoria para un subproceso dado tenderá a asignarse en espacios de direcciones contiguos.

En una nota lateral, si todavía no lo ha leído, sugiero IBM's Inside Memory Management artículo para cualquiera que implemente su propia gestión de memoria.

ACTUALIZACIÓN

Si el objetivo es que la asignación de memoria muy rápido optimizado para un entorno multi-hilo (en contraposición a aprender a hacerlo usted mismo), echar un vistazo a asignadores de memoria alternativos. Si el objetivo es aprender, tal vez revise su código fuente.

+0

Además de Hoarde, también existe [tcmalloc] (http://goog-perftools.sourceforge.net/doc/tcmalloc.html), que es básicamente el esquema propuesto de OP con lo obvio (heap local montón) y mejoras menos obvias. Cuando lo probé, fue más rápido que Hoarde, pero supongo que eso depende de los casos de uso. – Voo

+0

@Voo: para ser exactos, no es * heap * local de subprocesos, sino el * caché * local de subprocesos, que es un animal totalmente diferente. Por favor, lea mi pregunta actualizada. PD Me gustaría que compartas mi montón también, para ver cómo se compara. – valdo

+0

Gracias por sus sugerencias. Me gustó el punto sobre la tendencia a asignar bloques de memoria contiguos por subproceso con el fin de reducir la posibilidad de compartir falsamente. Sin embargo, en mi humilde opinión, el verdadero "punto ideal" es el asignador de ** caché ** por subproceso, no por subproceso * asignador *. Es extremadamente simple y probablemente rinda el mejor rendimiento posible para el uso de un solo subproceso, sin una penalización significativa para el rendimiento de subprocesos múltiples. Sí, las posibilidades de compartir falsamente son más altas, pero esto será de poca importancia en mi humilde opinión en los escenarios típicos. – valdo

0

Podría ser una buena idea leer Jeff Bonwicks papeles clásicos en el asignador de losa y vmem. El asignador de losa original suena un poco lo que estás haciendo. Aunque no es muy compatible con múltiples hilos, puede darte algunas ideas.

The Slab Allocator: An Object-Caching Kernel Memory Allocator

Luego se extendió el concepto con VMEM, que sin duda le dará algunas ideas, ya que tenía un comportamiento muy agradable en un entorno de múltiples CPU.

Magazines and Vmem: Extending the Slab Allocator to Many CPUs and Arbitrary Resources

Cuestiones relacionadas