Pregunta

¿Hay alguna diferencia entre las intrínsecs lógicas de SSE para diferentes tipos? Por ejemplo, si tomamos u operamos, hay tres intrínsecos: _mm_or_ps, _mm_or_pd y _mm_or_si128 todo lo cual hace lo mismo: calcular boxwise O de sus operandos. Mis preguntas:

  1. ¿Hay alguna diferencia entre usar uno u otro intrínseco (con el tipo de fundición de tipo apropiado)? ¿No habrá costos ocultos como una ejecución más larga en alguna situación específica?

  2. Estas intrínsecos se mapean a tres instrucciones X86 diferentes (POR, ORPS, ORPD). ¿Alguien tiene alguna idea de por qué Intel está desperdiciando un espacio precioso de Opcode para varias instrucciones que hacen lo mismo?

¿Fue útil?

Solución

Creo que los tres son efectivamente iguales, es decir, operaciones de 128 bit a bit a 128 bits. La razón por la cual existen diferentes formas es probablemente histórica, pero no estoy seguro. Supongo que es posible Que puede haber algún comportamiento adicional en las versiones de puntos flotantes, por ejemplo, cuando hay NANS, pero esto es puro conjeturas. Para las entradas normales, las instrucciones parecen ser intercambiables, por ejemplo

#include <stdio.h>
#include <emmintrin.h>
#include <pmmintrin.h>
#include <xmmintrin.h>

int main(void)
{
    __m128i a = _mm_set1_epi32(1);
    __m128i b = _mm_set1_epi32(2);
    __m128i c = _mm_or_si128(a, b);

    __m128 x = _mm_set1_ps(1.25f);
    __m128 y = _mm_set1_ps(1.5f);
    __m128 z = _mm_or_ps(x, y);

    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);

    c = (__m128i)_mm_or_ps((__m128)a, (__m128)b);
    z = (__m128)_mm_or_si128((__m128i)x, (__m128i)y);

    printf("a = %vld, b = %vld, c = %vld\n", a, b, c);
    printf("x = %vf, y = %vf, z = %vf\n", x, y, z);

    return 0;
}

$ gcc -Wall -msse3 por.c -o por

$ ./por

a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000
a = 1 1 1 1, b = 2 2 2 2, c = 3 3 3 3
x = 1.250000 1.250000 1.250000 1.250000, y = 1.500000 1.500000 1.500000 1.500000, z = 1.750000 1.750000 1.750000 1.750000

Otros consejos

  1. ¿Hay alguna diferencia entre usar uno u otro intrínseco (con el tipo de fundición de tipo apropiado)? ¿No habrá costos ocultos como una ejecución más larga en alguna situación específica?

Sí, puede haber razones de rendimiento para elegir una frente a la otra.

1: A veces hay un ciclo adicional o dos de latencia (retraso de reenvío) si la salida de una unidad de ejecución entera debe enrutarse a la entrada de una unidad de ejecución de FP, o viceversa. Se necesitan muchos cables para mover 128B de datos a cualquiera de los muchos destinos posibles, por lo que los diseñadores de CPU tienen que hacer compensaciones, como tener solo una ruta directa desde cada salida de FP a cada entrada de FP, no a todas las entradas posibles.

Ver esta respuesta, o Doc de microarquitectura de Agner Fog para entregas de derivación. Busque "retrasos de derivación de datos en Nehalem" en el Doc de Agner; Tiene algunos buenos ejemplos prácticos y discusión. Tiene una sección sobre ello por cada microarch que ha analizado.

Sin embargo, los retrasos para pasar datos entre los diferentes dominios o diferentes tipos de registros son más pequeños en el puente arenoso y el puente de hiedra que en el Nehalem, y a menudo cero. - Micro Arch Doc de Agner Fog

Recuerde que la latencia no importa si no está en la ruta crítica de su código. Usando pshufd en vez de movaps + shufps Puede ser una victoria si el rendimiento de UOP es su cuello de botella, en lugar de la latencia de su camino crítico.

2: los ...ps La versión toma 1 byte menos de código que los otros dos. Esto alineará las siguientes instrucciones de manera diferente, lo que puede importar para los decodificadores y/o las líneas de caché UOP.

