C++11 introdujo un modelo de memoria estandarizado. ¿Qué significa? ¿Y cómo va a afectar la programación en C++?


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

Este artículo (por Gavin Clarke que 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 está funcionando. 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 siguiendo suposiciones la gente va para hacer en el código, " Sutter dijo.

Bueno, puedo memorizar este y párrafos similares disponibles en línea (ya que he tenido mi propio modelo de memoria desde mi 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 multihilo incluso antes, así que ¿qué importa si se trata de hilos POSIX, o hilos de Windows, o hilos de C++11? ¿Cuáles son los beneficios? Quiero entender los detalles de bajo nivel.

También tengo la sensación de que el modelo de memoria C++11 está relacionado de alguna manera con el soporte de subprocesos múltiples de C++11, ya que a menudo veo estos dos junto. Si lo es, ¿cómo exactamente? ¿Por qué deberían estar relacionados?

Como no se como funcionan las funciones internas de multi-threading, y lo que significa el modelo de memoria en general, por favor ayúdame a entender estos conceptos. :-)

Author: KIN, 2011-06-12

6 answers

Primero, tienes que aprender a pensar como un Abogado de Idiomas.

La especificación de C++ no hace referencia a ningún compilador, sistema operativo o CPU en particular. Hace referencia a una máquina abstracta que es una generalización de sistemas reales. En el mundo del Abogado Lingüístico, 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 rígidamente a la especificación, puede ser asegúrese de que su código se compilará y ejecutará sin modificaciones en cualquier sistema con un compilador C++ compatible, ya sea hoy o dentro de 50 años.

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

Por supuesto, puede escribir código multihilo en la práctica para sistemas concretos particulares like como pthreads o Windows. Pero no existe una forma estándar de escribir código multihilo para C++98/C++03.

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

Considere el siguiente ejemplo, donde un par de variables globales se accede simultáneamente por dos hilos:

           Global
           int x, y;

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

¿Qué podría Thread 2 salida?

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

Bajo C++11, el resultado es un Comportamiento Indefinido, porque las cargas y los almacenes no necesitan ser atómicos en general. Que puede no parecer como una gran mejora... Y por sí solo, 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 Hilo 1), 37 17 (si se ejecuta después del Hilo 1), o 0 17 (si se ejecuta después de que el Hilo 1 asigne a x pero antes de que asigne a y).

Lo que no puede imprimir es 37 0, porque el modo predeterminado para cargas/almacenes atómicos en C++11 es aplicar consistencia secuencial. Esto solo significa que todas las cargas y almacenes deben ser" como si " sucedieran en el orden en que las escribió dentro de cada hilo, mientras que las operaciones entre hilos pueden intercalarse como quiera el sistema. Por lo tanto, el comportamiento predeterminado de atomics proporciona atomicidad y ordenando para cargas y tiendas.

Ahora, en una CPU moderna, garantizar la consistencia secuencial puede ser costoso. En particular, es probable que el compilador emita barreras de memoria en toda regla entre cada acceso aquí. Pero si su algoritmo puede tolerar cargas y almacenes fuera de orden; es decir, si requiere atomicidad pero no orden; es decir, si puede tolerar 37 0 como salida de este programa, entonces puede 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 será que sea más rápida que el ejemplo anterior.

Finalmente, si solo necesita mantener determinadas cargas y almacenes 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 almacenes ordenados {por lo que 37 0 ya no es una salida posible but pero lo hace con una sobrecarga mínima. (En este ejemplo trivial, el resultado es el mismo que la consistencia secuencial completa; en un programa más grande, no lo sería.)

Por supuesto, si las únicas salidas que desea ver son 0 0 o 37 17, puede simplemente envolver un mutex alrededor del código original. Pero si has leído hasta aquí, apuesto a que ya sabes cómo funciona, y esta respuesta ya es más larga que yo destinado :-).

Entonces, en resumen. Los mutexes son geniales, y C++11 los estandariza. Pero a veces, por razones de rendimiento, desea primitivas de nivel inferior (por ejemplo, el clásico patrón de bloqueo doble comprobado). 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 memoria. Así que ahora puede escribir rutinas concurrentes sofisticadas y de alto rendimiento por completo dentro del lenguaje especificado por el estándar, y puede estar seguro de que su código se compilará y se ejecutará sin cambios tanto en los sistemas de hoy como en los de mañana.

