Pregunta C ++ 11 introdujo un modelo de memoria estandarizada. Qué significa eso? ¿Y cómo afectará la programación en C ++?


C ++ 11 introdujo un modelo de memoria estandarizado, pero ¿qué significa eso exactamente? ¿Y cómo afectará la programación en C ++?

Este artículo (por Gavin Clarke quien cita Herb Sutter) dice que,

El modelo de memoria significa que el código C ++   ahora tiene una biblioteca estandarizada para llamar   independientemente de quién hizo el compilador   y en qué plataforma se está ejecutando.   Hay una forma estándar de controlar cómo   diferentes hilos hablan con el   memoria del procesador

"Cuando estás hablando de dividir   [código] a través de diferentes núcleos que es   en el estándar, estamos hablando de   el modelo de memoria. Vamos a   optimizarlo sin romper el   Según las suposiciones, las personas van   hacer en el código " Sutter dijo.

Bueno, yo puedo memorizar este y otros párrafos similares disponibles en línea (ya que tuve mi propio modelo de memoria desde su nacimiento: P) e incluso puedo publicar como respuesta a preguntas hechas por otros, pero para ser honesto, no entiendo exactamente esto.

Entonces, lo que básicamente quiero saber es que los programadores de C ++ solían desarrollar aplicaciones de subprocesos múltiples incluso antes, entonces, ¿qué importa si se trata de subprocesos POSIX, o subprocesos de Windows, o subprocesos de C ++ 11? ¿Cuales son los beneficios? Quiero entender los detalles de bajo nivel.

También me da la sensación de que el modelo de memoria C ++ 11 está relacionado de algún modo con el soporte multihilo de C ++ 11, ya que a menudo los veo juntos. Si es así, ¿cómo exactamente? ¿Por qué deberían estar relacionados?

Como no sé cómo funciona el funcionamiento interno de multihilo y qué modelo de memoria significa en general, ayúdame a comprender estos conceptos. :-)


1550
2018-06-11 23:30


origen


Respuestas:


Primero, debes aprender a pensar como un abogado de idiomas.

La especificación C ++ no hace referencia a ningún compilador, sistema operativo o CPU en particular. Hace referencia a un máquina abstracta eso es una generalización de los sistemas reales. En el mundo de los Juristas de Idiomas, el trabajo del programador es escribir código para la máquina abstracta; el trabajo del compilador es actualizar ese código en una máquina concreta. Al codificar de forma rígida las especificaciones, puede estar seguro de que su código se compilará y ejecutará sin modificaciones en cualquier sistema con un compilador C ++ compatible, ya sea hoy o 50 años a partir de ahora.

La máquina abstracta en la especificación C ++ 98 / C ++ 03 es fundamentalmente de un solo hilo. Por lo tanto, no es posible escribir código C ++ de subprocesos múltiples que sea "totalmente portátil" con respecto a la especificación. La especificación ni siquiera dice nada sobre el atomicidad de cargas de memoria y tiendas o la orden en el que pueden ocurrir cargas y tiendas, no importa cosas como mutexes.

Por supuesto, puede escribir código de subprocesos múltiples en la práctica para determinados sistemas concretos, como pthreads o Windows. Pero no hay estándarforma de escribir código multiproceso para C ++ 98 / C ++ 03.

La máquina abstracta en C ++ 11 tiene múltiples hilos por diseño. También tiene un bien definido modelo de memoria; es decir, dice lo que el compilador puede y no puede hacer cuando se trata de acceder a la memoria.

Considere el siguiente ejemplo, donde dos subprocesos acceden simultáneamente a un par de variables globales:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

¿Qué podría hilo de salida 2?

En C ++ 98 / C ++ 03, esto ni siquiera es un comportamiento indefinido; la pregunta en sí es sin sentido porque el estándar no contempla nada llamado "hilo".

Bajo C ++ 11, el resultado es Comportamiento no definido, porque las cargas y las tiendas no necesitan ser atómicas en general. Lo cual puede no parecer una gran mejora ... Y por sí mismo, no lo es.

Pero con C ++ 11, puedes escribir esto:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Ahora las cosas se ponen mucho más interesantes. En primer lugar, el comportamiento aquí es definido. El hilo 2 ahora podría imprimir 0 0 (si se ejecuta antes del subproceso 1), 37 17 (si se ejecuta después del subproceso 1), o 0 17 (si se ejecuta después de que el Tema 1 se asigna a x pero antes se asigna a y).

