¿El hardware moderno x86 no puede almacenar un solo byte en la memoria?

Hablando del modelo de memoria de C ++ para concurrencia, Stroustrup’s C ++ Programming Language, 4th ed., Secc. 41.2.1, dice:

… (como la mayoría del hardware moderno) la máquina no podía cargar ni almacenar nada más pequeño que una palabra.

Sin embargo, mi procesador x86, hace unos años, puede almacenar y almacena objetos más pequeños que una palabra. Por ejemplo:

#include  int main() { char a = 5; char b = 25; a = b; std::cout << int(a) << "\n"; return 0; } 

Sin optimización, GCC comstack esto como:

  [...] movb $5, -1(%rbp) # a = 5, one byte movb $25, -2(%rbp) # b = 25, one byte movzbl -2(%rbp), %eax # load b, one byte, not extending the sign movb %al, -1(%rbp) # a = b, one byte [...] 

Los comentarios son míos, pero la asamblea es de GCC. Funciona bien, por supuesto.

Obviamente, no entiendo de qué habla Stroustrup cuando explica que el hardware puede cargar y almacenar nada más pequeño que una palabra. Por lo que puedo decir, mi progtwig no hace más que cargar y almacenar objetos más pequeños que una palabra.

El enfoque minucioso de C ++ en las abstracciones de costo cero y amigables con el hardware diferencia a C ++ de otros lenguajes de progtwigción que son más fáciles de dominar. Por lo tanto, si Stroustrup tiene un modelo mental interesante de señales en un autobús, o tiene algo más de este tipo, entonces me gustaría entender el modelo de Stroustrup.

¿De qué está hablando Stroustrup, por favor?

CITA MÁS LARGA CON CONTEXTO

Aquí está la cita de Stroustrup en un contexto más completo:

Considere lo que podría suceder si un enlazador asignara [variables de tipo de char como] c y b en la misma palabra en la memoria y (como la mayoría de los equipos modernos) la máquina no podría cargar o almacenar nada más pequeño que una palabra … Sin un pozo modelo de memoria definido y razonable, el subproceso 1 podría leer la palabra que contiene c , cambiar c y escribir la palabra nuevamente en la memoria. Al mismo tiempo, el hilo 2 podría hacer lo mismo con b . Entonces, cualquiera que sea el hilo que logre leer la palabra primero y el hilo que logre escribir su resultado nuevamente en la memoria, determinará el resultado …

OBSERVACIONES ADICIONALES

No creo que Stroustrup esté hablando de líneas de caché. Incluso si fuera, hasta donde yo sé, los protocolos de coherencia de caché manejarían ese problema de manera transparente, excepto tal vez durante la E / S de hardware.

Revisé la hoja de datos de hardware de mi procesador. Eléctricamente, mi procesador (un Intel Ivy Bridge) parece abordar la memoria DDR3L mediante algún tipo de esquema de multiplexación de 16 bits, por lo que no sé de qué se trata. Sin embargo, no está claro para mí que eso tenga mucho que ver con el punto de vista de Stroustrup.

Stroustrup es un hombre inteligente y un científico eminente, así que no dudo que esté tomando algo sensato. Estoy confundido.

Ver también esta pregunta. Mi pregunta se asemeja a la pregunta vinculada de varias maneras, y las respuestas a la pregunta vinculada también son útiles aquí. Sin embargo, mi pregunta va también al modelo de hardware / bus que motiva a C ++ a ser como es y que hace que Stroustrup escriba lo que escribe. No busco una respuesta meramente con respecto a lo que el estándar C ++ formalmente garantiza, sino que también deseo entender por qué el estándar C ++ lo garantizaría. ¿Cuál es el pensamiento subyacente? Esto es parte de mi pregunta, también.

No creo que sea una statement muy precisa, clara o útil. Sería más exacto decir que las CPU modernas no pueden cargar ni almacenar nada más pequeño que una línea de caché. (Aunque eso no es cierto para las regiones de memoria descartables, por ejemplo, para MMIO).

Probablemente hubiera sido mejor hacer un ejemplo hipotético, en lugar de dar a entender que el hardware real es así. Pero si lo intentamos, quizás podamos encontrar una interpretación que no sea tan obvia o totalmente incorrecta, que podría haber sido lo que Stroustrup estaba pensando cuando escribió esto para introducir el tema de los modelos de memoria. (Lamento que esta respuesta sea tan larga; terminé escribiendo mucho mientras adivinaba lo que podría haber querido decir y sobre temas relacionados …)

