Pregunta

Esta pregunta y mi respuesta a continuación se encuentran principalmente en respuesta a un área de confusión en otra pregunta.

Al final de la respuesta, hay algunas cuestiones WRT y sincronización de hilos "volátil" que no estoy del todo seguro sobre - Doy la bienvenida a comentarios y respuestas alternativas. El punto de la cuestión se relaciona principalmente con registros de la CPU y la forma en que se utilizan, sin embargo.

¿Fue útil?

Solución

registros de la CPU son pequeñas áreas de almacenamiento de datos sobre el silicio de la CPU. Para la mayoría de las arquitecturas, que son el principal lugar todas las operaciones ocurren (los datos se carga en la memoria, operado, y empujó de vuelta).

hilo

Lo que se está ejecutando utiliza los registros y posee el puntero de instrucción (que dice que la enseñanza que viene a continuación). Cuando las permutas de OS en otro hilo, todos del estado de la CPU, incluyendo los registros y el puntero de instrucción, se guardan en algún lugar, el secado por congelación con eficacia el estado de la rosca para cuando el próximo vuelve a la vida.

Mucho más documentación sobre todo esto, por supuesto, todo el lugar. Wikipedia sobre los registros. Wikipedia sobre el cambio de contexto. para empezar. Editar: o leer la respuesta de Steve314. :)

Otros consejos

Los registros son la "memoria de trabajo" en una CPU. Son muy rápidos, pero un recurso muy limitado. Por lo general, una CPU tiene un pequeño conjunto fijo de registros con nombre, los nombres que forman parte de la convención de lenguaje ensamblador para que el código máquina de CPU. Por ejemplo, 32-bit CPU Intel x86 tiene cuatro principales registros de datos denominado EAX, EBX, ECX y EDX, junto con una serie de indexación y otros registros más especializados.

En sentido estricto, esto no es del todo cierto en estos días - registro el cambio de nombre, por ejemplo, es común. Algunos procesadores tienen suficientes registros que sean contados en lugar de nombrarlos etc. Sigue siendo, sin embargo, un buen modelo básico para trabajar. Por ejemplo, regístrese cambio de nombre se utiliza para conservar la ilusión de este modelo básico a pesar de ejecución fuera de orden.

El uso de registros en ensamblador escrito manualmente tiende a tener un patrón simple de utilización registro. Unas pocas variables se mantienen exclusivamente en los registros para la duración de una subrutina, o alguna parte sustancial del mismo. Otros registros se utilizan en un patrón de lectura-modificación-escritura. Por ejemplo ...

mov eax, [var1]
add eax, [var2]
mov [var1], eax

IIRC, que es válida (aunque probablemente ineficaz) x86 código ensamblador. En un Motorola 68000, podría escribir ...

move.l [var1], d0
add.l  [var2], d0
move.l d0, [var1]

Esta vez, la fuente suele ser el parámetro de la izquierda, con el destino de la derecha. El 68000 tenían 8 registros de datos (d0..d7) y 8 registros de dirección (a0..a7), con a7 IIRC que también sirve como el puntero de pila.

En un (nuevo en el viejo Commodore 64) 6510 Me podría escribir ...

lda    var1
adc    var2
sta    var1

Los registros aquí son en su mayoría implícito en las instrucciones - el uso, sobre todo, la A (acumulador) se registra

.

Por favor, perdona los errores tontos en estos ejemplos - No he escrito una cantidad significativa de ensamblador "real" (en lugar de virtual) durante al menos 15 años. El principio es el punto, sin embargo.

Uso de registros es específica para un fragmento de código particular. Lo que un registro contiene es, básicamente, cualquiera que sea la última instrucción ha quedado en ella. Es la responsabilidad del programador para realizar un seguimiento de lo que es en cada registro en cada punto en el código.

Cuando se llama a una subrutina, o bien la persona que llama o el destinatario de la llamada debe asumir la responsabilidad de asegurar que no hay conflicto, lo que generalmente significa que los registros se guardan a la pila en el inicio de la llamada y luego se vuelven a leer al final. Cuestiones similares se producen con las interrupciones. Cosas como quién es responsable de guardar los registros de llamadas (o destinatario) son típicamente una parte de la documentación de cada subprograma.

Un compilador normalmente decidirá cómo utilizar registros de una manera mucho más sofisticado que un programador humano, pero opera en los mismos principios. El mapeo de los registros a determinadas variables es dinámica y varía drásticamente según el cual el fragmento de código que está viendo. Guardar y restaurar los registros se maneja en su mayoría de acuerdo con las convenciones estándar, aunque el compilador puede improvisar "llamando a medida convenciones" en algunas circunstancias.

Por lo general, las variables locales de una función se imaginaron que vivo en la pila. Esta es la regla general con las variables "auto" en C. Desde "auto" es el valor por defecto, estos son variables locales normales. Por ejemplo ...

void myfunc ()
{
  int i;  //  normal (auto) local variable
  //...
  nested_call ();
  //...
}

En el código anterior, "i" puede muy bien llevará a cabo principalmente en un registro. Incluso puede mover de un registro a otro y de vuelta como los progresos de función. Sin embargo, cuando "nested_call" se llama, el valor de dicho registro será casi con certeza en la pila - ya sea porque la variable es una variable de pila (no un registro), o porque el contenido del registro se guardan para permitir nested_call su propio almacenamiento de trabajo .

En una aplicación de múltiples hilos, las variables locales normales la hora local de un hilo en particular. Cada hilo tiene su propia pila, y mientras se está ejecutando, el uso exclusivo de los registros de la CPU. En un cambio de contexto, esos registros se guardan. si in registros o en la pila, las variables locales no se comparten entre los hilos.

