Instrucciones de SSE: ¿qué CPU pueden hacer operaciones de memoria atómica 16B?


Considere un solo acceso a la memoria (una sola lectura o una sola escritura, no lectura+escritura) instrucción SSE en una CPU x86. La instrucción está accediendo a 16 bytes (128 bits) de memoria y la ubicación de la memoria accedida está alineada a 16 bytes.

El documento "Intel® 64 Architecture Memory Ordering White Paper" indica que para "Instrucciones que leen o escriben una palabra cuádruple (8 bytes) cuya dirección está alineada en un límite de 8 bytes", la operación de memoria parece ejecutarse como un acceso a memoria única independientemente del tipo de memoria.

La pregunta: ¿Existen CPU Intel/AMD/etc x86 que garanticen que la lectura o escritura de 16 bytes (128 bits) alineados a un límite de 16 bytes se ejecuta como un solo acceso a la memoria? Es así, qué tipo particular de CPU es (Core2/Atom/K8/Phenom/...)? Si proporciona una respuesta (sí/no) a esta pregunta, también especifique el método que se utilizó para determinar la respuesta: búsqueda de documentos PDF, pruebas de fuerza bruta, pruebas matemáticas o lo que sea otro método que usaste para determinar la respuesta.

Esta pregunta se refiere a problemas como http://research.swtch.com/2010/02/off-to-races.html


Actualización:

He creado un programa de prueba simple en C que se puede ejecutar en sus equipos. Por favor compílelo y ejecútelo en su CPU Phenom, Athlon, Bobcat, Core2, Atom, Sandy Bridge o cualquier CPU con capacidad SSE2 que tenga. Gracias.

// Compile with:
//   gcc -o a a.c -pthread -msse2 -std=c99 -Wall -O2
//
// Make sure you have at least two physical CPU cores or hyper-threading.

#include <pthread.h>
#include <emmintrin.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>

typedef int v4si __attribute__ ((vector_size (16)));
volatile v4si x;

unsigned n1[16] __attribute__((aligned(64)));
unsigned n2[16] __attribute__((aligned(64)));

void* thread1(void *arg) {
        for (int i=0; i<100*1000*1000; i++) {
                int mask = _mm_movemask_ps((__m128)x);
                n1[mask]++;

                x = (v4si){0,0,0,0};
        }
        return NULL;
}

void* thread2(void *arg) {
        for (int i=0; i<100*1000*1000; i++) {
                int mask = _mm_movemask_ps((__m128)x);
                n2[mask]++;

                x = (v4si){-1,-1,-1,-1};
        }
        return NULL;
}

int main() {
        // Check memory alignment
        if ( (((uintptr_t)&x) & 0x0f) != 0 )
                abort();

        memset(n1, 0, sizeof(n1));
        memset(n2, 0, sizeof(n2));

        pthread_t t1, t2;
        pthread_create(&t1, NULL, thread1, NULL);
        pthread_create(&t2, NULL, thread2, NULL);
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);

        for (unsigned i=0; i<16; i++) {
                for (int j=3; j>=0; j--)
                        printf("%d", (i>>j)&1);

                printf("  %10u %10u", n1[i], n2[i]);
                if(i>0 && i<0x0f) {
                        if(n1[i] || n2[i])
                                printf("  Not a single memory access!");
                }

                printf("\n");
        }

        return 0;
}

La CPU que tengo en mi portátil es Core Duo (no Core2). Este CPU particular falla la prueba, implementa lectura/escritura de memoria de 16 bytes con una granularidad de 8 bytes. La salida es:

0000    96905702      10512
0001           0          0
0010           0          0
0011          22      12924  Not a single memory access!
0100           0          0
0101           0          0
0110           0          0
0111           0          0
1000           0          0
1001           0          0
1010           0          0
1011           0          0
1100     3092557       1175  Not a single memory access!
1101           0          0
1110           0          0
1111        1719   99975389
Author: Peter Cordes, 2011-10-04

6 answers