O tal vez este es otro caso de diseñadores de lenguaje de alto nivel que no son expertos en hardware o que, al menos ocasionalmente, hacen malas declaraciones.


Creo que Stroustrup está hablando de cómo las CPU funcionan internamente para implementar instrucciones de byte-store. Sugiere que una CPU sin un modelo de memoria razonable y bien definido podría implementar un byte-store con un RMW no atómico de la palabra que contiene en una línea de caché, o en la memoria de una CPU sin caché.

Incluso este reclamo más débil sobre el comportamiento interno (no visible externamente) no es cierto para la mayoría de las CPU de alto rendimiento, incluido el moderno x86. Las CPUs modernas de Intel no tienen una penalización de rendimiento para las tiendas de bytes, o incluso las tiendas de palabras o vectores no alineadas que no cruzan un límite de la línea de caché. Si alguno de estos tuviera que hacer un ciclo RMW como la tienda comprometida con la memoria caché L1D, interferiría con el ancho de banda de la carga.

Alpha AXP, un diseño RISC de alto rendimiento de 1992, famoso (y únicamente entre los ISA modernos que no son DSP) omitió las instrucciones de carga / almacenamiento de bytes hasta Alpha 21164A (EV56) en 1996 . Aparentemente, no consideraban que Word-RMW fuera una opción viable para implementar tiendas de bytes, porque una de las ventajas citadas para implementar solo tiendas alineadas de 32 y 64 bits era una ECC más eficiente para la memoria caché L1D. “ECD SECDED tradicional requeriría 7 bits extra sobre gránulos de 32 bits (22% de sobrecarga) frente a 4 bits extra sobre gránulos de 8 bits (50% de sobrecarga)”. (La respuesta de @Paul A. Clayton sobre el direccionamiento de palabra vs. byte tiene otras cosas interesantes sobre architecture de computadora.) Si las tiendas de bytes se implementaban con word-RMW, aún se podía hacer la detección / corrección de errores con granularidad de palabras.

Las CPU actuales de Intel solo usan paridad (no ECC) en L1D por este motivo. Consulte esta sección Preguntas y respuestas sobre hardware (no) eliminando “tiendas silenciosas”: verificar los contenidos anteriores de la memoria caché antes de escribir para evitar marcar la línea como sucia si se necesitara un RMW en lugar de solo una tienda, y ese es un obstáculo importante.

Supongo que otros diseños de CPU (no x86) modernos no consideraron a RMW como una opción para asignar byte-stores a caché L1D. Word-RMW tampoco es una opción útil para las tiendas de bytes MMIO , así que, a menos que tengas una architecture que no necesite almacenes de palabras secundarias para IO, necesitarías algún tipo de manejo especial para IO (como I dispersa de Alpha O espacio donde las cargas / almacenes de palabras se asignaron a cargas / tiendas de bytes para poder usar tarjetas PCI básicas en lugar de necesitar hardware especial sin registros de IO de bytes).

Como señala @Margaret , los controladores de memoria DDR3 pueden almacenar bytes almacenando señales de control que enmascaran otros bytes de una ráfaga. Los mismos mecanismos que obtienen esta información para el controlador de memoria (para tiendas que no están en la memoria caché) también pueden hacer pasar esa información junto con una carga o almacenamiento al espacio de MMIO. Entonces, hay mecanismos de hardware para hacer realmente una tienda de bytes incluso en sistemas de memoria orientados a ráfagas, y es muy probable que las CPU modernas usen eso en lugar de implementar un RMW, porque es probablemente más simple y es mucho mejor para la corrección de MMIO.


El próximo párrafo de Stroustrup es

“El modelo de memoria C ++ garantiza que dos hilos de ejecución pueden actualizar y acceder a ubicaciones de memoria separadas sin interferir entre sí . Esto es exactamente lo que ingenuamente esperaríamos. Es tarea del comstackdor protegernos de los comportamientos a veces muy extraños y sutiles de hardware moderno. Cómo logra una combinación de comstackdor y hardware que depende del comstackdor … ”

Entonces, aparentemente, él piensa que el hardware real y moderno puede no proporcionar una carga / almacenamiento de bytes “seguro”. Las personas que diseñan modelos de memoria de hardware están de acuerdo con las personas de C / C ++, y se dan cuenta de que las instrucciones de la tienda de bytes no serían muy útiles para los progtwigdores / comstackdores si pudieran pisar los bytes vecinos.