Esta situación básica se conserva en una aplicación de múltiples núcleos, aunque dos o más hilos pueden estar activos al mismo tiempo. Cada núcleo tiene su propia pila y sus propios registros.

Los datos almacenados en la memoria compartida requiere más cuidado. Esto incluye variables globales, las variables estáticas dentro de ambas clases y funciones, y los objetos del montón asignados. Por ejemplo ...

void myfunc ()
{
  static int i;  //  static variable
  //...
  nested_call ();
  //...
}

En este caso, el valor de "i" se conserva entre llamadas a funciones. Una región estática de la memoria principal está reservado para almacenar este valor (de ahí el nombre de "estática"). En principio, no hay necesidad de ninguna acción especial para preservar "i" durante la llamada a "nested_call", y, a primera vista, la variable puede accederse desde cualquier hilo que se ejecuta en cualquier núcleo (o incluso en una CPU aparte).

Sin embargo, el compilador sigue trabajando duro para optimizar la velocidad y el tamaño de su código. Repetida lee y escribe en la memoria principal son mucho más lentos que los accesos de registro. El compilador es casi seguro que elija no para seguir la sencilla lectura-modificación-escritura patrón descrito anteriormente, sino que en su lugar mantener el valor en el registro durante un período relativamente prolongado, evitando repetidas lecturas y escrituras en el mismo la memoria.

Esto significa que las modificaciones realizadas en un hilo no puede ser visto por otro hilo durante algún tiempo. Dos hilos podrían llegar a tener ideas muy diferentes sobre el valor de "i" anterior.

No hay una solución mágica de hardware para esto. Por ejemplo, no existe un mecanismo para sincronizar el registro entre los hilos. Para la CPU, la variable y el registro son completamente entidades separadas - no se sabe que tienen que estar sincronizados. Ciertamente no hay sincronización entre registros en diferentes hilos o se ejecutan en diferentes núcleos -. No hay razón para creer que otro hilo está utilizando el mismo registro con el mismo propósito en un momento determinado

Una solución parcial es bandera una variable como "volátil" ...

void myfunc ()
{
  volatile static int i;
  //...
  nested_call ();
  //...
}

Esto le dice al compilador que no optimizan la lee y escribe en la variable. El procesador no tiene un concepto de volatilidad. Esta palabra clave indica al compilador para generar código diferente, haciendo inmediata de lectura y escritura a la memoria como se especifica en las tareas, en lugar de evitar los accesos mediante el uso de un registro.

Este es no una solución de sincronización multi-hilo, sin embargo - al menos no en sí mismo. Una solución multi-hilo adecuada es utilizar algún tipo de bloqueo para administrar el acceso a este "recurso compartido". Por ejemplo ...

void myfunc ()
{
  static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

No está más en todo esto que es inmediatamente obvio. En principio, en lugar de escribir el valor de "i" de nuevo a su variable listo para la llamada "release_lock_on_i", podría ser guardado en la pila. En lo que se refiere al compilador, esto no es razonable. Que está haciendo el acceso pila de todos modos (por ejemplo, el ahorro de la dirección de retorno), por lo que el ahorro del registro en la pila puede ser más eficiente que escribirlo de nuevo a "i" -. Más caché de usar que el acceso a un bloque completamente separada de la memoria

Lamentablemente, sin embargo, la función de bloqueo de liberación no sabe que la variable no ha sido de nuevo a la memoria escrita, sin embargo, por lo que no puede hacer nada para solucionarlo. Después de todo, esa función es sólo una llamada de biblioteca (el abrepuertas real puede estar oculto en una llamada más profundamente anidado) y que la biblioteca puede haber sido compilado años antes de su aplicación - que no sabe ¿Cómo sus llamadores utilizan registros o la pila. Esa es una gran parte de por qué se utiliza una pila, y por eso las convenciones de llamada que ser estandarizada (por ejemplo, que guarda los registros). La función de bloqueo de liberación no puede obligar a las personas que llaman a los registros "Sincronizar".

Del mismo modo, es posible vincular una aplicación antigua por una nueva biblioteca - la persona que llama no sabe lo que hace "release_lock_on_i" o cómo, es sólo una llamada de función. Lo haceno sabe que se necesita para salvar registros de vuelta a la memoria en primer lugar.

Para resolver esto, podemos traer de vuelta el "volátil".

void myfunc ()
{
  volatile static int i;
  //...
  acquire_lock_on_i ();
  //  do stuff with i
  release_lock_on_i ();
  //...
}

puede utilizar una variable local normales temporalmente mientras el bloqueo está activo, para dar el compilador la oportunidad de utilizar un registro para ese período breve. En principio, sin embargo, un bloqueo debe ser liberado tan pronto como sea posible, lo que no debería ser mucho código en allí. Si lo hacemos, sin embargo, escribimos nuestra espalda variable temporal para "i" antes de liberar el bloqueo, y la volatilidad de la "i" asegura que se ha escrito de nuevo a la memoria principal.

En principio, esto no es suficiente. Escrito a la memoria principal no significa que haya escrito a la memoria principal - hay capas de caché para atravesar en el medio, y sus datos podría sentarse en cualquiera de las capas por un tiempo. Hay un problema de "memoria barrera" aquí, y no sé mucho acerca de esto -. Pero afortunadamente este problema es responsabilidad de sincronización de subproceso llama tales como los adquieren de bloqueo y liberación de las llamadas anteriores

Este problema barrera de memoria no elimina la necesidad de que la palabra clave "volátil", sin embargo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top