2012-01-25 11 views
17

Me estoy ejecutando en una máquina de 32 bits y puedo confirmar que los valores largos pueden rasgarse usando el siguiente fragmento de código que se ejecuta muy rápido.Simular rasgar un doble en C#

 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 los dobles, no consigo desgarrarme. ¿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); 
     } 
    } 
+4

Pregunta estúpida: ¿qué es lagrimeo? – Oded

+0

operaciones en ints son garantizadas para ser atómicas en cuanto al acceso por múltiples hilos. No es así con los largos. Lagrimeo consiste en obtener una mezcla de dos valores provisionales (malo). Se pregunta por qué no se ve lo mismo en dobles, ya que los dobles tampoco garantizan las operaciones atómicas. – hatchet

+13

@Oded: en máquinas de 32 bits, solo se escriben 32 bits a la vez. Si escribe un valor de 64 bits en una máquina de 32 bits y escribe en la misma dirección al mismo tiempo en dos hilos diferentes, en realidad tiene * cuatro * escrituras, no * dos *, porque las escrituras se hacen 32 bits en un momento. Por lo tanto, es posible que los hilos corran, y cuando el humo se borre, la variable contiene los 32 bits superiores escritos por un hilo, y los 32 bits inferiores escritos por el otro. Entonces puede escribir 0xDEADBEEF00000000 en un hilo y 0x00000000BAADF00D en otro, y terminar con 0x0000000000000000 en la memoria. –

Respuesta

10
static double s_x; 

Es mucho más difícil de demostrar el efecto cuando se utiliza un doble. La CPU utiliza instrucciones específicas para cargar y almacenar un doble, respectivamente FLD y FSTP. Es mucho más fácil con largo ya que no hay instrucciones únicas que carguen/almacenen un entero de 64 bits en el modo de 32 bits. Para observarlo, es necesario que la dirección de la variable esté desalineada para que se extienda a ambos lados del límite de la línea de caché de la CPU.

Eso nunca sucederá con la declaración que utilizó, el compilador JIT asegura que el doble está alineado correctamente, almacenado en una dirección que es un múltiplo de 8. Puede almacenarlo en un campo de una clase, el asignador GC solamente se alinea a 4 en modo de 32 bits. Pero eso es una mierda.

La mejor manera de hacerlo es alineando incorrectamente el doble utilizando un puntero.Ponga insegura frente a la clase del Programa y hacer que parezca 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 garantizará una buena desalineación (jeje) ya que no hay manera de controlar exactamente donde AllocCoTaskMem() se alineará la asignación relativa al inicio de la línea de caché de la CPU. Y depende de la asociatividad del caché en su núcleo de la CPU (el mío es un Core i5). Tendrás que jugar con el desplazamiento, obtuve el valor 28 por experimentación. El valor debe ser divisible entre 4 pero no entre 8 para simular realmente el comportamiento del montón de GC. Siga agregando 8 al valor hasta que obtenga el doble para montar a horcajadas en la línea de caché y activar la afirmación.

Para hacerlo menos artificial tendrás 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 muestra que asegure esto sucede.

También tenga en cuenta cómo su programa puede mostrar un problema llamado compartir falsa. Comente la llamada al método Start() para el subproceso B y observe cuánto más rápido se ejecuta el subproceso A. Está viendo el costo de la CPU manteniendo constante la línea de caché entre los núcleos de la CPU. Compartir está pensado aquí ya que los hilos acceden a la misma variable. El intercambio falso real ocurre cuando los subprocesos tienen acceso a diferentes variables que están almacenadas en la misma línea de caché. Por lo demás, esta es la razón por la cual la alineación es importante, solo se puede observar el rasgado de un doble cuando parte de él está en una línea de caché y parte de él está en otra.

+0

No entiendo cómo el cruce de límite de la línea de caché puede causar desgarro. Pensé que esto solo se debía a que el valor ocupaba más espacio que el tamaño de un registro. ¿Puedes explicar esto un poco más? – Tudor

+0

@Tudor: es un efecto completamente diferente, no asociado con el tamaño del registro. Concéntrese en el último párrafo, observe cómo la sincronización de caché de la CPU tiene una línea de caché como la unidad de actualización. Un doble desalineado que se extiende a lo largo de una línea requiere * dos * actualizaciones, similar a la forma en que un largo requiere dos escrituras de registro. Lo cual toma suficiente tiempo para permitir que el código que se ejecuta en otro núcleo observe el desgarro. –

11

Por extraño que parezca, eso depende de su CPU. Mientras que los dobles son no garantizados no se rompen, no lo harán en muchos procesadores actuales. Prueba un AMD Sempron si quieres desgarrarte en esta situación.

EDIT: aprendido de la manera difícil hace unos años.

+0

¿TIENE esto que ver con el tamaño de los registros de coma flotante? – leppie

+0

TBH No tengo la menor idea, nunca lo analicé. Un daemon mío (Free Pascal de todos los idiomas) comenzó a producir resultados absurdos en una y solo una máquina de muchas (tal vez 100), todas configuradas a partir de la misma imagen, etc. Resultó que era un doble global que se actualizó por el hilo principal y un hilo secundario creado por GTK. No hay primitivas de bloqueo en FPK, entonces ... (improperio, improperio) –

+0

Sí, no lo dudo si las extensiones MMX o SSE en la CPU tienen algo que ver con esto. – antiduh

0

Hacer algo de investigación, he encontrado algunas interesantes lee acerca de las operaciones de punto flotante en arquitecturas x86:

valores de punto flotante

Según Wikipedia, el x86 unidad de coma flotante almacenados en los registros de 80 bits:

[...] procesadores x86 posteriores después se integra esta funcionalidad x87 en el chip que hizo que el facto una parte de las instrucciones x87 integral de el 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 IEEE de punto flotante .

También esta pregunta otra SO está relacionado: Some floating point precision and numeric limits question

Esto podría explicar por qué, a pesar de los dobles son de 64 bits, que son operadas en forma atómica.

0

Por lo que vale, este tema y el ejemplo de código se pueden encontrar aquí.

http://msdn.microsoft.com/en-us/magazine/cc817398.aspx

+0

Ese artículo solo habla de largo, no doble. – Tudor

+0

De acuerdo. En realidad, creo que el código de muestra que publiqué en la pregunta es de esa publicación (excepto para las cosas dobles). (Lo tenía en un proyecto de prueba y lo había olvidado por un tiempo). –