Todas las architectures modernas (que no son DSP), excepto las primeras Alpha AXP, tienen instrucciones de almacenamiento y carga de bytes, y AFAIK están definidas arquitectónicamente para no afectar a los bytes contiguos. Sin embargo, lo logran en hardware, el software no necesita preocuparse por la corrección. Incluso la primera versión de MIPS (en 1983) tenía cargas / tiendas de bytes y de media palabra, y es una ISA muy orientada a las palabras.

Sin embargo, en realidad no afirma que la mayoría del hardware moderno necesite ningún soporte de comstackción especial para implementar esta parte del modelo de memoria C ++, solo que algunos podrían hacerlo. Tal vez realmente solo está hablando de DSPs direccionables por palabras en ese segundo párrafo (donde las implementaciones C y C ++ a menudo usan caracteres de 16 o 32 bits exactamente como el tipo de solución del comstackdor de la que hablaba Stroustrup).


La mayoría de las CPU “modernas” (incluyendo todas las x86) tienen una caché L1D. Buscarán líneas de caché completas (normalmente 64 bytes) y rastrearán las áreas sucias / no sucias por línea de caché. Entonces, dos bytes adyacentes son prácticamente iguales a dos palabras adyacentes, si ambos están en la misma línea de caché. Escribir un byte o palabra dará como resultado una búsqueda de toda la línea y, finalmente, una reescritura de toda la línea. Vea lo que todo progtwigdor debería saber sobre la memoria de Ulrich Drepper. Tiene razón en que MESI (o un derivado como MESIF / MOESI) se asegura de que esto no sea un problema. (Pero, nuevamente, esto es porque el hardware implementa un modelo de memoria sano).

Una tienda solo puede comprometerse con la memoria caché L1D mientras la línea está en el estado Modificado (de MESI). Entonces, incluso si la implementación del hardware interno es lenta para los bytes y toma más tiempo fusionar el byte en la palabra que contiene en la línea de caché, es efectivamente una escritura de modificación de lectura atómica , siempre que no permita invalidar la línea y -accesado entre la lectura y la escritura. ( Si bien esta memoria caché tiene la línea en estado Modificado, ninguna otra memoria caché puede tener una copia válida ). Vea el comentario de @ old_timer haciendo el mismo punto (pero también para RMW en un controlador de memoria).

Esto es más fácil que, por ejemplo, un xchg atómico o add desde un registro que también necesita una ALU y registro de acceso, ya que todos los HW involucrados están en la misma etapa de canalización, que simplemente puede detenerse durante un ciclo extra o dos. Eso es obviamente malo para el rendimiento y requiere hardware adicional para permitir que esa etapa de canalización indique que se está estancando. Esto no necesariamente entra en conflicto con el primer reclamo de Stroustrup, porque estaba hablando de un ISA hipotético sin un modelo de memoria, pero todavía es un tramo.

En un microcontrolador de un solo núcleo, la palabra RMW interna para las tiendas de bytes almacenadas en caché sería más plausible, ya que no habrá Invalidación de solicitudes procedentes de otros núcleos a los que tendrían que retrasar la respuesta durante una actualización RMW de memoria caché atómica . Pero eso no ayuda a E / S en regiones que no se pueden descartar. Digo microcontrolador porque otros diseños de CPU de un solo núcleo generalmente admiten algún tipo de SMP de varios sockets.


Muchas RISC ISA no admiten cargas / tiendas de palabras no alineadas con una sola instrucción, pero eso es un problema aparte (la dificultad es manejar el caso cuando una carga abarca dos líneas de caché o incluso páginas, lo que no puede suceder con bytes o alineados medias palabras). Sin embargo, cada vez más ISA están agregando soporte garantizado para load / store sin alinear en versiones recientes. (por ejemplo, MIPS32 / 64 Release 6 en 2014, y creo que es AArch64 y reciente ARM de 32 bits).


La 4ª edición del libro se publicó en 2013 cuando Alpha había estado muerta durante años. La primera edición se publicó en 1985 , cuando RISC era la nueva gran idea (por ejemplo, Stanford MIPS en 1983, según la línea de tiempo de computación HW de Wikipedia , pero las CPU “modernas” en ese momento eran byte-direccionables con tiendas de bytes) Cyber ​​CDC 6600 direccionable por palabras y probablemente todavía alrededor, pero no podría llamarse moderno.