Lo que no puede imprimir es 37 0, porque el modo predeterminado para cargas / tiendas atómicas en C ++ 11 es hacer cumplir consistencia secuencial. Esto solo significa que todas las cargas y las tiendas deben ser "como si" sucedieran en el orden en que las escribió dentro de cada subproceso, mientras que las operaciones entre subprocesos se pueden intercalar, pero el sistema le gusta. Entonces, el comportamiento predeterminado de atomics proporciona ambos atomicidad y ordenando para cargas y tiendas.

Ahora, en una CPU moderna, asegurar la consistencia secuencial puede ser costoso. En particular, es probable que el compilador emita barreras de memoria completas entre cada acceso aquí. Pero si su algoritmo puede tolerar cargas y tiendas fuera de servicio; es decir, si requiere atomicidad pero no ordena; es decir, si puede tolerar 37 0 como salida de este programa, entonces puedes escribir esto:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

Cuanto más moderna sea la CPU, más probable es que sea más rápida que el ejemplo anterior.

Finalmente, si solo necesita mantener determinadas cargas y tiendas en orden, puede escribir:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Esto nos lleva de vuelta a las cargas y tiendas ordenadas, por lo que 37 0 ya no es un resultado posible, pero lo hace con una sobrecarga mínima. (En este ejemplo trivial, el resultado es igual a la consistencia secuencial en toda regla, en un programa más grande, no lo sería).

Por supuesto, si las únicas salidas que quieres ver son 0 0 o 37 17, puedes simplemente envolver un mutex alrededor del código original. Pero si ha leído hasta aquí, apuesto a que ya sabe cómo funciona, y esta respuesta ya es más larga de lo que esperaba :-).

Entonces, línea de fondo. Los mutex son geniales y C ++ 11 los estandariza. Pero a veces, por motivos de rendimiento, desea primitivas de nivel inferior (p. Ej., El clásico patrón de bloqueo doblemente verificado) El nuevo estándar proporciona gadgets de alto nivel como mutexes y variables de condición, y también proporciona gadgets de bajo nivel como tipos atómicos y los diversos sabores de la barrera de la memoria. Ahora puede escribir rutinas concurrentes sofisticadas de alto rendimiento completamente dentro del lenguaje especificado por el estándar, y puede estar seguro de que su código se compilará y se ejecutará sin cambios en los sistemas actuales y futuros.

Aunque, para ser sincero, a menos que sea un experto y trabaje con un código grave de bajo nivel, probablemente debería apegarse a mutexes y variables de condición. Eso es lo que pretendo hacer.

Para más sobre esto, ver esta publicación en el blog.


1797
2018-06-12 00:23



Daré la analogía con la que entiendo los modelos de consistencia de la memoria (o modelos de memoria, para abreviar). Está inspirado en el papel seminal de Leslie Lamport "Tiempo, relojes y el ordenamiento de eventos en un sistema distribuido". La analogía es adecuada y tiene una importancia fundamental, pero puede ser excesiva para muchas personas. Sin embargo, espero que proporcione una imagen mental (una representación pictórica) que facilite el razonamiento sobre los modelos de coherencia de la memoria.

Veamos los historiales de todas las ubicaciones de memoria en un diagrama de espacio-tiempo en el que el eje horizontal representa el espacio de direcciones (es decir, cada ubicación de memoria está representada por un punto en ese eje) y el eje vertical representa el tiempo (veremos que en general, no hay una noción universal de tiempo). El historial de valores que posee cada ubicación de memoria se representa, por lo tanto, mediante una columna vertical en esa dirección de memoria. Cada cambio de valor se debe a que uno de los hilos está escribiendo un nuevo valor en esa ubicación. Por un imagen de memoria, nos referiremos a la suma / combinación de valores de todas las ubicaciones de memoria observables en un momento particular por un hilo en particular.

Citando de "Un manual sobre coherencia de memoria y coherencia de caché"

El modelo de memoria intuitiva (y más restrictiva) es la consistencia secuencial (SC) en la que una ejecución multiproceso debe parecerse a un entrelazado de las ejecuciones secuenciales de cada subproceso constituyente, como si los subprocesos se multiplexaran en tiempo en un procesador de núcleo único.

