c# - doble - while con dos opciones
Simular el rasgado de un doble en C# (4)
Al investigar un poco, he encontrado algunas lecturas interesantes sobre las operaciones de punto flotante en arquitecturas x86:
Según Wikipedia , la unidad de punto flotante x86 almacenó valores de punto flotante en registros de 80 bits:
[...] los procesadores x86 subsiguientes integraron esta funcionalidad x87 en el chip, lo que convirtió a las instrucciones x87 en una parte integral de facto del conjunto de instrucciones x86. Cada registro x87, conocido como ST (0) a ST (7), tiene 80 bits de ancho y almacena números en el formato de precisión extendida doble estándar de punto flotante IEEE.
También se relaciona esta otra pregunta de SO: Cierta precisión de punto flotante y pregunta de límites numéricos
Esto podría explicar por qué, aunque los dobles son de 64 bits, se operan de forma atómica.
Estoy ejecutando en una máquina de 32 bits y puedo confirmar que los valores largos pueden desgarrarse utilizando el siguiente fragmento de código, que llega muy rápido.
static void TestTearingLong()
{
System.Threading.Thread A = new System.Threading.Thread(ThreadA);
A.Start();
System.Threading.Thread B = new System.Threading.Thread(ThreadB);
B.Start();
}
static ulong s_x;
static void ThreadA()
{
int i = 0;
while (true)
{
s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;
i++;
}
}
static void ThreadB()
{
while (true)
{
ulong x = s_x;
Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);
}
}
Pero cuando intento algo similar con dobles, no puedo rasgarme. ¿Alguien sabe por qué? Por lo que puedo decir de la especificación, solo la asignación a un flotador es atómica. La asignación a un doble debe tener un riesgo de desgarro.
static double s_x;
static void TestTearingDouble()
{
System.Threading.Thread A = new System.Threading.Thread(ThreadA);
A.Start();
System.Threading.Thread B = new System.Threading.Thread(ThreadB);
B.Start();
}
static void ThreadA()
{
long i = 0;
while (true)
{
s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
i++;
if (i % 10000000 == 0)
{
Console.Out.WriteLine("i = " + i);
}
}
}
static void ThreadB()
{
while (true)
{
double x = s_x;
System.Diagnostics.Debug.Assert(x == 0.0 || x == double.MaxValue);
}
}
Para lo que vale la pena este tema y código de muestra se puede encontrar aquí.
Tan extraño como suena, eso depende de tu CPU. Si bien no se garantiza que los dobles no se rompan, no lo harán en muchos procesadores actuales. Pruebe un AMD Sempron si desea desgarrarse en esta situación.
EDITAR: Aprendí que de la manera más dura hace unos años.
static double s_x;
Es mucho más difícil demostrar el efecto cuando usas un doble. La CPU utiliza instrucciones dedicadas para cargar y almacenar un doble, respectivamente FLD y FSTP. Es mucho más fácil con el tiempo, ya que no hay una sola instrucción que cargue / almacene un entero de 64 bits en el modo de 32 bits. Para observarlo, es necesario que la dirección de la variable esté desalineada, de modo que quede entre el límite de la línea de la memoria caché de la CPU.
Eso nunca sucederá con la declaración que utilizó, el compilador JIT se asegura de que el doble esté alineado correctamente, almacenado en una dirección que sea un múltiplo de 8. Podría almacenarlo en un campo de una clase, el asignador GC solo se alinea a 4 en Modo de 32 bits. Pero eso es un tiroteo.
La mejor manera de hacerlo es alinear el doble intencionalmente con un puntero. Ponga inseguro frente a la clase del programa y haga que se vea similar a esto:
static double* s_x;
static void Main(string[] args) {
var mem = Marshal.AllocCoTaskMem(100);
s_x = (double*)((long)(mem) + 28);
TestTearingDouble();
}
ThreadA:
*s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
ThreadB:
double x = *s_x;
Esto todavía no garantiza una buena desalineación (jeje) ya que no hay manera de controlar exactamente dónde AllocCoTaskMem () alineará la asignación relativa al inicio de la línea de caché de CPU. Y depende de la asociatividad del caché en su núcleo de CPU (el mío es un Core i5). Tendrá que juguetear con el offset, obtuve el valor 28 por experimentación. El valor debe ser divisible por 4 pero no por 8 para simular verdaderamente el comportamiento del montón del GC. Continúe agregando 8 al valor hasta que obtenga el doble para separar la línea de caché y activar la afirmación.
Para hacerlo menos artificial, tendrá que escribir un programa que almacene el doble en el campo de una clase y conseguir que el recolector de basura lo mueva en la memoria para que quede desalineado. Es un poco difícil encontrar un programa de ejemplo que garantice que esto suceda.
También tenga en cuenta cómo su programa puede demostrar un problema llamado compartir falso . Comente la llamada al método Start () para el subproceso B y observe cuánto se ejecuta el subproceso A más rápido. Usted está viendo el costo de la CPU manteniendo la línea de caché consistente entre los núcleos de la CPU. Compartir está destinado aquí ya que los hilos acceden a la misma variable. El verdadero intercambio falso ocurre cuando los hilos acceden a diferentes variables que están almacenadas en la misma línea de caché. Esta es la razón por la que la alineación es importante, solo se puede observar la rotura de un doble cuando parte de ella está en una línea de caché y parte de ella está en otra.