Incluso las máquinas RISC muy orientadas a las palabras como MIPS y SPARC tienen instrucciones de almacenamiento de bytes y carga de bytes (con extensión de signo o cero). No admiten cargas de palabra no alineadas, simplifican la caché (o acceso a memoria si no hay caché) y puertos de carga, pero puede cargar cualquier byte con una instrucción y, lo que es más importante, almacenar un byte sin reescribir los bytes circundantes.

Supongo que C ++ 11 (que introduce un modelo de memoria sensible a hilos en el lenguaje) en Alpha necesitaría usar char 32 bits si se dirige a una versión de Alpha ISA sin tiendas de bytes. O tendría que usar software atomic-RMW con LL / SC cuando no podía probar que ningún otro subproceso podría tener un puntero que les permitiera escribir bytes contiguos.


IDK cómo las instrucciones de cargar / almacenar lento byte están en cualquier CPU donde se implementan en hardware pero no tan baratos como cargas / tiendas de palabra. Las cargas de bytes son económicas en x86 siempre que use movzx/movsx para evitar el registro parcial de dependencias falsas o la fusión de puestos. En AMD pre Ryzen, movsx necesita un ALU uop extra, pero de lo contrario la extensión cero / firmar se maneja directamente en el puerto de carga de las CPU Intel y AMD. El principal inconveniente de x86 es que necesita una instrucción de carga separada en lugar de utilizar un operando de memoria como fuente para una instrucción ALU, lo que ahorra ancho de banda de procesamiento de entrada y tamaño de código. Las ISA de la tienda de carga RISC siempre necesitan instrucciones de carga e inventario separadas de todos modos. Las tiendas de bytes x86 no son más caras que las tiendas de 32 bits.

Como un problema de rendimiento, una buena implementación de C ++ para hardware con tiendas de bytes lentos puede poner cada char en su propia palabra y usar cargas de palabras / tiendas siempre que sea posible (por ejemplo, para globales fuera de las estructuras y para los locales en la stack). IDK si alguna implementación real de MIPS / ARM / lo que sea tiene carga / almacenamiento lento de bytes, pero si es así, tal vez gcc tenga -mtune= opciones para controlarlo.

Eso no ayuda con char[] , ni eliminando un char * cuando no sabes hacia dónde apunta. (Esto incluye volatile char* que usarías para MMIO). Hacer que el comstackdor + vinculador coloque variables char en palabras separadas no es una solución completa, solo un truco de rendimiento si las tiendas de bytes verdaderos son lentas.


Más sobre Alpha:

Del Linux Alpha HOWTO .

Cuando se presentó la architecture Alpha, fue única entre las architectures RISC para evitar cargas y tiendas de 8 y 16 bits. Admitía cargas y tiendas de 32 y 64 bits (palabra larga y palabra cuadrada, en la nomenclatura de Digital). Los co-arquitectos (Dick Sites, Rich Witek) justificaron esta decisión citando las ventajas:

  1. El soporte de bytes en el subsistema de caché y memoria tiende a ralentizar los accesos para cantidades de 32 bits y 64 bits.
  2. El soporte de bytes hace que sea difícil construir circuitos de corrección de errores de alta velocidad en el subsistema de caché / memoria.

Alpha compensa proporcionando potentes instrucciones para manipular bytes y grupos de bytes dentro de registros de 64 bits. Los puntos de referencia estándar para las operaciones de cadena (por ejemplo, algunos de los puntos de referencia de Byte) muestran que Alpha funciona muy bien en la manipulación de bytes.

Las CPU x86 no solo son capaces de leer y escribir un solo byte, sino que todas las CPU modernas de propósito general son capaces de hacerlo. Más importante aún es que la mayoría de las CPU modernas (incluyendo x86, ARM, MIPS, PowerPC y SPARC) son capaces de leer y escribir atómicamente bytes individuales.

No estoy seguro de a qué se estaba refiriendo Stroustrup. Solía ​​haber algunas máquinas direccionables por palabra que no eran capaces de direccionamiento de bytes de 8 bits, como el Cray, y como Peter Cordes mencionó las primeras CPU Alpha no soportaban cargas de bytes y tiendas, pero hoy las únicas CPU son incapaces de byte las cargas y las tiendas son ciertos DSP utilizados en aplicaciones de nicho. Incluso si suponemos que quiere decir que la mayoría de las CPU modernas no tienen carga de bytes atómicos y las almacena, esto no es cierto para la mayoría de las CPU.