Esa orden de memoria global puede variar de una ejecución del programa a otra y puede no conocerse de antemano. La característica de SC es el conjunto de sectores horizontales en el diagrama de dirección-espacio-tiempo que representa planos de simultaneidad (es decir, imágenes de memoria). En un plano dado, todos sus eventos (o valores de memoria) son simultáneos. Hay una noción de Tiempo Absoluto, en el que todos los hilos coinciden en qué valores de memoria son simultáneos. En SC, en todo momento, solo hay una imagen de memoria compartida por todos los hilos. Es decir, en cada instante, todos los procesadores acuerdan la imagen de la memoria (es decir, el contenido agregado de la memoria). Esto no solo implica que todos los hilos vean la misma secuencia de valores para todas las ubicaciones de memoria, sino también que todos los procesadores observen el mismo combinaciones de valores de todas las variables Esto es lo mismo que decir que todas las secuencias recuerdan todas las operaciones de memoria (en todas las ubicaciones de memoria) en el mismo orden total.

En los modelos de memoria relajada, cada subproceso dividirá dirección-espacio-tiempo de su propia manera, la única restricción es que las divisiones de cada subproceso no se cruzarán porque todos los subprocesos deben coincidir en el historial de cada ubicación de memoria individual (por supuesto , las rodajas de diferentes hilos pueden, y se cruzarán). No hay una forma universal de dividirlo (no hay foliación privilegiada de espacio-tiempo de direcciones). Las rebanadas no tienen que ser planas (o lineales). Pueden ser curvos y esto es lo que puede hacer que un hilo lea valores escritos por otro hilo fuera del orden en que fueron escritos. Las historias de diferentes ubicaciones de memoria pueden deslizarse (o estirarse) arbitrariamente entre sí. cuando se ve por un hilo en particular. Cada hilo tendrá un sentido diferente de qué eventos (o, de forma equivalente, valores de memoria) son simultáneos. El conjunto de eventos (o valores de memoria) que son simultáneos a un hilo no son simultáneos a otro. Por lo tanto, en un modelo de memoria relajado, todos los subprocesos siguen observando el mismo historial (es decir, secuencia de valores) para cada ubicación de memoria. Pero pueden observar diferentes imágenes de memoria (es decir, combinaciones de valores de todas las ubicaciones de memoria). Incluso si dos ubicaciones de memoria diferentes están escritas por el mismo hilo en secuencia, los dos nuevos valores escritos pueden observarse en orden diferente por otros hilos.

[Imagen de Wikipedia] Picture from Wikipedia

Lectores familiarizados con Einstein Teoría especial de la relatividad notará a lo que me refiero. Traducir las palabras de Minkowski al reino de los modelos de memoria: el espacio de direcciones y el tiempo son sombras del espacio-tiempo de la dirección. En este caso, cada observador (es decir, hilo) proyectará sombras de eventos (es decir, memoria almacena / cargas) en su propia línea de mundo (es decir, su eje de tiempo) y su propio plano de simultaneidad (su eje dirección-espacio) . Los hilos en el modelo de memoria C ++ 11 corresponden a observadores que se mueven entre sí en relatividad especial. La consistencia secuencial corresponde a Espacio-tiempo galileano (es decir, todos los observadores acuerdan un orden absoluto de eventos y un sentido global de simultaneidad).

La semejanza entre los modelos de memoria y la relatividad especial surge del hecho de que ambos definen un conjunto de eventos parcialmente ordenados, a menudo llamado conjunto causal. Algunos eventos (es decir, almacenes de memoria) pueden afectar (pero no verse afectados) por otros eventos. Un hilo de C ++ 11 (u observador en física) no es más que una cadena (es decir, un conjunto totalmente ordenado) de eventos (por ejemplo, cargas de memoria y almacena en direcciones posiblemente diferentes).

En relatividad, se restaura cierto orden a la imagen aparentemente caótica de eventos parcialmente ordenados, ya que el único orden temporal en el que todos los observadores coinciden es en ordenar entre eventos "temporales" (es decir, aquellos eventos que en principio son conectables por cualquier partícula que vaya más lenta que la velocidad de la luz en el vacío). Solo los eventos relacionados con el tiempo se ordenan invariablemente. Tiempo en Física, Craig Callender.

En el modelo de memoria C ++ 11, se usa un mecanismo similar (el modelo de consistencia de adquisición-liberación) para establecer estos relaciones de causalidad local.

Para proporcionar una definición de consistencia de memoria y una motivación para abandonar SC, voy a citar de "Un manual sobre coherencia de memoria y coherencia de caché"