Aunque para ser franco, a menos que sea un experto y trabaje en algún código serio de bajo nivel, probablemente debería atenerse a los mutexes y las variables de condición. Eso es lo que pretendo hacer.

Para más información sobre esto, ver esta entrada del blog.

 1839
Author: Nemo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-06 03:35:04

Solo daré la analogía con la que entiendo los modelos de consistencia de memoria (o modelos de memoria, para abreviar). Está inspirado en el artículo seminal de Leslie Lamport "Time, Clocks, and the Ordering of Events in a Distributed System". La analogía es apropiada y tiene un significado fundamental, pero puede ser exagerada para muchas personas. Sin embargo, espero que proporcione una imagen mental (una representación pictórica) que facilite el razonamiento sobre los modelos de consistencia de la memoria.

Veamos el historias de todas las ubicaciones de memoria en un diagrama 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). Por lo tanto, el historial de valores de cada ubicación de memoria está representado por una columna vertical en esa dirección de memoria. Cada cambio de valor se debe a que uno de los hilos escribe un nuevo valor ubicación. Por a imagen de memoria, nos referiremos al agregado / combinación de valores de todas las ubicaciones de memoria observables en un momento determinado por un hilo en particular.

Citando de "A Primer on Memory Consistency and Cache Coherence"

El modelo de memoria intuitivo (y más restrictivo) es la consistencia secuencial (SC) en la que una ejecución multiproceso debería parecerse a un entrelazado de la secuencia ejecuciones de cada hilo constituyente, como si los hilos fueran multiplexados en tiempo en un procesador de un solo núcleo.

Ese orden de memoria global puede variar de una ejecución del programa a otra y puede no ser conocido de antemano. El rasgo característico de SC es el conjunto de cortes horizontales en el diagrama 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 un noción de Tiempo Absoluto, en el que todos los hilos están de acuerdo en qué valores de memoria son simultáneos. En SC, en cada instante de tiempo, solo hay una imagen de memoria compartida por todos los hilos. Es decir, en cada instante de tiempo, todos los procesadores están de acuerdo en la imagen de la memoria (es decir, el contenido agregado de la memoria). Esto no solo implica que todos los subprocesos vean la misma secuencia de valores para todas las ubicaciones de memoria, sino también que todos los procesadores observen las mismas combinaciones de valores de todos variable. Esto es lo mismo que decir que todas las operaciones de memoria (en todas las ubicaciones de memoria) se observan en el mismo orden total por todos los hilos.

En los modelos de memoria relajada, cada hilo cortará dirección-espacio-tiempo a su manera, la única restricción es que los cortes de cada hilo no se cruzarán entre sí porque todos los hilos deben estar de acuerdo en el historial de cada ubicación de memoria individual (por supuesto, los cortes de diferentes hilos pueden, y lo harán, cruzarse entre sí). No hay universal forma de cortarlo (sin foliación privilegiada de dirección-espacio-tiempo). Los cortes no tienen que ser planos (o lineales). Pueden ser curvados y esto es lo que puede hacer que un hilo lea valores escritos por otro hilo fuera del orden en que fueron escritos. Los historiales de diferentes ubicaciones de memoria pueden deslizarse (o estirarse) arbitrariamente en relación entre sí cuando se ve por cualquier hilo en particular. Cada hilo tendrá un sentido diferente de qué eventos (o, equivalentemente, valores de memoria) son simultáneas. 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 hilos 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 valores recién escritos pueden observarse en diferentes ordenar por otros hilos.

[Imagen de Wikipedia] Imagen de Wikipedia

Los lectores familiarizados con la Teoría Especial de la Relatividad de Einstein se darán cuenta de lo que estoy aludiendo. Traduciendo las palabras de Minkowski al reino de los modelos de memoria: el espacio de direcciones y el tiempo son sombras de dirección-espacio-tiempo. En este caso, cada observador (es decir, thread) proyectará sombras de eventos (es decir, almacenes/cargas de memoria) 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 están moviendo uno con respecto al otro en la relatividad especial. La consistencia secuencial corresponde alespacio-tiempo galileo (es decir, todos los observadores están de acuerdo en un orden absoluto de eventos y un sentido global de simultaneidad).

La semejanza entre los modelos de memoria y la relatividad especial se deriva del hecho de que ambos definen un conjunto parcialmente ordenado de eventos, a menudo llamado causal establecer. Algunos eventos (es decir, almacenes de memoria) pueden afectar (pero no ser afectados por) otros eventos. Un subproceso 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 almacenes a direcciones posiblemente diferentes).