Sin embargo, las cargas atómicas simples y las tiendas no son de mucha utilidad en la progtwigción multiproceso. También suele necesitar solicitar garantías y una forma de hacer que las operaciones de lectura, modificación y escritura sean atómicas. Otra consideración es que mientras que la CPU a puede tener instrucciones de carga y almacenamiento de bytes, el comstackdor no está obligado a usarlas. Un comstackdor, por ejemplo, aún podría generar el código que describe Stroustrup, cargando ambos c usando una sola instrucción de carga de palabras como una optimización.

Entonces, si bien necesita un modelo de memoria bien definido, aunque solo sea para que el comstackdor se vea obligado a generar el código que espera, el problema no es que las CPU modernas no sean capaces de cargar o almacenar nada más pequeño que una palabra.

Al autor parece preocuparle que el hilo 1 y el hilo 2 entren en una situación donde las escrituras leídas-modificaciones-escrituras (no en el software, el software hace dos instrucciones separadas de un tamaño de byte, en alguna parte de la línea la lógica tiene que hacer una lectura- modificar-escribir) en lugar de la lectura ideal modificar escribir leer modificar escribir, convertirse en leer leer modificar modificar escribir escribir o en algún otro momento tal que ambos lean la versión pre-modificada y la última para escribir gana. leer leer modificar modificar escribir escribir, o leer modificar leer leer escribir escribir o leer modificar leer escribir modificar escribir.

La preocupación es comenzar con 0x1122 y un hilo quiere que sea 0x33XX, el otro quiere que sea 0xXX44, pero con, por ejemplo, leer leer modificar modificar escribir escribir terminar con 0x1144 o 0x3322, pero no 0x3344

Un diseño sano (sistema / lógica) simplemente no tiene ese problema, ciertamente no para un procesador de propósito general como este, he trabajado en diseños con problemas de sincronización como este, pero eso no es de lo que estamos hablando aquí, diseños de sistemas completamente diferentes para diferentes propósitos. La lectura-modificación-escritura no abarca una distancia suficientemente larga en un diseño sano, y los x86 son diseños sanos.

La lectura-modificación-escritura ocurriría muy cerca de la primera SRAM involucrada (idealmente L1 cuando se ejecuta un x86 de manera típica con un sistema operativo capaz de ejecutar progtwigs multiproceso comstackdos en C ++) y ocurriría dentro de unos pocos ciclos de reloj cuando el ram sea a la velocidad del autobús idealmente. Y, como señaló Peter, se considera que es toda la línea de caché que experimenta esto, dentro de la memoria caché, no una lectura-modificación-escritura entre el núcleo del procesador y la caché.

La noción de “al mismo tiempo” incluso con sistemas multi-core no es necesariamente al mismo tiempo, eventualmente se serializa porque el desempeño no se basa en que sean paralelos de principio a fin, se basa en mantener los buses cargado.

La cita es decir variables asignadas a la misma palabra en la memoria, por lo que ese es el mismo progtwig. Dos progtwigs separados no van a compartir un espacio de direcciones como ese. asi que

Le invitamos a probar esto, haga un progtwig multiproceso que uno escriba para decir dirección 0xnnn00000 el otro escriba a dirección 0xnnnn00001, cada uno escribe, luego lee o mejor varias escrituras del mismo valor que una lectura, verifique que la lectura fue la byte que escribieron, luego repite con un valor diferente. Deje que funcione por un tiempo, horas / días / semanas / meses. Vea si se desconecta el sistema … use el ensamblado para las instrucciones de escritura reales para asegurarse de que está haciendo lo que usted solicitó (no C ++ o cualquier comstackdor que haga o afirme que no colocará estos elementos en la misma palabra). Puede agregar retrasos para permitir más desalojos de caché, pero eso reduce las probabilidades de colisiones “al mismo tiempo”.