Para una máquina de memoria compartida, el modelo de coherencia de memoria define el comportamiento arquitectónico visible de su sistema de memoria. El criterio de corrección para un núcleo de procesador único divide el comportamiento entre "un resultado correcto"Y"muchas alternativas incorrectas". Esto se debe a que la arquitectura del procesador exige que la ejecución de un hilo transforma un estado de entrada dado en un único estado de salida bien definido, incluso en un núcleo fuera de servicio. Los modelos de coherencia de memoria compartida, sin embargo, se refieren a las cargas y almacenamientos de múltiples hilos y generalmente permiten muchas ejecuciones correctas mientras que rechaza muchas (más) incorrectas. La posibilidad de múltiples ejecuciones correctas se debe a que el ISA permite que se ejecuten varios subprocesos al mismo tiempo, a menudo con muchos entrelazados legales posibles de instrucciones de diferentes subprocesos.

Relajado o débiles Los modelos de coherencia de memoria están motivados por el hecho de que la mayoría de los ordenamientos de memoria en modelos sólidos son innecesarios. Si un hilo actualiza diez elementos de datos y luego un indicador de sincronización, a los programadores generalmente no les importa si los elementos de datos se actualizan uno con respecto al otro, sino que todos los elementos de datos se actualizan antes de actualizar el indicador (generalmente implementado usando instrucciones FENCE ) Los modelos relajados buscan capturar esta mayor flexibilidad de pedidos y preservar solo las órdenes que los programadores "exigir"Para obtener un mayor rendimiento y la corrección de SC. Por ejemplo, en ciertas arquitecturas, cada núcleo utiliza búferes de escritura FIFO para mantener los resultados de las tiendas comprometidas (retiradas) antes de escribir los resultados en las memorias caché. Esta optimización mejora el rendimiento pero viola SC. El búfer de escritura oculta la latencia de dar servicio a un error de tienda. Debido a que las tiendas son comunes, poder evitar el estancamiento en la mayoría de ellas es un beneficio importante. Para un procesador de un solo núcleo, un búfer de escritura puede hacerse arquitectónicamente invisible asegurando que una carga en la dirección A devuelve el valor de la tienda más reciente a A incluso si uno o más almacenamientos en A están en el búfer de escritura. Esto normalmente se realiza omitiendo el valor de la tienda más reciente en A a la carga desde A, donde "más reciente" está determinado por el orden del programa, o bloqueando una carga de A si una tienda en A está en el búfer de escritura . Cuando se usan múltiples núcleos, cada uno tendrá su propio bypass en el búfer de escritura. Sin búferes de escritura, el hardware es SC, pero con búferes de escritura, no lo es, lo que hace que los búferes de escritura sean arquitectónicamente visibles en un procesador multinúcleo.

El reordenamiento de tienda puede ocurrir si un núcleo tiene un búfer de escritura que no es FIFO que permite que las tiendas salgan en un orden diferente al orden en el que ingresaron. Esto podría ocurrir si la primera tienda falla en la memoria caché mientras la segunda visita o si la segunda tienda puede fusionarse con una tienda anterior (es decir, antes de la primera tienda). El reordenamiento de carga de carga también puede ocurrir en núcleos programados dinámicamente que ejecutan las instrucciones fuera del orden del programa. Eso puede comportarse de la misma manera que reordenando tiendas en otro núcleo (¿se puede encontrar un ejemplo de entrelazado entre dos hilos?). Reordenar una carga anterior con una tienda posterior (un reordenamiento de la tienda de carga) puede causar muchos comportamientos incorrectos, como cargar un valor después de liberar el bloqueo que lo protege (si la tienda es la operación de desbloqueo). Tenga en cuenta que los reordenamientos de la carga de la tienda también pueden surgir debido a la derivación local en el búfer de escritura FIFO comúnmente implementado, incluso con un núcleo que ejecuta todas las instrucciones en orden de programa.

Debido a que la coherencia del caché y la coherencia de la memoria a veces se confunden, es instructivo tener también esta cita:

A diferencia de la consistencia, coherencia de caché no es visible para el software ni es obligatorio. Coherence busca hacer que las memorias caché de un sistema de memoria compartida sean funcionalmente invisibles como las memorias caché en un sistema de núcleo único. La coherencia correcta garantiza que un programador no pueda determinar si un sistema tiene cachés y dónde lo tiene al analizar los resultados de las cargas y las tiendas. Esto se debe a que la coherencia correcta garantiza que las memorias caché nunca habiliten nuevas o diferentes funcional comportamiento (los programadores aún pueden inferir una estructura de caché probable usando sincronización información). El objetivo principal de los protocolos de coherencia de caché es mantener el invariante de único escritor-múltiples lectores (SWMR) para cada ubicación de memoria.   Una distinción importante entre coherencia y coherencia es que la coherencia se especifica en una base de ubicación por memoria, mientras que la coherencia se especifica con respecto a todas ubicaciones de memoria.