3: Las CPU Intel recientes solo pueden ejecutar las versiones FP en Port5.

  • Merom (Core2) y Penryn: orps Puede funcionar en P0/P1/P5, pero solo dominio entero. Presumiblemente las 3 versiones decodificadas en exactamente la misma UOP. Entonces ocurre el retraso de reenvío de dominio cruzado. (Las CPU AMD también hacen esto: FP Instrucciones bitwise se ejecutan en el dominio IVEC).

  • Nehalem / Sandybridge / IVB / Haswell / Broadwell: por puede funcionar en P0/P1/P5, pero orps Solo puede funcionar en Port5. P5 también es necesario por Shuffles, pero las unidades FMA, FP Add y FP MUL están en los puertos 0/1.

  • Skylake: por y orps Ambos tienen un rendimiento del 3 por ciclo. La información sobre los retrasos del reenvío aún no está disponible.

Tenga en cuenta que en SNB/IVB (AVX pero no AVX2), solo P5 necesita manejar operaciones lógicas de 256B, como vpor ymm, ymm requiere avx2. Probablemente esta no fue la razón del cambio, ya que Nehalem hizo esto.

Cómo elegir sabiamente:

Si el OP de OP lógico en Port5 podría ser un cuello de botella, use las versiones enteras, incluso en los datos de FP. Esto es especialmente cierto si desea utilizar bendiciones enteras u otras instrucciones de movimiento de datos.

Las CPU AMD siempre usan el dominio entero para lógicos, por lo que si tiene múltiples cosas de dominio entero que hacer, hágalos todos a la vez para minimizar los viajes redondos entre dominios. Las latencias más cortas eliminarán las cosas del búfer de reorden más rápido, incluso si una cadena DEP no es el cuello de botella para su código.

Si solo desea configurar/borrar/volar un poco en los vectores FP entre las instrucciones FP Add y MUL, use el ...ps lógicos, incluso en datos de doble precisión, porque FP simple y doble son el mismo dominio en cada CPU que existen, y el ...ps Las versiones son un byte más cortas.

Hay razones prácticas / de factor humano para usar el ...pd Sin embargo, las versiones que a menudo superarán el ahorro de 1 byte de código. La legibilidad de su código por parte de otros humanos es un factor: se preguntarán por qué está tratando sus datos como solteros cuando en realidad son dobles. Espeal con C/C ++ Intrinsics, basándose en su código con moldes entre __mm256 y __mm256d no vale la pena. Si el ajuste en el nivel de alineación de INSN es importante, escriba en ASM directamente, ¡no intrínsecos! (Tener la instrucción un byte más largo podría alinear las cosas mejor para la densidad de línea de caché de UOP y/o decodificadores).

Para datos enteros, use las versiones enteras. Guardar un byte de instrucción no vale la pena el retraso de derivación, y el código entero a menudo mantiene a Port5 completamente ocupado con barajas. Para Haswell, muchas instrucciones Shuffle / Insert / Extract / Pack / Desempack se convirtieron en P5 solo, en lugar de P1 / P5 para SNB / IVB.

  1. Estas intrínsecas se mapean a tres instrucciones X86 diferentes (por, orps, orpd). ¿Alguien tiene alguna idea de por qué Intel está desperdiciando un espacio precioso de Opcode para varias instrucciones que hacen lo mismo?

Si observa el historial de estos conjuntos de instrucciones, puede ver cómo llegamos aquí.

por  (MMX):     0F EB /r
orps (SSE):     0F 56 /r
orpd (SSE2): 66 0F 56 /r
por  (SSE2): 66 0F EB /r

MMX existió antes de SSE, por lo que parece código de operación para SSE (...ps) Las instrucciones fueron elegidas de la misma 0F xx espacio. Luego para sse2, el ...pd la versión agregó un 66 prefijo de tamaño de operando al ...ps Opcode, y la versión entera agregó un 66 Prefijo a la versión MMX.

Ellos pudo me han ido orpd y/o por, pero no lo hicieron. Tal vez pensaron que los futuros diseños de CPU podrían tener rutas de reenvío más largas entre diferentes dominios, por lo que usar la instrucción coincidente para sus datos sería un trato mayor. A pesar de que hay códigos de operación separados, AMD e Intel temprano los trataron de todos modos, como int-vector.

De acuerdo con las pautas de optimización de Intel y AMD, la mezcla de OP con tipos de datos produce un éxito de rendimiento ya que la CPU etiqueta internamente las mitades de 64 bits del registro para un tipo de datos particular. Esto parece afectar principalmente la línea de tubería a medida que la instrucción se decodifica y se programan los UEP. Funcionalmente producen el mismo resultado. Las versiones más nuevas para los tipos de datos enteros tienen una codificación más grande y ocupan más espacio en el segmento de código. Entonces, si el tamaño del código es un problema, use el OPS anterior, ya que tienen una codificación más pequeña.

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