Su ejemplo, siempre y cuando se asegure de que no está sentado en dos lados de un límite (caché u otro) como 0xNNNNFFFFF y 0xNNNN00000, aísle las dos escrituras de bytes en direcciones como 0xNNNN00000 y 0xNNNN00001 tenga las instrucciones al dorso y vea si obtiene una lectura leer modificar modificar escribir escribir. Envuelva una prueba a su alrededor, que los dos valores sean diferentes en cada ciclo, que vuelva a leer la palabra como un todo en el retraso posterior que desee y verifique los dos valores. Repita durante días / semanas / meses / años para ver si falla. Lea sobre la ejecución de su procesador y características de microcódigo para ver qué hace con esta secuencia de instrucciones y, según sea necesario, cree una secuencia de instrucciones diferente que intente obtener las transacciones iniciadas dentro de un puñado de ciclos de reloj en el extremo más alejado del núcleo del procesador.

EDITAR

el problema con las citas es que todo se trata del lenguaje y el uso de. “como la mayoría del hardware moderno” pone todo el tema / texto en una posición delicada, es demasiado vago, un lado puede argumentar que todo lo que tengo que hacer es encontrar un caso que sea verdadero para hacer que todo lo demás sea cierto, también un lado podría discutir si encuentro un caso, el rest no es verdadero. Usar la palabra como una especie de desastre con eso como una posible salida de la tarjeta libre de la cárcel.

La realidad es que un porcentaje significativo de nuestros datos se almacena en DRAM en memorias de 8 bits de ancho, solo que no accedemos a ellos como de 8 bits de ancho, normalmente accedemos a 8 de ellos a la vez, con 64 bits de ancho. En algunas semanas / meses / años / décadas, esta statement será incorrecta.

La cita más grande dice “al mismo tiempo” y luego dice “leer … primero, escribir … primero, bien primero y último y al mismo tiempo no tener sentido juntos, ¿es paralelo o en serie? El contexto en su conjunto está preocupado por las lecturas de lectura leídas, las variaciones de escritura y escritura de arriba, donde tiene una última escritura y depende de cuándo esa lectura determina si ambas modificaciones sucedieron o no. No es al mismo tiempo que “como la mayoría del hardware moderno” no tiene sentido que las cosas que empiezan en paralelo en núcleos / módulos separados eventualmente se serialicen si apuntan al mismo flip-flop / transistor en una memoria, uno finalmente tiene que esperar que el otro vaya primero. Al estar basado en la física, no veo que esto sea incorrecto en las próximas semanas / meses / años.

Esto es correcto. Una CPU x86_64, al igual que una CPU x86 original, no puede leer o escribir nada más pequeño que una palabra (en este caso de 64 bits) de rsp. a la memoria Y no suele leer o escribir menos que una línea de caché completa, aunque hay formas de eludir la caché, especialmente por escrito (ver a continuación).

En este contexto , sin embargo, Stroustrup se refiere a las posibles carreras de datos (falta de atomicidad en un nivel observable). Este problema de corrección es irrelevante en x86_64, debido al protocolo de coherencia de caché que mencionaste. En otras palabras, sí, la CPU está limitada a transferencias de palabras completas, pero esto se maneja de forma transparente, y usted, como progtwigdor, generalmente no tiene que preocuparse por ello. De hecho, el lenguaje C ++, a partir de C ++ 11, garantiza que las operaciones concurrentes en distintas ubicaciones de memoria tengan un comportamiento bien definido, es decir, el que usted esperaría. Incluso si el hardware no lo garantizara, la implementación tendría que encontrar la forma de generar un código posiblemente más complejo.

Dicho esto, puede ser una buena idea mantener el hecho de que palabras enteras o incluso líneas de caché siempre están involucradas en el nivel de la máquina en la parte posterior de la cabeza, por dos razones.

  • En primer lugar, y esto solo es relevante para las personas que escriben controladores de dispositivos o dispositivos de diseño, las E / S asignadas a la memoria pueden ser sensibles a la forma en que se accede a ellas. Como ejemplo, piense en un dispositivo que expone un registro de comandos de solo escritura de 64 bits en el espacio de direcciones físicas. Entonces puede ser necesario:
    • Deshabilitar el almacenamiento en caché No es válido leer una línea de caché, cambiar una sola palabra y escribir nuevamente la línea de caché. Además, incluso si fuera válido, aún existiría un gran riesgo de que los comandos se pierdan porque la memoria caché de la CPU no se ha escrito lo suficientemente pronto. Como mínimo, la página debe configurarse como “escritura simultánea”, lo que significa que las escrituras tienen efecto inmediato. Por lo tanto, una entrada de tabla de página x86_64 contiene indicadores que controlan el comportamiento de almacenamiento en caché de la CPU para esta página .
    • Asegúrese de que la palabra completa siempre esté escrita, en el nivel de ensamblaje. Por ejemplo, considere un caso en el que escriba el valor 1 en el registro, seguido de un 2. Un comstackdor, especialmente cuando optimiza para el espacio, puede decidir sobreescribir solo el byte menos significativo porque los otros ya se supone que son cero (es decir, para RAM común), o podría eliminar la primera escritura porque este valor parece sobrescribirse inmediatamente de todos modos. Sin embargo, ninguno se supone que suceda aquí. En C / C ++, la palabra clave volatile es vital para evitar tales optimizaciones inadecuadas.
  • En segundo lugar, y esto es relevante para casi cualquier progtwigdor que escriba progtwigs de subprocesos múltiples, el protocolo de coherencia de memoria caché, a la vez que evita el desastre, puede tener un gran costo de rendimiento si se “abusa”.