En el Manual del Desarrollador de Arquitecturas Intel® 64 e IA-32: Vol. 3A , que hoy en día contiene las especificaciones del libro blanco de ordenación de memoria que menciona, se dice en la sección 8.2.3.1, como usted mismo observa, que

The Intel-64 memory ordering model guarantees that, for each of the following 
memory-access instructions, the constituent memory operation appears to execute 
as a single memory access:

• Instructions that read or write a single byte.
• Instructions that read or write a word (2 bytes) whose address is aligned on a 2
byte boundary.
• Instructions that read or write a doubleword (4 bytes) whose address is aligned
on a 4 byte boundary.
• Instructions that read or write a quadword (8 bytes) whose address is aligned on
an 8 byte boundary.

Any locked instruction (either the XCHG instruction or another read-modify-write
 instruction with a LOCK prefix) appears to execute as an indivisible and 
uninterruptible sequence of load(s) followed by store(s) regardless of alignment.

Ahora, dado que la lista anterior NO contiene el mismo lenguaje para double quadword (16 bytes), se deduce que la arquitectura NO garantiza que las instrucciones que acceden a 16 bytes de memoria sean atómicas.

Dicho esto, el el último párrafo hace alusión a una salida, a saber, la instrucción CMPXCHG16B con el prefijo de BLOQUEO. Puede usar la instrucción CPUID para averiguar si su procesador admite CMPXCHG16B (el bit de característica "CX16").

En el documento AMD correspondiente, AMD64 Technology AMD64 Architecture Programmer's Manual Volume 2: System Programming, no puedo encontrar un lenguaje claro similar.

EDITAR: Resultados del programa de prueba