En la relatividad, se restaura algún orden a la imagen aparentemente caótica de eventos parcialmente ordenados, ya que el único orden temporal en el que todos los observadores están de acuerdo es el orden entre eventos "temporales" (es decir, aquellos eventos que son en principio conectable por cualquier partícula que va más lento que la velocidad de la luz en el vacío). Solo los eventos relacionados con el tiempo están ordenados invariablemente. Time in Physics, Craig Callender .

En el modelo de memoria C++11, se utiliza un mecanismo similar (el modelo de consistencia acquire-release) para establecer estos relaciones de causalidad local.

Para proporcionar una definición de consistencia de memoria y una motivación para abandonar SC, citaré de " A Primer on Consistencia de Memoria y Coherencia de Caché "

Para una máquina de memoria compartida, el modelo de consistencia de memoria define el comportamiento arquitectónicamente visible de su sistema de memoria. El criterio de corrección para un solo procesador el comportamiento de las particiones del núcleo entre " un resultado correcto" y " muchas alternativas incorrectas". Esto se debe a que la arquitectura del procesador exige que la ejecución de un subproceso transforme un estado de entrada dado en una única salida bien definida estado, incluso en un núcleo fuera de orden. Los modelos de consistencia de memoria compartida, sin embargo, se refieren a las cargas y almacenes de múltiples subprocesos y generalmente permiten muchas ejecuciones correctas mientras que no permiten muchas (más) ejecuciones incorrectas. La posibilidad de múltiples ejecuciones correctas se debe a que la ISA permite que múltiples subprocesos se ejecuten simultáneamente, a menudo con muchas posibles intercalaciones legales de instrucciones de diferentes subprocesos.

Relajado o débil los modelos de consistencia de memoria están motivados por el hecho de que la mayoría de los pedidos de memoria en modelos fuertes 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 en orden con respecto a los demás, sino solo que todos los elementos de datos se actualizan antes de que se actualice el indicador (generalmente implementado usando instrucciones FENCE). Los modelos relajados buscan capturar esta mayor flexibilidad de pedidos y preservar solo los pedidos que los programadores" requieren " para obtener un mayor rendimiento y corrección de SC. Por ejemplo, en ciertas arquitecturas, los búferes de escritura FIFO son utilizados por cada núcleo para mantener los resultados de las tiendas confirmadas (retiradas) antes de escribir los resultados en las cachés. Esta optimización mejora el rendimiento pero viola SC. El búfer de escritura oculta la latencia del mantenimiento de un error de tienda. Debido a que las tiendas son comunes, poder evitar estancarse en la mayoría de ellas es un beneficio importante. Para un solo núcleo procesador, un búfer de escritura se puede hacer arquitectónicamente invisible asegurando que una carga para direccionar A devuelve el valor del almacén más reciente a A incluso si uno o más almacenes a A están en el búfer de escritura. Esto se hace típicamente omitiendo el valor de la tienda más reciente a A a la carga de A, donde "más reciente" está determinado por el orden del programa, o estancando una carga de A si una tienda a A está en el búfer de escritura. Cuando se utilizan varios núcleos, cada uno tendrá su propia escritura de derivación búfer. Sin búferes de escritura, el hardware es SC, pero con búferes de escritura, no lo es, haciendo que los búferes de escritura sean arquitectónicamente visibles en un procesador multinúcleo.

Store-el reordenamiento de la tienda puede ocurrir si un núcleo tiene un búfer de escritura no FIFO que permite que las tiendas salgan en un orden diferente al orden en el que ingresaron. Esto puede ocurrir si la primera tienda falla en la caché mientras que la segunda visita o si la segunda tienda puede fusionarse con una tienda anterior (es decir, antes de la primera tienda). Carga: el reordenamiento de carga también puede ocurrir en núcleos programados dinámicamente que ejecutan instrucciones fuera del orden del programa. Eso puede comportarse de la misma manera que reordenar las tiendas en otro núcleo (¿Puedes crear un ejemplo intercalando entre dos hilos?). Reordenar una carga anterior con una tienda posterior (reordenar carga-tienda) 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 carga de almacén pueden también surgen debido al bypass 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 de la caché y la consistencia 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 necesario. Coherence busca hacer que los cachés de un sistema de memoria compartida sean tan cachés en un sistema de un solo núcleo. La coherencia correcta asegura que un programador no pueda determinar si un sistema tiene cachés y dónde mediante el análisis de los resultados de las cargas y los almacenes. Esto se debe a que la coherencia correcta garantiza que las cachés nunca habiliten nuevas o diferentes funcional comportamiento (los programadores todavía pueden ser capaces de inferir la estructura de caché probable utilizando calendario información). El propósito principal de los protocolos de coherencia de caché es mantener el invariante de escritor único-lector múltiple (SWMR) para cada ubicación de memoria. Una distinción importante entre coherencia y consistencia es que la coherencia se especifica en un base de ubicación por memoria, considerando que la coherencia se especifica con respecto a todos 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 lugar, pero allí puede ser un número ilimitado de observadores de cualquier lugar.

 291