Aquí hay un ejemplo algo artificial de una estructura de datos muy mala. Supongamos que tiene 16 subprocesos al analizar parte del texto de un archivo. Cada hilo tiene una id de 0 a 15.

 // shared state char c[16]; FILE *file[16]; void threadFunc(int id) { while ((c[id] = getc(file[id])) != EOF) { // ... } } 

Esto es seguro porque cada hilo opera en una ubicación de memoria diferente. Sin embargo, estas ubicaciones de memoria suelen residir en la misma línea de caché o, como máximo, se dividen en dos líneas de caché. El protocolo de coherencia de caché se usa luego para sincronizar correctamente los accesos a c[id] . Y aquí radica el problema, porque esto obliga a cada hilo a esperar hasta que la línea de caché esté disponible exclusivamente antes de hacer algo con c[id] , a menos que ya se esté ejecutando en el núcleo que “posee” la línea de caché. Suponiendo que varios, por ejemplo, 16 núcleos, la coherencia de la memoria caché normalmente transferirá la línea de caché de un núcleo a otro todo el tiempo. Por razones obvias, este efecto se conoce como “línea de caché ping-pong”. Crea un cuello de botella de rendimiento horrible. Es el resultado de un muy mal caso de uso compartido falso , es decir, subprocesos que comparten una línea de caché física sin acceder realmente a las mismas ubicaciones de memoria lógica.

En contraste con esto, especialmente si uno da el paso adicional de asegurarse de que la matriz de file resida en su propia línea de caché, usarla sería completamente inofensivo (en x86_64) desde una perspectiva de rendimiento porque solo se leen los punteros, la mayoría del tiempo . En este caso, varios núcleos pueden “compartir” la línea de caché como de solo lectura. Solo cuando un núcleo intente escribir en la línea de caché, debe decirle a los otros núcleos que va a “aprovechar” la línea de caché para tener acceso exclusivo.

(Esto se simplifica enormemente, ya que existen diferentes niveles de cachés de CPU, y varios núcleos pueden compartir el mismo caché L2 o L3, pero debería darle una idea básica del problema).

Not sure what Stroustrup meant by “WORD”. Maybe it is the minimum size of memory storage of the machine?

Anyway not all machines were created with 8bit (BYTE) resolution. In fact I recommend this awesome article by Eric S. Raymond describing some of the history of computers: http://www.catb.org/esr/faqs/things-every-hacker-once-knew/

“… It used also to be generally known that 36-bit architectures explained some unfortunate features of the C language. The original Unix machine, the PDP-7, featured 18-bit words corresponding to half-words on larger 36-bit computers. These were more naturally represented as six octal (3-bit) digits.”

Stroustrup is not saying that no machine can perform loads and stores smaller than their native word size, he is saying that a machine couldn’t .

While this seems surprising at first, it’s nothing esoteric.
For starter, we will ignore the cache hierarchy, we will take that into account later.
Assume there are no caches between the CPU and the memory.

The big problem with memory is density , trying to put more bits possible into the smallest area.
In order to achieve that it is convenient, from an electrical design point of view, to expose a bus as wider as possible (this favours the reuse of some electrical signals, I haven’t looked at the specific details though).
So, in architecture where big memories are needed (like the x86) or a simple low-cost design is favourable (for example where RISC machines are involved), the memory bus is larger than the smallest addressable unit (typically the byte).

