c++ - valor - ¿Puede el hardware moderno x86 no almacenar un solo byte en la memoria?
tipos de datos de programacion en c (6)
Hablando del modelo de memoria de C ++ para la concurrencia, Stroustrup''s C ++ Programming Language, 4ª ed., Sec. 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, de unos pocos años, puede y almacena objetos más pequeños que una palabra. Por ejemplo:
#include <iostream>
int main()
{
char a = 5;
char b = 25;
a = b;
std::cout << int(a) << "/n";
return 0;
}
Sin optimización, GCC compila 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é está hablando Stroustrup cuando explica que el hardware puede cargar y almacenar nada más pequeño que una palabra. Por lo que puedo decir, mi programa no hace más que cargar y almacenar objetos más pequeños que una palabra.
El enfoque completo de C ++ en abstracciones de costo cero y amigables con el hardware distingue a C ++ de otros lenguajes de programació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 pasar si un enlazador asignara [variables de tipo
char
como]c
yb
en la misma palabra en la memoria y (como la mayoría del hardware moderno) la máquina no podría cargar o almacenar nada más pequeño que una palabra ... Sin un pozo -definido y modelo de memoria razonable, el hilo 1 podría leer la palabra que contienec
, cambiarc
y volver a escribir la palabra en la memoria. Al mismo tiempo, el hilo 2 podría hacer lo mismo conb
. Entonces, cualquier hilo que haya podido leer la palabra primero y el hilo que haya podido escribir su resultado de nuevo en la memoria determinará el resultado ...
OBSERVACIONES ADICIONALES
No creo que Stroustrup esté hablando de líneas de caché. Incluso si él 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.
He revisado 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 Stroustrup.
Stroustrup es un hombre inteligente y un científico eminente, por lo 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 simplemente con respecto a lo que el estándar C ++ garantiza formalmente, sino que también deseo entender por qué el estándar C ++ lo garantizaría. ¿Cuál es el pensamiento subyacente? Esto también es parte de mi pregunta.
Esto es correcto. Una CPU x86_64, al igual que una CPU x86 original, no puede leer ni escribir nada más pequeño que una palabra (en este caso, 64 bits) de rsp. a la memoria Y, por lo general, no leerá ni escribirá menos que una línea de caché completa, aunque hay formas de omitir el caché, especialmente por escrito (ver más abajo).
Sin embargo, en este contexto , Stroustrup se refiere a 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 mencionó. En otras palabras, sí, la CPU está limitada a transferencias de palabras completas, pero esto se maneja de manera transparente y usted, como programador, generalmente no tiene que preocuparse por eso. De hecho, el lenguaje C ++, a partir de C ++ 11, garantiza que las operaciones concurrentes en ubicaciones de memoria distintas tengan un comportamiento bien definido, es decir, el que cabría esperar. Incluso si el hardware no garantizara esto, la implementación tendría que encontrar la manera de generar código posiblemente más complejo.
Dicho esto, aún puede ser una buena idea mantener el hecho de que palabras completas o incluso líneas de caché siempre están involucradas en el nivel de la máquina en la parte posterior de su cabeza, por dos razones.
-
Primero, y esto solo es relevante para las personas que escriben controladores de dispositivos o diseñan dispositivos, las E / S mapeadas en memoria pueden ser sensibles a la forma en que se accede a ellas.
Como ejemplo, piense en un dispositivo que expone un registro de comando 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 volver a escribir la línea de caché. Además, incluso si fuera válido, aún habría un gran riesgo de que se pierdan los comandos porque la memoria caché de la CPU no se vuelve a escribir lo suficientemente pronto. Como mínimo, la página debe configurarse como "escritura directa", 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 toda la palabra se escriba siempre, en el nivel de ensamblado.
Por ejemplo, considere un caso en el que escribe el valor 1 en el registro, seguido de un 2. Un compilador, especialmente cuando se optimiza para el espacio, podría decidir sobrescribir solo el byte menos significativo porque los otros ya se supone que son cero (es decir, para RAM normal), o podría en su lugar eliminar la primera escritura porque este valor parece sobrescribirse inmediatamente de todos modos.
Sin embargo, tampoco se supone que suceda aquí.
En C / C ++, la
volatile
palabra clave es vital para evitar tales optimizaciones inadecuadas.
- En segundo lugar, y esto es relevante para casi cualquier desarrollador que escriba programas de subprocesos múltiples, el protocolo de coherencia de la memoria caché, si bien evita el desastre, puede tener un costo de rendimiento enorme si se "abusa" de él.
Aquí hay un ejemplo, algo artificial, de una estructura de datos muy mala. Suponga que tiene 16 hilos que analizan parte del texto de un archivo. Cada hilo tiene un
id
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 subproceso opera en una ubicación de memoria diferente. Sin embargo, estas ubicaciones de memoria normalmente residirían en la misma línea de caché, o como máximo se dividirían en dos líneas de caché. El protocolo de coherencia de caché se utiliza para sincronizar correctamente los accesos
c[id]
. Y aquí radica el problema, porque esto obliga a cada
otro
hilo a esperar hasta que la línea de caché esté disponible exclusivamente antes de hacer algo
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é generalmente transferirá la línea de memoria caché de un núcleo a otro todo el tiempo. Por razones obvias, este efecto se conoce como "ping-pong de línea de caché". 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 dio el paso adicional de asegurarse de que la
file
matriz reside en su propia línea de caché, usarlo sería completamente inofensivo (en x86_64) desde una perspectiva de rendimiento porque los punteros solo se leen, la mayoría del tiempo.
En este caso, varios núcleos pueden "compartir" la línea de caché como de solo lectura.
Solo cuando cualquier núcleo intenta escribir en la línea de caché, tiene que decirle a los otros núcleos que va a "aprovechar" la línea de caché para acceso exclusivo.
(Esto se simplifica enormemente, ya que hay 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).
Stroustrup no dice que ninguna máquina pueda realizar cargas y almacenes más pequeños que su tamaño de palabra nativo, dice que una máquina no puede .
Si bien esto parece sorprendente al principio, no es nada esotérico.
Para empezar, ignoraremos la jerarquía de caché, lo tendremos en cuenta más adelante.
Suponga que no hay cachés entre la CPU y la memoria.
El gran problema con la memoria es la
densidad
, tratando de poner más bits posibles en el área más pequeña.
Para lograrlo, es conveniente, desde el punto de vista del diseño eléctrico, exponer un bus lo más ancho posible (esto favorece la reutilización de algunas señales eléctricas, aunque no he examinado los detalles específicos).
Entonces, en la arquitectura donde se necesitan grandes recuerdos (como el x86) o un diseño simple de bajo costo es favorable (por ejemplo, donde están involucradas máquinas RISC), el bus de memoria es más grande que la unidad direccionable más pequeña (generalmente el byte).
Dependiendo del presupuesto y el legado del proyecto, la memoria puede exponer un bus más amplio solo o junto con algunas señales de banda lateral para seleccionar una unidad en particular.
¿Qué significa esto practicamente?
Si echa un vistazo a la
hoja de datos de un DDR3 DIMM
, verá que hay 64
pines DQ0-DQ63
para leer / escribir los datos.
Este es el bus de datos, de 64 bits de ancho, 8 bytes a la vez.
Esta cosa de 8 bytes está muy bien fundada en la arquitectura x86 hasta el punto de que Intel se refiere a ella en la sección WC de su manual de optimización donde dice que los datos se transfieren desde los 64
bytes
fill buffer (recuerde: estamos ignorando las cachés por ahora, pero esto es similar a cómo se vuelve a escribir una línea de caché) en ráfagas de 8 bytes (con suerte, continuamente).
¿Significa esto que el x86 solo puede escribir QWORDS (64 bits)?
No, la misma hoja de datos muestra que cada DIMM tiene las
señales
DM0 – DM7, DQ0 – DQ7
y
DQS0 – DQS7
para enmascarar, dirigir y encender cada uno de los 8 bytes en el bus de datos de 64 bits.
Entonces x86 puede leer y escribir bytes de forma nativa y atómica.
Sin embargo, ahora es fácil ver que este no podría ser el caso para todas las arquitecturas.
Por ejemplo, la memoria de video VGA era direccionable DWORD (32 bits) y hacerla encajar en el mundo direccionable de bytes del 8086 condujo a los planos de bits desordenados.
En general, la arquitectura de propósito específico, como los DSP, no podría tener una memoria direccionable de bytes a nivel de hardware.
Hay un giro: acabamos de hablar sobre el bus de datos de memoria, esta es la capa más baja posible.
Algunas CPU pueden tener instrucciones que crean una memoria direccionable de bytes sobre la memoria direccionable de una palabra.
Qué significa eso?
Es fácil cargar una parte más pequeña de una palabra: ¡simplemente descarte el resto de los bytes!
Desafortunadamente, no puedo recordar el nombre de la arquitectura (¡si es que existió!) Donde el procesador simuló una carga de un byte no alineado al leer la palabra alineada que la contiene y rotar el resultado antes de guardarlo en un registro.
Con las tiendas, el asunto es más complejo: si no podemos simplemente escribir la parte de la palabra que acabamos de actualizar, también debemos escribir la parte restante sin cambios.
La CPU, o el programador, debe leer el contenido anterior, actualizarlo y escribirlo de nuevo.
Esta es una operación de lectura-modificación-escritura y es un concepto central cuando se habla de atomicidad.
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;
¿Hay una carrera de datos?
Esto es seguro en x86 porque pueden escribir bytes, pero ¿y si la arquitectura no puede?
Ambos hilos tendrían que leer la
matriz
completa
foo
, modificarla y volver a escribirla.
En
pseudo-C
esto sería
/* 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;
Ahora podemos ver de qué estaba hablando Stroustrup: las dos tiendas se
*((int*)foo) = tmpX
obstruyen entre sí, para ver esto considere esta posible secuencia de ejecución:
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 */
Si el C ++ no tuviera un modelo de memoria, este tipo de molestias habrían sido detalles específicos de la implementación, dejando al C ++ un lenguaje de programación inútil en un entorno de subprocesos múltiples.
Considerando lo común que es la situación representada en el ejemplo del juguete, Stroustrup
destacó
la importancia de un modelo de memoria bien definido.
Formalizar un modelo de memoria es un trabajo duro, es un proceso agotador, propenso a errores y abstracto, así que también veo un poco de
orgullo
en las palabras de Stroustrup.
No he repasado el modelo de memoria C ++, pero actualizar diferentes elementos de la matriz
está bien
.
Esa es una garantía muy fuerte.
Hemos omitido los cachés, pero eso realmente no cambia nada, al menos para el caso x86.
El x86 escribe en la memoria a través de los cachés, los cachés se expulsan en líneas de 64
bytes
.
Internamente, cada núcleo puede actualizar una línea atómicamente en cualquier posición, a menos que una carga / almacén cruce un límite de línea (por ejemplo, escribiendo cerca del final de la misma).
Esto se puede evitar alineando los datos de forma natural (¿puede probar eso?).
En un entorno de múltiples códigos / sockets, el protocolo de coherencia de caché garantiza que solo una CPU a la vez pueda escribir libremente en una línea de memoria en caché (la CPU que la tiene en estado Exclusivo o Modificado).
Básicamente, la familia de protocolos MESI utiliza un concepto similar al bloqueo encontrado en los DBMS.
Esto tiene el efecto, a efectos de escritura, de "asignar" diferentes regiones de memoria a diferentes CPU.
Por lo tanto, realmente no afecta la discusión de arriba.
El autor parece estar preocupado porque el subproceso 1 y el subproceso 2 entran en una situación en la que la lectura-modificación-escritura (no en el software, el software hace dos instrucciones separadas de un tamaño de byte, en algún lugar de la línea lógica tiene que hacer una lectura- modificar-escribir) en lugar de la lectura ideal modificar escribir leer leer modificar escribir, se convierte en una lectura leer modificar modificar escribir escribir o algún otro momento tal que ambos lean la versión pre-modificada y la última en escribir gana. leer leer modificar modificar escribir escribir, o leer modificar leer leer modificar 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, una lectura, lectura, modificación, modificación, escritura y escritura, usted termina con 0x1144 o 0x3322, pero no con 0x3344
Un diseño sano (sistema / lógica) simplemente no tiene ese problema, ciertamente no es para un procesador de propósito general como este, he trabajado en diseños con problemas de tiempo 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 lo suficientemente larga en un diseño sensato, y los x86 son diseños sensatos.
La lectura-modificación-escritura ocurriría muy cerca de la primera SRAM involucrada (idealmente L1 cuando se ejecuta un x86 de una manera típica con un sistema operativo capaz de ejecutar programas multiproceso compilados en C ++) y ocurre dentro de unos pocos ciclos de reloj cuando el ram está funcionando. A la velocidad del autobús idealmente. Y como Peter señaló, se considera que es toda la línea de caché que experimenta esto, dentro del caché, no una lectura-modificación-escritura entre el núcleo del procesador y el caché.
La noción de "al mismo tiempo", incluso con sistemas multinúcleo, no es necesariamente al mismo tiempo, eventualmente se serializa porque el rendimiento no se basa en que sean paralelos de principio a fin, sino en mantener los buses cargado.
La cita dice variables asignadas a la misma palabra en la memoria, por lo que ese es el mismo programa. Dos programas separados no van a compartir un espacio de direcciones como ese. asi que
Le invitamos a probar esto, haga un programa multiproceso que uno escriba para decir la dirección 0xnnn00000, el otro escriba en la dirección 0xnnnn00001, cada uno escriba, luego lea o mejor escriba varias veces del mismo valor que una lectura, verifique que la lectura haya sido Byte que escribieron, luego se repite con un valor diferente. Deje que se ejecute por un tiempo, horas / días / semanas / meses. Vea si activa el sistema ... use el ensamblado para las instrucciones de escritura reales para asegurarse de que está haciendo lo que le pidió (no C ++ o cualquier compilador que haga o afirme que no pondrá estos elementos en la misma palabra). Puede agregar demoras para permitir más desalojos de caché, pero eso reduce sus 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 escrituras de dos bytes en direcciones como 0xNNNN00000 y 0xNNNN00001 con las instrucciones al revés y vea si obtiene una lectura leer modificar modificar escribir escribir. Envuelva una prueba a su alrededor, que los dos valores son diferentes en cada ciclo, lea la palabra en su conjunto en cualquier retraso que desee más tarde y verifique los dos valores. Repita durante días / semanas / meses / años para ver si falla. Lea sobre la ejecución de sus procesadores y las 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 iniciar las transacciones dentro de unos pocos ciclos de reloj en el lado más alejado del núcleo del procesador.
EDITAR
El problema con las citas es que 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 que todo lo demás sea verdadero, del mismo modo un lado Podría argumentar que si encuentro un caso, el resto no es cierto. Usar la palabra como un lío 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 ya que 8 bits de ancho normalmente accedemos a 8 de ellos a la vez, 64 bits de ancho. En algunas semanas / meses / años / décadas, esta declaración será incorrecta.
La cita más grande dice "al mismo tiempo" y luego dice leer ... primero, escribir ... último, bueno primero y último y al mismo tiempo no tienen sentido juntos, ¿es paralelo o en serie? El contexto en su conjunto se refiere a las variaciones de lectura, lectura, modificación, modificación, escritura y escritura en las que tiene una última escritura y dependiendo de cuándo esa lectura determina si ambas modificaciones ocurrieron o no. No se trata al mismo tiempo de que "como la mayoría del hardware moderno" no tiene sentido, las cosas que comienzan en paralelo en núcleos / módulos separados eventualmente se serializan si apuntan al mismo flip-flop / transistor en una memoria, uno eventualmente tiene que esperar a 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.
Las CPU x86 no solo son capaces de leer y escribir un solo byte, sino que todas las CPU modernas de uso general son capaces de hacerlo. Más importante aún, la mayoría de las CPU modernas (incluidas x86, ARM, MIPS, PowerPC y SPARC) son capaces de leer y escribir atómicamente bytes individuales.
No estoy seguro de a qué se refería Stroustrup. Solía haber algunas máquinas direccionables de palabras que no eran capaces de direccionar bytes de 8 bits, como Cray, y como Peter Cordes mencionó, las primeras CPU Alpha no admitían cargas y tiendas de bytes, pero hoy en día las únicas CPU 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 una carga de bytes atómica y almacena, esto no es cierto para la mayoría de las CPU.
Sin embargo, las cargas y tiendas atómicas simples no son de mucha utilidad en la programación multiproceso.
Por lo general, también necesita garantías de pedido y una forma de hacer que las operaciones de lectura-modificación-escritura sean atómicas.
Otra consideración es que si bien la CPU a puede tener instrucciones para cargar y almacenar bytes, el compilador no está obligado a usarlas.
Un compilador, por ejemplo, aún podría generar el código que describe Stroustrup, cargando
b
y
c
utilizando una sola instrucción de carga de palabras como optimización.
Entonces, si bien necesita un modelo de memoria bien definido, aunque el compilador 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 algo más pequeño que una palabra.
No estoy seguro de lo que Stroustrup quiso decir con "PALABRA". Tal vez es el tamaño mínimo de almacenamiento de memoria de la máquina?
De todos modos, no todas las máquinas se crearon con una resolución de 8 bits (BYTE). De hecho, recomiendo este impresionante artículo de Eric S. Raymond que describe parte de la historia de las computadoras: http://www.catb.org/esr/faqs/things-every-hacker-once-knew/
"... También se sabía que las arquitecturas de 36 bits explicaban algunas características desafortunadas del lenguaje C. La máquina original de Unix, el PDP-7, presentaba palabras de 18 bits correspondientes a medias palabras en 36 bits más grandes. computadoras. Estos fueron representados más naturalmente como seis dígitos octales (3 bits) ".
TL: DR: en todos los ISA modernos que tienen instrucciones de almacenamiento de bytes (incluido x86), son atómicos y no molestan a los bytes circundantes. (No conozco ningún ISA anterior en el que las instrucciones de almacenamiento de bytes puedan "inventar escrituras" a bytes vecinos tampoco).
El mecanismo de implementación real ( en las CPU que no son x86 ) es a veces un ciclo RMW interno para modificar una palabra completa en una línea de caché, pero eso se hace "invisiblemente" dentro de un núcleo mientras tiene la propiedad exclusiva de la línea de caché, por lo que solo es problema de rendimiento, no corrección. (Y la fusión en el búfer de la tienda a veces puede convertir las instrucciones de la tienda de bytes en una confirmación eficiente de palabra completa para la caché L1d).
Sobre la redacción de Stroustrup
No creo que sea una declaración 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 no almacenables en caché, por ejemplo, para MMIO).
Probablemente hubiera sido mejor hacer un ejemplo hipotético para hablar sobre modelos de memoria , en lugar de implicar que el hardware real es así. Pero si lo intentamos, tal vez podamos encontrar una interpretación que no sea tan obvia o totalmente errónea, que podría haber sido lo que Stroustrup estaba pensando cuando escribió esto para presentar el tema de los modelos de memoria. (Lo siento, esta respuesta es muy 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 al menos ocasionalmente hacen declaraciones erróneas.
Creo que Stroustrup está hablando sobre cómo las CPU funcionan internamente para implementar instrucciones de almacenamiento de bytes. Sugiere que una CPU sin un modelo de memoria razonable y bien definido podría implementar un almacén de bytes con un RMW no atómico de la palabra que lo contiene en una línea de caché, o en la memoria para una CPU sin caché.
Incluso esta afirmación más débil sobre el comportamiento interno (no visible externamente) no es cierto para las CPU x86 de alto rendimiento . Las CPU Intel modernas no tienen penalización de rendimiento para los almacenes de bytes, o incluso los almacenes de palabras o vectores no alineados que no cruzan un límite de línea de caché. AMD es similar.
Si los almacenes de bytes o no alineados tuvieran que hacer un ciclo de RMW ya que el almacén se comprometió con el caché L1D, interferiría con el almacenamiento y / o la carga de instrucciones / rendimiento de UOP de una manera que pudiéramos medir con contadores de rendimiento. (En un experimento cuidadosamente diseñado que evita la posibilidad de que la tienda se fusione en el búfer de la tienda antes de comprometerse con la caché L1d ocultando el costo, porque las unidades de ejecución de la tienda solo pueden ejecutar 1 tienda por reloj en las CPU actuales).
Sin embargo, algunos diseños de alto rendimiento para ISA que no son x86 utilizan un ciclo atómico RMW para confirmar internamente las tiendas en la caché L1d. ¿Hay alguna CPU moderna donde un almacén de bytes en caché sea realmente más lento que un almacén de palabras? La línea de caché permanece en estado MESI Exclusivo / Modificado todo el tiempo, por lo que no puede presentar ningún problema de corrección, solo un pequeño golpe de rendimiento. Esto es muy diferente de hacer algo que podría pisar las tiendas de otras CPU. (Los argumentos a continuación sobre que eso no suceda todavía se aplican, pero mi actualización puede haber perdido algunas cosas que aún argumentan que es poco probable que atomic cache-RMW).
(En muchos ISA que no son x86, los almacenes no alineados no son compatibles en absoluto, o se usan con menos frecuencia que en el software x86. Y los ISA con un orden débil permiten una mayor fusión en los almacenamientos intermedios de la tienda, por lo que no tantas instrucciones de almacenamiento de bytes resultan en un solo el byte se compromete con L1d. Sin estas motivaciones para hardware de acceso a caché sofisticado (hambriento de energía), la palabra RMW para tiendas de bytes dispersos es una compensación aceptable en algunos diseños).
Alpha AXP , un diseño RISC de alto rendimiento de 1992, famoso (y único entre las ISA modernas que no son DSP) omitió las instrucciones de carga / almacenamiento de bytes hasta Alpha 21164A (EV56) en 1996 . Aparentemente, no consideraron word-RMW como una opción viable para implementar almacenes de bytes, porque una de las ventajas citadas para implementar solo almacenes alineados de 32 y 64 bits era un ECC más eficiente para el caché L1D. "El ECC SECDED tradicional requeriría 7 bits adicionales sobre gránulos de 32 bits (22% de sobrecarga) frente a 4 bits adicionales sobre gránulos de 8 bits (50% de sobrecarga)". (La respuesta de @Paul A. Clayton sobre el direccionamiento de palabra vs. byte tiene algunas otras cosas interesantes de arquitectura de computadora.) Si los almacenes de bytes se implementaron con word-RMW, aún podría hacer la detección / corrección de errores con granularidad de palabras.
Las CPU Intel actuales solo usan paridad (no ECC) en L1D por este motivo. Vea estas preguntas y respuestas sobre el hardware (no) eliminando "almacenes silenciosos": verificar el contenido anterior de la memoria caché antes de la escritura para evitar marcar la línea sucia si coincidía requeriría un RMW en lugar de solo una tienda, y eso es un obstáculo importante.
Resulta que algunos diseños canalizados de alto rendimiento utilizan la palabra atómica-RMW para comprometerse con L1d, a pesar de que detiene la canalización de la memoria, pero (como argumento más adelante) es mucho menos probable que alguno haga un RMW externo visible a la RAM.
Word-RMW tampoco es una opción útil para las tiendas de bytes MMIO , por lo que, a menos que tenga una arquitectura que no necesite almacenes de sub-palabras para IO, necesitaría algún tipo de manejo especial para IO (como el escaso I / I de Alpha O espacio donde la carga / tiendas de palabras se asignaron a la carga / tiendas de bytes para que pueda usar tarjetas PCI de productos básicos en lugar de necesitar hardware especial sin registros de E / S de bytes.
Como señala @Margaret , los controladores de memoria DDR3 pueden almacenar bytes mediante la configuración de señales de control que ocultan otros bytes de una ráfaga. Los mismos mecanismos que llevan esta información al controlador de memoria (para almacenes no almacenados en caché) también podrían hacer que esa información se transfiera junto con una carga o almacenamiento al espacio MMIO. Por lo tanto, existen mecanismos de hardware para realmente hacer un almacenamiento de bytes incluso en sistemas de memoria orientados a ráfagas, y es muy probable que las CPU modernas lo usen en lugar de implementar un RMW, porque probablemente sea más simple y sea mucho mejor para la corrección de MMIO.
Cuántos y qué ciclos de tamaño se necesitarán para realizar la transferencia de palabras largas a la CPU muestra cómo un microcontrolador ColdFire señala el tamaño de transferencia (byte / palabra / palabra larga / línea de 16 bytes) con líneas de señal externas, lo que le permite realizar cargas de bytes / almacenar incluso si la memoria de 32 bits de ancho estaba conectada a su bus de datos de 32 bits. Algo como esto es presumiblemente típico para la mayoría de las configuraciones de bus de memoria (pero no lo sé). El ejemplo de ColdFire es complicado porque también es configurable para usar memoria de 16 u 8 bits, lo que requiere ciclos adicionales para transferencias más amplias. Pero no importa eso, el punto importante es que tiene señalización externa para el tamaño de transferencia, para decirle a la memoria HW qué byte está escribiendo realmente.
El siguiente párrafo de Stroustrup es
"El modelo de memoria C ++ garantiza que dos hilos de ejecución puedan actualizarse y acceder a ubicaciones de memoria separadas sin interferir entre sí . Esto es exactamente lo que esperaríamos ingenuamente. Es el trabajo del compilador protegernos de los comportamientos a veces muy extraños y sutiles de hardware moderno. Cómo una combinación de compilador y hardware logra eso depende del compilador ... "
Aparentemente, él piensa que el hardware moderno real puede no proporcionar una carga / almacenamiento de bytes "segura". Las personas que diseñan modelos de memoria de hardware están de acuerdo con las personas C / C ++, y se dan cuenta de que las instrucciones de almacenamiento de bytes no serían muy útiles para los programadores / compiladores si pudieran pisar bytes vecinos.
Todas las arquitecturas modernas (no DSP), excepto Alpha AXP anterior, tienen instrucciones de almacenamiento y carga de bytes, y AFAIK están definidas arquitectónicamente para no afectar los bytes vecinos. Sin embargo, logran eso en el 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 medias palabras, 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 compilador especial para implementar esta parte del modelo de memoria C ++, solo que
algunos
podrían necesitarlo.
Tal vez realmente solo está hablando de DSP direccionables por palabras en ese segundo párrafo (donde las implementaciones de C y C ++ a menudo usan
char
16 o 32 bits como exactamente el tipo de solución compiladora de la que Stroustrup estaba hablando).
La mayoría de las CPU "modernas" (incluidas todas las x86) tienen un caché L1D . Buscarán líneas completas de caché (generalmente 64 bytes) y rastrearán sucio / no sucio por línea de caché. Entonces, dos bytes adyacentes son casi exactamente lo mismo que 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 programador debe saber sobre la memoria de Ulrich Drepper. Tienes razón en que MESI (o un derivado como MESIF / MOESI) se asegura de que esto no sea un problema. (Pero nuevamente, esto se debe a que el hardware implementa un modelo de memoria sano).
Una tienda solo puede comprometerse con el caché L1D mientras la línea está en el estado Modificado (de MESI). Por lo tanto, 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 que la línea se invalide y vuelva a adquirido entre la lectura y la escritura. ( Si bien este caché tiene la línea en estado Modificado, ningún otro 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 acceso de registro, ya que todos los HW involucrados están en la misma etapa de la tubería, que simplemente puede detenerse por un ciclo adicional o dos.
Obviamente, eso es malo para el rendimiento y requiere hardware adicional para permitir que la 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 una exageración.
En un microcontrolador de un solo núcleo, la palabra interna RMW para las tiendas de bytes en caché sería más plausible, ya que no habrá solicitudes de invalidación provenientes de otros núcleos a las que tendrían que retrasar la respuesta durante una actualización atómica de la palabra caché RMW . Pero eso no ayuda para E / S a regiones que no se pueden almacenar en caché. Digo microcontrolador porque otros diseños de CPU de un solo núcleo suelen admitir algún tipo de SMP multi-socket.
Muchos ISA RISC no admiten cargas / almacenes de palabras no alineadas con una sola instrucción, pero ese es un problema separado (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 carga / almacenamiento no alineado en versiones recientes. (por ejemplo, MIPS32 / 64 Release 6 en 2014, y creo AArch64 y ARM reciente de 32 bits).
La cuarta edición del libro de Stroustrup se publicó en 2013 cuando Alpha había estado muerta durante años. La primera edición se publicó en 1985 , cuando RISC fue la nueva gran idea (por ejemplo, Stanford MIPS en 1983, de acuerdo con la línea de tiempo de Wikipedia para calcular HW , pero las CPU "modernas" en ese momento eran direccionables por bytes con tiendas de bytes. Cyber CDC 6600 era direccionable por palabras y probablemente todavía presente, pero no podría llamarse moderno.
Incluso las máquinas RISC muy orientadas a palabras como MIPS y SPARC tienen instrucciones de almacenamiento de bytes y carga de bytes (con signo o extensión cero). No admiten cargas de palabras no alineadas, lo que simplifica la memoria caché (o el acceso a la memoria si no hay memoria caché) y los puertos de carga, pero puede cargar cualquier byte único con una instrucción y, lo que es más importante, almacenar un byte sin ninguna arquitectura no visible. reescritura atómica de los bytes circundantes. (Aunque las tiendas en caché pueden
Supongo que C ++ 11 (que introduce un modelo de memoria compatible con subprocesos en el lenguaje) en Alpha necesitaría usar
char
32 bits si apunta a una versión de Alpha ISA sin almacenes de bytes.
O tendría que usar el software atomic-RMW con LL / SC cuando no pudiera probar que ningún otro subproceso podría tener un puntero que les permitiera escribir bytes vecinos.
IDK
cuán
lentas son las instrucciones de carga / almacenamiento de bytes en cualquier CPU donde se implementan en hardware pero no tan baratas como las cargas / tiendas de palabras
.
Las cargas de bytes son baratas en x86 siempre que use
movzx/movsx
para evitar dependencias falsas de registro parcial o paradas de fusión.
En AMD pre-Ryzen,
movsx
/
movzx
necesita una unidad adicional de ALU, pero de lo contrario, la extensión cero / signo se maneja directamente en el puerto de carga en las CPU Intel y AMD.
) La desventaja principal de x86 es que necesita una instrucción de carga por separado en lugar de usar un operando de memoria como fuente para una instrucción ALU (si está agregando un byte de extensión cero a un entero de 32 bits), lo que ahorra uop de front-end ancho de banda de rendimiento y tamaño de código.
O si solo está agregando un byte a un registro de byte, básicamente no hay inconveniente en x86.
Los ISA de almacenamiento de carga RISC siempre necesitan instrucciones de carga y almacenamiento 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 almacenes de bytes lentos podría poner cada
char
en su propia palabra y usar cargas / almacenes de palabras siempre que sea posible (por ejemplo, para estructuras externas globales y para locales en la pila).
IDK si alguna implementación real de MIPS / ARM / lo que sea tiene una carga / almacenamiento de bytes lenta, pero si es así, tal vez gcc tenga
-mtune=
opciones para controlarlo.
Eso no ayuda para
char[]
, o desreferenciar un
char *
cuando no sabes a dónde podría estar apuntando.
(Esto incluye
volatile char*
que usaría para MMIO.) Por lo tanto, tener el compilador + enlazador poner variables
char
en palabras separadas no es una solución completa, solo un truco de rendimiento si las tiendas de bytes verdaderos son lentas.
PD: Más sobre Alpha:
Alpha es interesante por muchas razones: uno de los pocos ISA de 64 bits de pizarra limpia, no una extensión de un ISA de 32 bits existente. Y uno de los ISA de pizarra limpia más recientes, Itanium es otro de varios años más tarde que intentó algunas ideas ingeniosas de arquitectura de CPU.
Del Linux Alpha HOWTO .
Cuando se introdujo la arquitectura Alpha, era única entre las arquitecturas RISC para evitar cargas y tiendas de 8 y 16 bits. Admitía cargas y almacenes de 32 y 64 bits (palabra larga y palabra cuádruple, en la nomenclatura de Digital). Los co-arquitectos (Dick Sites, Rich Witek) justificaron esta decisión citando las ventajas:
- El soporte de bytes en el subsistema de caché y memoria tiende a ralentizar los accesos para cantidades de 32 bits y 64 bits.
- El soporte de bytes dificulta la creación de circuitos de corrección de errores de alta velocidad en el subsistema de memoria / caché.
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 operaciones de cadena (por ejemplo, algunos de los puntos de referencia de bytes) muestran que Alpha funciona muy bien en la manipulación de bytes.