Continuando con nuestra imagen mental, el invariante SWMR corresponde al requisito físico de que haya como máximo una partícula ubicada en cualquier ubicación, pero puede haber un número ilimitado de observadores en cualquier ubicación.


279
2017-08-29 20:42



Esta es ahora una pregunta de varios años, pero al ser muy popular, vale la pena mencionar un recurso fantástico para aprender sobre el modelo de memoria C ++ 11. No veo sentido resumir su charla para hacer de esta una respuesta más completa, pero teniendo en cuenta que es el tipo que realmente escribió el estándar, creo que vale la pena ver la charla.

Herb Sutter tiene una charla de tres horas sobre el modelo de memoria C ++ 11 titulado "Atomic <> Weapons", disponible en el sitio de Channel9 - parte 1 y parte 2. La charla es bastante técnica y cubre los siguientes temas:

  1. Optimizaciones, razas y el modelo de memoria
  2. Pedido: qué: adquirir y liberar
  3. Pedido - Cómo: Mutexes, Atomics y / o Fences
  4. Otras restricciones sobre compiladores y hardware
  5. Código Gen y Rendimiento: x86 / x64, IA64, POWER, ARM
  6. Atomics relajada

La charla no detalla la API, sino el razonamiento, el trasfondo, el contexto y detrás de escena (¿sabías que la semántica relajada se agregó al estándar solo porque POWER y ARM no admiten la carga sincronizada de manera eficiente?).


79
2017-12-20 13:22



Significa que el estándar ahora define multi-threading, y define lo que sucede en el contexto de múltiples hilos. Por supuesto, las personas usaron implementaciones variadas, pero eso es como preguntar por qué deberíamos tener un std::string cuando todos podríamos estar usando una película string clase.

Cuando se habla de hilos POSIX o hilos de Windows, esto es un poco ilusorio ya que en realidad se trata de hilos x86, ya que es una función de hardware que se ejecuta simultáneamente. El modelo de memoria C ++ 0x hace garantías, ya sea que esté en x86, o ARM, o MIPS, o cualquier otra cosa que se te ocurra


67
2018-06-11 23:42



Para idiomas que no especifican un modelo de memoria, está escribiendo código para el idioma y el modelo de memoria especificado por la arquitectura del procesador. El procesador puede elegir reordenar accesos de memoria para el rendimiento. Asi que, si su programa tiene carreras de datos (una carrera de datos es cuando es posible que múltiples núcleos / hiperprocesos accedan a la misma memoria al mismo tiempo), entonces su programa no es multiplataforma debido a su dependencia del modelo de memoria del procesador. Puede consultar los manuales del software Intel o AMD para averiguar cómo los procesadores pueden reordenar los accesos a la memoria.

Muy importante, los bloqueos (y la semántica de simultaneidad con bloqueo) se implementan típicamente de forma cruzada ... Así que si está utilizando bloqueos estándar en un programa multiproceso sin carreras de datos, entonces usted no tiene que preocuparse por los modelos de memoria multiplataforma.

Curiosamente, los compiladores de Microsoft para C ++ tienen una semántica de adquisición / liberación de volátiles, que es una extensión de C ++ para lidiar con la falta de un modelo de memoria en C ++. http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx. Sin embargo, dado que Windows solo se ejecuta en x86 / x64, eso no dice mucho (los modelos de memoria Intel y AMD hacen que sea fácil y eficiente implementar la semántica de adquisición / liberación en un idioma).


49
2017-07-26 04:27



Si usa mutexes para proteger todos sus datos, no debería preocuparse. Los mutexes siempre han proporcionado suficientes garantías de orden y visibilidad.

Ahora, si usó algoritmos atómicos o sin bloqueo, debe pensar en el modelo de memoria. El modelo de memoria describe con precisión cuándo los atómicos proporcionan garantías de orden y visibilidad, y proporciona vallas portátiles para las garantías codificadas a mano.

Anteriormente, los átomos se realizarían utilizando intrínsecos del compilador o alguna biblioteca de nivel superior. Las cercas se habrían hecho usando instrucciones específicas de CPU (barreras de memoria).


22
2018-06-11 23:49