Depending on the budget and legacy of the project the memory can expose a wider bus alone or along with some sideband signals to select a particular unit into it.
What does this mean practically?
If you take a look at the datasheet of a DDR3 DIMM you’ll see that there are 64 DQ0–DQ63 pins to read/write the data.
This is the data bus, 64-bit wide, 8 bytes at a time.
This 8 bytes thing is very well founded in the x86 architecture to the point that Intel refers to it in the WC section of its optimisation manual where it says that data are transferred from the 64 bytes fill buffer (remember: we are ignoring the caches for now, but this is similar to how a cache line gets written back) in bursts of 8 bytes (hopefully, continuously).

Does this mean that the x86 can only write QWORDS (64-bit)?
No, the same datasheet shows that each DIMM has the DM0–DM7 ,DQ0–DQ7 and DQS0–DQS7 signals to mask, direct and strobe each of the 8 bytes in the 64-bit data bus.

So x86 can read and write bytes natively and atomically.
However, now it’s easy to see that this could not be the case for every architecture.
For instance, the VGA video memory was DWORD (32-bit) addressable and making it fit in the byte addressable world of the 8086 led to the messy bit-planes.

In general specific purpose architecture, like DSPs, could not have a byte addressable memory at the hardware level.

There is a twist: we have just talked about the memory data bus, this is the lowest layer possible.
Some CPUs can have instructions that build a byte addressable memory on top of a word addressable memory.
Qué significa eso?
It’s easy to load a smaller part of a word: just discard the rest of the bytes!
Unfortunately, I can’t recall the name of the architecture (if it even existed at all!) where the processor simulated a load of an unaligned byte by reading the aligned word containing it and rotating the result before saving it in a register.

With stores, the matter is more complex: if we can’t simply write the part of the word that we just updated we need to write the unchanged remaining part too.
The CPU, or the programmer, must read the old content, update it and write it back.
This is a Read-Modify-Write operation and it is a core concept when discussing atomicity.

Considerar:

 /* Assume unsigned char is 1 byte and a word is 4 bytes */ unsigned char foo[4] = {}; /* Thread 0 Thread 1 */ foo[0] = 1; foo[1] = 2; 

Is there a data race?
This is safe on x86 because they can write bytes, but what if the architecture cannot?
Both threads would have to read the whole foo array, modify it and write it back.
In pseudo-C this would be

 /* Assume unsigned char is 1 byte and a word is 4 bytes */ unsigned char foo[4] = {}; /* Thread 0 Thread 1 */ /* What a CPU would do (IS) What a CPU would do (IS) */ int tmp0 = *((int*)foo) int tmp1 = *((int*)foo) /* Assume little endian Assume little endian */ tmp0 = (tmp0 & ~0xff) | 1; tmp1 = (tmp1 & ~0xff00) | 0x200; /* Store it back Store it back */ *((int*)foo) = tmp0; *((int*)foo) = tmp1; 

We can now see what Stroustrup was talking about: the two stores *((int*)foo) = tmpX obstruct each other, to see this consider this possible execution sequence:

 int tmp0 = *((int*)foo) /* T0 */ tmp0 = (tmp0 & ~0xff) | 1; /* T1 */ int tmp1 = *((int*)foo) /* T1 */ tmp1 = (tmp1 & ~0xff00) | 0x200; /* T1 */ *((int*)foo) = tmp1; /* T0 */ *((int*)foo) = tmp0; /* T0, Whooopsy */ 

If the C++ didn’t have a memory model these kinds of nuisances would have been implementation specific details, leaving the C++ a useless programming language in a multithreading environment.

Considering how common is the situation depicted in the toy example, Stroustrup stressed out the importance of a well-defined memory model.
Formalizing a memory model is hard work, it’s an exhausting, error-prone and abstract process so I also see a bit of pride in the words of Stroustrup.

I have not brushed up on the C++ memory model but updating different array elements is fine .
That’s a very strong guarantee.

We have left out the caches but that doesn’t really change anything, at least for the x86 case.
The x86 writes to memory through the caches, the caches are evicted in lines of 64 bytes .
Internally each core can update a line at any position atomically unless a load/store crosses a line boundary (eg by writing near the end of it).
This can be avoided by naturally aligning data (can you prove that?).

In a multi-code/socket environment, the cache coherency protocol ensures that only a CPU at a time is allowed to freely write to a cached line of memory (the CPU that has it in the Exclusive or Modified state).
Basically, the MESI family of protocol use a concept similar to locking found the DBMSs.
This has the effect, for the writing purpose, of “assigning” different memory regions to different CPUs.
So it doesn’t really affect the discussion of above.