Cuando los cálculos limitados de ancho de banda de memoria se llevan a cabo en entornos de memoria compartida (por ejemplo, roscado a través de OpenMP, Pthreads, o TBB), hay un dilema de cómo garantizar que la memoria se distribuye correctamente a través de física de la memoria, de tal manera que cada hilo en su mayoría los accesos a memoria en una bus de memoria "local". Aunque las interfaces no son portátiles, la mayoría de los sistemas operativos tienen formas de afinidad hilo de conjunto (por ejemplo, pthread_setaffinity_np()
en muchos sistemas POSIX, sched_setaffinity()
en Linux, SetThreadAffinityMask()
en Windows). Hay también librerías como hwloc para determinar la jerarquía de memoria, pero, por desgracia, la mayoría de los sistemas operativos todavía no proporcionan formas de políticas de la memoria NUMA conjunto. Linux es una notable excepción, con libnumapermitiendo que la aplicación manipule la política de memoria y la migración de página en granularidad de página (en línea principal desde 2004, por lo tanto ampliamente disponible). Otros sistemas operativos esperan que los usuarios observen una política implícita de "primer contacto".
Trabajar con una política de "primer contacto" significa que la persona que llama debe crear y distribuir hilos con cualquier afinidad que planeen usar más tarde cuando escriban por primera vez en la memoria recién asignada. (Muy pocos sistemas están configurados de modo que malloc()
realmente encuentren páginas, solo promete encontrarlas cuando realmente tienen fallas, tal vez por diferentes subprocesos). Esto implica que la asignación que usa calloc()
o inicializa inmediatamente la memoria después de la asignación memset()
es dañina ya que tenderá a fallar toda la memoria en el bus de memoria del núcleo ejecutando el hilo asignación, lo que lleva a un ancho de banda de memoria peor de los casos cuando se accede a la memoria desde varios subprocesos. Lo mismo se aplica al new
operador de C ++ que insiste en inicializar muchas asignaciones nuevas (p. Ej.std::complex
) Algunas observaciones acerca de este entorno:
- Asignación puede hacerse "colectivo hilo", pero ahora la asignación se convierte en mezcla en el modelo de hilos que es indeseable para las bibliotecas que pueden tener que interactuar con los clientes utilizando diferentes modelos de subprocesamiento (tal vez cada uno con sus propios conjuntos de subprocesos).
- RAII se considera una parte importante de C ++ idiomático, pero parece ser activamente perjudicial para el rendimiento de la memoria en un entorno NUMA. La ubicación
new
se puede utilizar con la memoria asignada a través demalloc()
o desde rutinaslibnuma
, pero esto cambia el proceso de asignación (que creo que es necesario). - EDITAR: Mi declaración anterior sobre el operador
new
era incorrecta, puede soportar múltiples argumentos, vea la respuesta de Chetan. Creo que todavía existe la preocupación de que las bibliotecas o los contenedores STL utilicen una afinidad específica. Campos múltiples pueden ser empacados y puede ser un inconveniente para que, por ejemplo, unstd::vector
reasigna con el gestor de contexto correcto activo. - Cada subproceso puede asignar y criticar su propia memoria privada, pero luego la indexación en regiones vecinas es más complicada. (Considere un producto de matriz-vector escaso con una partición de fila de la matriz y los vectores; indexar la parte no propietaria de requiere una estructura de datos más complicada cuando no es contigua en la memoria virtual).
Se ninguna solución a la asignación de la NUMA / inicialización considerados idiomática? ¿He dejado de lado otras trampas críticas?
(No me refiero a mi C ejemplos ++ dar a entender un énfasis en ese idioma, sin embargo, el C ++ lenguaje codifica algunas decisiones sobre la gestión de memoria que un lenguaje como C no, por lo tanto no tiende a ser más resistencia cuando lo que sugiere que los programadores de C ++ hacen los cosas diferentes)
fuente
Esta respuesta es en respuesta a dos conceptos erróneos relacionados con C ++ en la pregunta.
No es una respuesta directa a los problemas de múltiples núcleos que mencionas. Simplemente respondiendo a los comentarios que clasifican a los programadores de C ++ como fanáticos de C ++ para que se mantenga la reputación;).
Para apuntar 1. C ++ "nuevo" o asignación de pila no insiste en inicializar nuevos objetos, ya sean POD o no. El constructor predeterminado de la clase, según lo definido por el usuario, tiene esa responsabilidad. El primer código a continuación muestra la basura impresa si la clase es POD o no.
Para el punto 2. C ++ permite sobrecarga "nuevo" con múltiples argumentos. El segundo código de abajo muestra un caso tal para la asignación de objetos individuales. Debe dar una idea y tal vez sea útil para la situación que tiene. El operador new [] también se puede modificar adecuadamente.
// Código para el punto 1.
El compilador 11.1 de Intel muestra esta salida (que por supuesto es memoria no inicializada señalada por "a").
// Código para el punto 2.
fuente
std::complex
la que se inicializan de forma explícita.std::complex
?En deal.II tenemos la infraestructura de software para paralelizar el ensamblaje en cada celda en múltiples núcleos utilizando Threading Building Blocks (en esencia, tiene una tarea por celda y necesita programar estas tareas en los procesadores disponibles, no es así implementado pero es la idea general). El problema es que para la integración local necesita una cantidad de objetos temporales (reutilizables) y debe proporcionar al menos la cantidad de tareas que pueden ejecutarse en paralelo. Vemos una aceleración deficiente, presumiblemente porque cuando una tarea se coloca en un procesador, toma uno de los objetos reutilizables que generalmente estarán en la caché de otro núcleo. Teníamos dos preguntas:
(i) ¿Es esta realmente la razón? Cuando ejecutamos el programa bajo cachegrind, veo que estoy usando básicamente la misma cantidad de instrucciones que cuando ejecuto el programa en un solo hilo, pero el tiempo de ejecución total acumulado en todos los hilos es mucho mayor que el de un solo hilo. ¿Es realmente porque continuamente fallo el caché?
(ii) ¿Cómo puedo averiguar dónde estoy, dónde están cada uno de los objetos reutilizables y qué objeto reutilizable necesitaría para acceder al que está caliente en el caché de mi núcleo actual?
En última instancia, no hemos encontrado respuestas a ninguna de estas soluciones y después de un par de trabajos decidimos que nos faltaban las herramientas para investigar y resolver estos problemas. Sí sé, al menos en principio, resolver el problema (ii) (es decir, usar objetos locales de subprocesos, suponiendo que los subprocesos permanecen anclados a los núcleos del procesador, otra conjetura que no es trivial para probar), pero no tengo herramientas para probar el problema (yo).
Por lo tanto, desde nuestro punto de vista, se trata de la NUMA es todavía una cuestión sin resolver.
fuente
Más allá de hwloc, existen algunas herramientas que pueden informar sobre el entorno de memoria de un clúster HPC y que pueden usarse para establecer una variedad de configuraciones NUMA.
Recomendaría LIKWID como una de esas herramientas, ya que evita un enfoque basado en código que le permite, por ejemplo, anclar un proceso a un núcleo. Este enfoque de herramientas para abordar la configuración de memoria específica de la máquina ayudará a garantizar la portabilidad de su código entre los clústeres.
Puede encontrar una breve presentación del ISC'13 " LIKWID - Lightweight Performance Tools " y los autores han publicado un documento sobre Arxiv " Mejores prácticas para la ingeniería de rendimiento asistida por HPM en un procesador multinúcleo moderno ". Este documento describe un enfoque para interpretar los datos de los contadores de hardware para desarrollar código de rendimiento específico para la arquitectura de su máquina y la topología de la memoria.
fuente