(Programa de prueba modificado para aumentar # iteraciones por un factor de 10)

En un Xeon X3450 (x86-64):

0000   999998139       1572
0001           0          0
0010           0          0
0011           0          0
0100           0          0
0101           0          0
0110           0          0
0111           0          0
1000           0          0
1001           0          0
1010           0          0
1011           0          0
1100           0          0
1101           0          0
1110           0          0
1111        1861  999998428

En un Xeon 5150 (32 bits):

0000   999243100     283087
0001           0          0
0010           0          0
0011           0          0
0100           0          0
0101           0          0
0110           0          0
0111           0          0
1000           0          0
1001           0          0
1010           0          0
1011           0          0
1100           0          0
1101           0          0
1110           0          0
1111      756900  999716913

En un Opteron 2435 (x86-64):

0000   999995893       1901
0001           0          0
0010           0          0
0011           0          0
0100           0          0
0101           0          0
0110           0          0
0111           0          0
1000           0          0
1001           0          0
1010           0          0
1011           0          0
1100           0          0
1101           0          0
1110           0          0
1111        4107  999998099

¿Significa esto que Intel y/o AMD garantizan que los accesos a memoria de 16 bytes son atómicos en estas máquinas? En mi humilde opinión, no lo hace. No está en la documentación como comportamiento arquitectónico garantizado, y por lo tanto uno no puede saber si en estos procesadores particulares los accesos a memoria de 16 bytes son realmente atómicos o si el programa de prueba simplemente falla en activarlos por una razón u otra. Y por lo tanto confiar en ella es peligroso.

EDITAR 2: Cómo hacer que el programa de prueba falle

Ha! Logré que el programa de prueba fallara. En el mismo Opteron 2435 que el anterior, con el mismo binario, pero ahora ejecutándolo a través de la herramienta" numactl " especificando que cada hilo se ejecuta en un socket separado, obtuve:

0000   999998634       5990
0001           0          0
0010           0          0
0011           0          0
0100           0          0
0101           0          0
0110           0          0
0111           0          0
1000           0          0
1001           0          0
1010           0          0
1011           0          0
1100           0          1  Not a single memory access!
1101           0          0
1110           0          0
1111        1366  999994009

Entonces, ¿qué implica esto? Bueno, el Opteron 2435 puede, o no, garantizar que los accesos a memoria de 16 bytes son atomic para accesos intra-socket, pero al menos el protocolo de coherencia de caché que se ejecuta en la interconexión HyperTransport entre los dos sockets no proporciona tal garantía.

EDITAR 3: ASM para las funciones de hilo, a petición de " GJ."

Aquí está el asm generado para las funciones de subproceso para la versión GCC 4.4 x86-64 utilizada en el sistema Opteron 2435:


.globl thread2
        .type   thread2, @function
thread2:
.LFB537:
        .cfi_startproc
        movdqa  .LC3(%rip), %xmm1
        xorl    %eax, %eax
        .p2align 5,,24
        .p2align 3
.L11:
        movaps  x(%rip), %xmm0
        incl    %eax
        movaps  %xmm1, x(%rip)
        movmskps        %xmm0, %edx
        movslq  %edx, %rdx
        incl    n2(,%rdx,4)
        cmpl    $1000000000, %eax
        jne     .L11
        xorl    %eax, %eax
        ret
        .cfi_endproc
.LFE537:
        .size   thread2, .-thread2
        .p2align 5,,31
.globl thread1
        .type   thread1, @function
thread1:
.LFB536:
        .cfi_startproc
        pxor    %xmm1, %xmm1
        xorl    %eax, %eax
        .p2align 5,,24
        .p2align 3
.L15:
        movaps  x(%rip), %xmm0
        incl    %eax
        movaps  %xmm1, x(%rip)
        movmskps        %xmm0, %edx
        movslq  %edx, %rdx
        incl    n1(,%rdx,4)
        cmpl    $1000000000, %eax
        jne     .L15
        xorl    %eax, %eax
        ret
        .cfi_endproc

Y para completar, .LC3 que son los datos estáticos que contienen el vector (-1, -1, -1, -1) utilizado por thread2:


.LC3:
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .ident  "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
        .section        .note.GNU-stack,"",@progbits

También tenga en cuenta que esta es la sintaxis de AT&T ASM, no la sintaxis de Intel con la que los programadores de Windows podrían estar más familiarizados. Finalmente, esto es con march = native lo que hace que GCC prefiera MOVAPS; pero no importa, si uso march=core2 usará MOVDQA para almacenar en x, y aún puedo reproducir los fallos.

 32
Author: janneb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-07 13:05:51

El "AMD Architecture Programmer's Manual Volume 1: Application Programming" dice en la sección 3.9.1: "CMPXCHG16B se puede usar para realizar accesos atómicos de 16 bytes en modo de 64 bits (con ciertas restricciones de alineación)."

Sin embargo, no hay tal comentario sobre las instrucciones SSE. De hecho, hay un comentario en 4.8.3 que el prefijo de BLOQUEO "causa una excepción de opcode no válido cuando se usa con instrucciones de medios de 128 bits". Por lo tanto, me parece bastante concluyente que la AMD los procesadores NO garantizan accesos atómicos de 128 bits para instrucciones SSE, y la única manera de hacer un acceso atómico de 128 bits es usar CMPXCHG16B.

El " Intel 64 and IA-32 Architectures Software Developer's Manual Volume 3A: System Programming Guide, Part 1" dice en 8.1.1 "Una instrucción x87 o una instrucción SSE que accede a datos más grandes que un quadword puede implementarse utilizando múltiples accesos de memoria."Esto es bastante concluyente de que las instrucciones SSE de 128 bits no están garantizadas atómica por la ISA. El volumen 2A de los documentos de Intel dice de CMPXCHG16B: "Esta instrucción se puede usar con un prefijo de BLOQUEO para permitir que la instrucción se ejecute atómicamente."

Además, los fabricantes de CPU no han publicado garantías escritas de las operaciones atómicas 128b SSE para modelos de CPU específicos donde ese es el caso.

 4
Author: Anthony Williams,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-10 04:14:24

En realidad hay una advertencia en el Manual de Arquitectura de Intel Vol. 3A. Sección 8.1.1 (mayo de 2011), bajo la sección de operaciones atómicas garantizadas:

Una instrucción x87 o una instrucción SSE que accede a datos más grandes que un quadword puede ser implementado usando múltiples accesos de memoria. Si tales almacenes de la instrucción a la memoria, algunos de los accesos pueden completa (escribiendo en memoria) mientras que otro hace que la operación falta por razones arquitectónicas (por ejemplo, debido a un página-entrada de tabla que es marcado como "no presente"). En este caso, los efectos de la los accesos pueden ser visibles para el software a pesar de que el la instrucción causó un fallo. Si la invalidación de TLB se ha retrasado (ver Sección 4.10.4.4), tales fallas de página pueden ocurrir incluso si todos los accesos son a la misma página.

Por lo tanto, no se garantiza que las instrucciones SSE sean atómicas, incluso si la arquitectura subyacente usa un solo acceso a la memoria (esta es una de las razones por las que la memoria se introdujo la esgrima).

Combine eso con esta declaración del Manual de Optimización de Intel, Sección 13.3 (abril de 2011)

Las instrucciones AVX y FMA no introducen ningún nuevo sistema atómico garantizado operaciones de memoria.

Y el hecho de que ninguna de las operaciones de carga o almacenamiento para SIMD garantiza atomicidad, podemos llegar a la conclusión de que Intel no admite ninguna forma de SIMD atómico (todavía).

Como un bit adicional, si la memoria se divide a lo largo de la caché líneas o límites de página (cuando se usan cosas como movdqu que permiten el acceso no alineado), los siguientes procesadores no realizarán accesos atómicos, independientemente de la alineación, pero los procesadores posteriores lo harán (de nuevo del Manual de Arquitectura de Intel):

Intel Core 2 Duo, Intel ® Atom™, Intel Core Duo, Pentium M, Pentium 4, Procesadores Intel Xeon, familia P6, Pentium e Intel486. Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon, y P6 procesadores familiares

 3
Author: Necrolis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-07 11:02:58

El x86 ISA no garantiza atomicidad para nada mayor que 8B, por lo que las implementaciones son libres de implementar el soporte SSE / AVX de la forma en que Pentium III / Pentium M / Core Duo lo hace: internamente los datos se manejan en mitades de 64 bits. Una tienda de 128 bits se realiza como dos tiendas de 64 bits. La ruta de datos hacia / desde la caché es de solo 64b de ancho en la microarquitectura de Yonah (Core Duo). (fuente:Microarch doc de Agner Fog).

Las implementaciones más recientes tienen rutas de datos más amplias internamente, y manejar instrucciones de 128b como un solo op. Core 2 Duo (conroe/merom) fue el primer microarch descendiente de Intel P6 con rutas de datos de 128b. (IDK sobre P4, pero afortunadamente es lo suficientemente viejo como para ser totalmente irrelevante.)

Esta es la razón por la que el OP encuentra que los ops 128b no son atómicos en Intel Core Duo (Yonah), pero otros posters encuentran que son atómicos en diseños posteriores de Intel, comenzando con Core 2 (Merom).

Los diagramas en este Realworldtech writeup sobre Merom vs. Yonah muestra la ruta de 128 bits entre ALU y la caché de datos L1 en Merom (y P4), mientras que el Yonah de baja potencia tiene una ruta de datos de 64 bits. La ruta de datos entre la caché L1 y L2 es 256b en los 3 diseños.

El siguiente salto en el ancho de la ruta de datos vino con Intel Haswell, con 256b (32B) AVX/AVX2 carga/almacena, y una ruta de 64Byte entre la caché L1 y L2. Espero que las cargas/tiendas 256b sean atómicas en Haswell, Broadwell y Skylake, pero no tengo una para probar. Se me olvida si Skylake una vez más se ampliaron las rutas en preparación para AVX512 en Skylake-EP (la versión del servidor), o si tal vez la implementación inicial de AVX512 será como AVX de SnB/IvB, y tienen cargas/tiendas 512b ocupan un puerto de carga/tienda durante 2 ciclos.


Como janneb señala en su excelente respuesta experimental, el protocolo de coherencia de caché entre sockets en un sistema multinúcleo podría ser más estrecho que el que se obtiene dentro de una CPU de caché de último nivel compartida. No hay ningún requisito arquitectónico en atomicidad para cargas/tiendas amplias, por lo que los diseñadores son libres de hacerlos atómicos dentro de un zócalo pero no atómicos a través de zócalos si eso es conveniente. IDK qué tan amplia es la ruta de datos lógica entre sockets para la familia Bulldozer de AMD o para Intel. (Digo "lógico", porque incluso si los datos se transfieren en trozos más pequeños, es posible que no modifique una línea de caché hasta que se reciba completamente.)


Encontrar artículos similares sobre CPU AMD debería permitir sacar conclusiones razonables sobre si 128b ops son atómicas o no. Solo revisar las tablas de instrucciones es algo de ayuda:

K8 decodifica movaps reg, [mem] a 2 m-ops, mientras que K10 y la familia bulldozer lo decodifican a 1 m-op. El bobcat de baja potencia de AMD lo decodifica a 2 ops, mientras que jaguar decodifica 128b movaps a 1 m-op. (Soporta AVX1 similar a las CPU de la familia bulldozer: 256b insns (incluso ALU ops) se dividen en dos 128b ops. Intel SnB solo divide cargas/tiendas de 256b, mientras que tiene ALU de ancho completo.)

El Opteron 2435 de Janneb es una CPU Istanbul de 6 núcleos , que es parte de la familia K10, por lo que esta conclusión atómica single-m-op -> parece precisa dentro de un solo zócalo.

Intel Silvermont hace 128b carga/almacena con un solo uop, y un rendimiento de uno por reloj. Esto es lo mismo que para cargas enteras / almacenes, por lo que es muy probablemente atómica.

 2
Author: Peter Cordes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-13 02:53:32

EDITAR: En los últimos dos días he hecho varias pruebas en mis tres PC y no he reproducido ningún error de memoria, por lo que no puedo decir nada con más precisión. Tal vez este error de memoria también depende del sistema operativo.

EDITAR: Estoy programando en Delphi y no en C pero debería entender C. Así que he traducido el código, aquí tienes los procedimientos de subprocesos donde la parte principal está hecha en ensamblador:

procedure TThread1.Execute;
var
  n             :cardinal;
const
  ConstAll0     :array[0..3] of integer =(0,0,0,0);
begin
  for n := 0 to 100000000 do
    asm
      movdqa    xmm0, dqword [x]
      movmskps  eax, xmm0
      inc       dword ptr[n1 + eax *4]
      movdqu    xmm0, dqword [ConstAll0]
      movdqa    dqword [x], xmm0
    end;
end;

{ TThread2 }

procedure TThread2.Execute;
var
  n             :cardinal;
const
  ConstAll1     :array[0..3] of integer =(-1,-1,-1,-1);
begin
  for n := 0 to 100000000 do
    asm
      movdqa    xmm0, dqword [x]
      movmskps  eax, xmm0
      inc       dword ptr[n2 + eax *4]
      movdqu    xmm0, dqword [ConstAll1]
      movdqa    dqword [x], xmm0
    end;
end;

Resultado: no hay error en mi PC quad core y no error en mi PC de doble núcleo como se esperaba!

  1. PC con CPU Intel Pentium4
  2. PC con CPU Intel Core2 Quad Q6600
  3. PC con CPU Intel Core2 Duo P8400

¿Puedes mostrar cómo debuger ve tu código de procedimiento de subproceso? Favor...

 0
Author: GJ.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-19 04:09:16

Se han publicado muchas respuestas hasta ahora y, por lo tanto, mucha información ya está disponible (como efecto secundario, mucha confusión también). Me gustaría colocar datos del manual de Intel con respecto a las operaciones atómicas garantizadas por hardware ...

En los últimos procesadores de Intel de la familia nehalem y sandy Bridge, la lectura o escritura en un quadword alineado al límite de 64 bits está garantizada.

Incluso las lecturas o escrituras no alineadas de 2, 4 u 8 bytes están garantizadas como atómicas siempre que sean memoria en caché y encajar en una línea de caché.

Dicho esto, la prueba publicada en esta pregunta pasa por el procesador intel i5 basado en sandy Bridge.

 -1
Author: Nitin Kunal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-10 10:46:43