No tenemos que enseñarle a nadie sobre los tampones de la tienda.
Si eres programador y nunca has oído hablar de los buffers de las tiendas, deberías estar agradecido conmigo y con los gigantes en cuyos hombros estoy parado. Gracias a nosotros, nunca has oído hablar de ellos y nunca tienes que escucharlos en tu vida.
Ahora probablemente esté pensando “¿qué son las memorias intermedias de las tiendas y por qué debería importarme no tener que saber sobre ellas? Nunca he entrado en contacto con buffers de tiendas en toda mi vida “.
Pero si estás pensando eso, estás muy equivocado. Probablemente esté leyendo esto en un dispositivo que usa memorias intermedias de la tienda, es decir, si está usando un procesador Intel o AMD (y probablemente los procesadores ARM también tienen memorias intermedias de la tienda, pero no lo sabría).
¿Lo que ellos son? Pequeñas listas de tareas. Verá, los procesadores son procrastinadores del peor tipo (y estoy seguro de que he dicho esto antes). Cuando ejecutan una declaración que se supone que cambia el valor de algunas variables, piensan “ugh … el camino hacia la memoria compartida es demasiado largo … y luego tendré que estar en la cola y esperar hasta que haya un asiento libre en el bus de memoria …
Entonces simplemente no cambian el valor de la variable. Simplemente agregan un elemento al final de su lista de tareas: “[math] \ Box [/ math] change variable x to value 5” o algo así. Y eventualmente, cuando “el momento es correcto”, cuando están “de humor”, el procesador cambia la variable. A menudo son cientos de instrucciones más tarde.
Como probablemente pueda imaginar, esto tiene efectos muy divertidos en su programa, al menos si tiene múltiples hilos. El ejemplo clásico es un bloqueo de software, donde los subprocesos se dicen entre sí que desean ingresar a la sección crítica, pero luego solo ingresan a la sección crítica si el otro subproceso tampoco quiere hacerlo:
int enterCrit1 = 0, enterCrit2 = 0;
int z = 0;
Thread1 vacío () {
enterCrit1 = 1; // anuncia la intención de ingresar a la sección crítica
if (enterCrit2 == 0) {
// si el hilo 2 tampoco anunciaba intención, ingrese a la sección crítica
z ++;
}
}
Thread2 vacío () {
enterCrit2 = 1;
if (enterCrit1 == 0) {// sección crítica
z ++;
}
}
Ahora esperaría que uno de los hilos haga el primer paso, cambie la variable y, por lo tanto, el otro hilo verá el nuevo valor de 1 y no ingrese a la sección crítica.
Pero en una máquina Intel, el procesador “eventualmente lo logrará” y las variables no se cambiarán por un tiempo, por lo tanto, ambos hilos pueden (y probablemente lo harán) entrar en la sección crítica.
Entonces, ¿por qué nunca has oído hablar de esto? Debo estar burlándote de ti, ¿verdad?
No. Puede leer todo sobre esto en el manual del desarrollador de software de arquitecturas Intel® 64 e IA-32, volumen 3A: Guía de programación del sistema, parte 1.
La razón por la que nunca escuchó sobre esto es simple: si tradujo el programa anterior a C, el programa tendría una semántica indefinida (debido a, ya sabes, almacenar buffers). Para obtener una semántica definida, debe agregar algunas anotaciones de programa, mediante las cuales el programa se puede compilar correctamente. Las carreras, es decir, los accesos a la memoria de diferentes subprocesos a la misma variable que pueden ocurrir “simultáneamente” (y uno está cambiando el valor), deben ser anotados de alguna manera. En C / C ++, debe marcar las variables como atomic
, en Java debe anotar las variables como volatile
.
Ahora aquí está la pregunta del premio: ¿la variable z
necesita ser anotada también? En el modelo computacional ingenuo (sin almacenamientos intermedios de la tienda) la respuesta es no, porque como discutimos anteriormente, solo uno de los hilos puede ingresar a la sección crítica y acceder a z
. Pero en el hardware real (con almacenamientos intermedios de la tienda), la respuesta sería al principio sí, porque ambos hilos pueden poner sus tareas en la lista de tareas y entrar en la sección crítica, accediendo simultáneamente a z
.
El punto central de lo que muestro * en mi tesis es que no tiene que mirar el modelo computacional con buffers de la tienda. Por lo tanto, simplemente puede ver todas las razas en el modelo ingenuo, donde múltiples hilos no acceden a z
, y anotar solo las razas en ese modelo. Eso es suficiente para que el compilador se asegure de que su programa no se vea afectado por los búferes de la tienda (agregando instrucciones de sincronización lenta en los lugares correctos) y, por lo tanto, incluso cuando se ejecuta en el hardware real, nunca tendrá una carrera en z
.
Para resumir, no necesita saber qué hacen los buffers de la tienda o incluso si existen. Simplemente mire su programa como siempre lo ha hecho, anote las carreras que encuentre, y su programa se ejecutará en procesadores Intel y AMD como si no tuvieran las memorias intermedias de la tienda que ni siquiera sabe que existen.
(*) No soy el primero en mostrar ese resultado, Ernie Cohen y Norbert Schirmer lo fueron, pero extiendo su resultado en varios frentes, por ejemplo, a arquitecturas con accesos de media palabra, dispositivos, código de sistema operativo que se ejecuta código de usuario compilado no confiable (imagine que está compilado a partir del código C donde las anotaciones son incorrectas o no existen), interrupciones entre procesadores, etc. Muchas cosas que realmente hacen temblar el barco rocks