Author: Ahmed Nassar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-08 14:44:04

Esta es ahora una pregunta de varios años, pero siendo muy popular, vale la pena mencionar un recurso fantástico para aprender sobre el modelo de memoria C++11. No veo ningún punto en resumir su charla para hacer esta otra respuesta completa, pero dado que este 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, Carreras y el Modelo de Memoria
  2. Ordenar – Qué: Adquirir y Liberar
  3. Ordering-How: Mutexes, Atomics, and/or Fences{[12]]}
  4. Otras restricciones sobre Compiladores y Hardware
  5. Generación de código y rendimiento: x86/x64, IA64, POTENCIA, BRAZO
  6. Átomos relajados

La charla no se desarrolla en la API, sino más bien en la razonamiento, antecedentes, bajo el capó y detrás de escena (¿sabía que la semántica relajada se agregó al estándar solo porque la POTENCIA y el BRAZO no soportan la carga sincronizada de manera eficiente?).

 85
Author: eran,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-05 23:17:25

Significa que el estándar ahora define multi-threading, y define lo que sucede en el contexto de múltiples subprocesos. Por supuesto, la gente usó implementaciones variables, pero eso es como preguntar por qué deberíamos tener un std::string cuando todos podríamos estar usando una clase string hecha en casa.

Cuando estás hablando de hilos POSIX o hilos de Windows, entonces esto es un poco de una ilusión como en realidad estás hablando de hilos x86, ya que es una función de hardware para ejecutarse simultáneamente. La memoria C++0x el modelo hace garantías, ya sea que estés en x86, o ARM, o MIPS , o cualquier otra cosa que se te ocurra.

 69
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-05 23:06:03

Para los lenguajes que no especifican un modelo de memoria, está escribiendo código para el lenguaje y el modelo de memoria especificado por la arquitectura del procesador. El procesador puede optar por volver a ordenar los accesos a la memoria para el rendimiento. Por lo tanto, si su programa tiene carreras de datos (una carrera de datos es cuando es posible que varios núcleos / hiper-hilos accedan a la misma memoria simultáneamente), entonces su programa no es multiplataforma debido a su dependencia del modelo de memoria del procesador. Usted puede referir a los manuales de software de Intel o AMD para averiguar cómo los procesadores pueden reordenar los accesos a la memoria.

Es muy importante que los bloqueos (y la semántica de concurrencia con el bloqueo) se implementen típicamente de forma multiplataforma... Entonces, si está utilizando bloqueos estándar en un programa multiproceso sin carreras de datos, entonces no tiene que preocuparse por los modelos de memoria multiplataforma.

Curiosamente, los compiladores de Microsoft para C++ tienen semántica de adquisición / liberación para volatile, que es un C++ extensión para hacer frente a 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 se ejecuta solo en x86 / x64, eso no es decir 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 lenguaje).

 50
Author: ritesh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-05 23:09:19

Si utiliza mutexes para proteger todos sus datos, realmente no debería tener que preocuparse. Mutexes siempre ha proporcionado suficientes garantías de orden y visibilidad.

Ahora, si utilizas atomics, o algoritmos sin bloqueos, necesitas pensar en el modelo de memoria. El modelo de memoria describe con precisión cuándo atomics proporciona garantías de orden y visibilidad, y proporciona vallas portátiles para garantías codificadas a mano.

Anteriormente, atomics se hacía usando intrínsecos del compilador, o algunos biblioteca de nivel superior. Las cercas se habrían hecho usando instrucciones específicas de la CPU (barreras de memoria).

 23
Author: ninjalj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-11 23:49:53