Todas las operaciones SIMD que hemos visto hasta ahora han actuado carril a carril.
El valor del carril i del resultado se calcula a partir del carril i de las entradas.
Sin embargo, hay muchas situaciones en las que mover valores entre carriles es necesario o deseable. Por ejemplo, puede que los carriles estén desordenados para el cálculo que necesitamos hacer.
Hay muchas instrucciones SIMD que mueven datos entre carriles de distintas formas.
Un shuffle reordena los bytes o los carriles de un registro, tomando cada carril de destino de un carril de origen que puede estar en cualquier posición. Pueden seleccionar el mismo carril para distintos destinos, lo que las hace capaces también de difundir valores.
Las instrucciones de shuffle se comportan de forma distinta según su tamaño y su dominio de ejecución.
La instrucción pshufd reordena los cuatro carriles de 32 bits de su origen en su destino.
La selección es un inmediato de 8 bits, leído como cuatro campos de 2 bits, uno por cada carril de destino. Cada campo selecciona cuál de los cuatro carriles de origen se copia en ese carril de destino:
| campo | índice de carril |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
La posición del campo dentro del inmediato, leída de derecha a izquierda, indica dónde se inserta el carril seleccionado. Un carril de origen puede seleccionarse más de una vez, y así es como se difunde un único carril a todo el registro:
; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
Hay dos instrucciones de shuffle en coma flotante, que siguen la misma sintaxis general: shuf + p + el sufijo de tamaño (s o d).
También usan un inmediato para seleccionar los carriles.
shufps usa la misma codificación de campos de 2 bits que los shuffles de enteros anteriores.
Sin embargo, como solo hay 2 carriles de 64 bits en un operando de 128 bits, shufpd usa una codificación de campos de solo 1 bit.
Estas instrucciones se diferencian de sus equivalentes de enteros en que toman los carriles de dos operandos en lugar de uno:
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1 ; xmm4 = {xmm4[1], xmm5[0]}
Como ambos operandos alimentan el resultado, se pueden usar para entrelazar dos vectores en un solo paso. Si el origen y el destino son el mismo registro, todos los carriles se toman de él.
pshufb es el shuffle más general.
Aunque pshufb y pshufd tienen nombres muy parecidos, que solo se diferencian en el sufijo de tamaño, realizan operaciones muy distintas.
En primer lugar, mientras que pshufd usa un inmediato para seleccionar las posiciones de los carriles, pshufb usa un vector de control en el operando de origen.
Este vector de control es un registro xmm o un operando de memoria de 16 bytes.
Para cada uno de los 16 carriles de destino, los cuatro bits bajos del vector de control dan un índice de byte de origen, de 0 a 15.
Si el carril i del vector de control contiene el índice de byte de origen j, el carril j-th del destino se moverá al carril i-th.
Esto pone de relieve una segunda diferencia entre las dos instrucciones.
Mientras que pshufd toma los carriles que va a reordenar de un operando de origen distinto, pshufb realiza el shuffle in situ en el destino.
section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
section .text
fn:
pshufb xmm0, [rel reverse]
; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
; ...
; in the end, the bytes in xmm0 are reversed
pshufb es aún más flexible: puede poner a cero cualquiera de los carriles.
Si el bit más alto está activado en un carril i del vector de control, el carril i del destino se pone a cero.
Esto hace que pshufb sea a la vez una permutación de bytes arbitraria y un borrado selectivo, en una sola instrucción.
Como pshufb actúa a nivel de byte, también se puede usar para hacer shuffle de carriles de distintos tamaños, agrupando carriles vecinos.
Por ejemplo, se puede usar para hacer shuffle de dwords:
section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes
section .text
fn:
movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
pshufb xmm0, [rel reverse_dwords]
; xmm0 = {49, 37, 25, 13}
Las instrucciones unpack toman carriles de dos operandos y los entrelazan, alternando entre ambos. Hay variantes para enteros y para coma flotante, y siguen en gran medida la misma estructura de sintaxis general, con dos diferencias:
l o h para indicar si actúa sobre la mitad baja (l) o la mitad alta (h) de cada operando.bw o qdq (el sufijo de tamaño para 16 bytes es dq, como en movdqu).punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}
unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}
Las instrucciones pack van en la dirección contraria, combinando los carriles de dos operandos en carriles de la mitad de ancho del destino.
Estas instrucciones no llevan el prefijo p.
Aparte de eso, la sintaxis combina elementos que ya hemos visto:
pack.s o una u para indicar si los valores de salida son con signo o sin signo, respectivamente.s de saturación, como en la aritmética saturante que vimos en un concepto anterior.unpck).Como la operación pack es de estrechamiento, no necesita un sufijo l o h:
packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)
Estas instrucciones son saturantes, es decir, los valores de salida se limitan al rango más estrecho. Operan de dword a word y de word a byte. No hay una variante de qword a dword.
Ten en cuenta que la entrada siempre se interpreta con signo.
El tipo, ya sea con signo o sin signo, es para la salida.
Indica el rango al que se debe limitar.
Por ejemplo, packsswb limita al rango de un byte con signo, es decir, [-128, 127].
Los carriles bajos del resultado provienen del operando de destino y los altos, del operando de origen:
packusdw xmm0, xmm1 ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}
No hay un equivalente en coma flotante para estas instrucciones.
Hasta ahora, siempre hemos movido datos entre un registro SIMD y un registro de propósito general usando movq/movd.
Estas instrucciones solo pueden escribir en el carril bajo de un registro SIMD o leer de él y, al escribir, ponen a cero todos los demás carriles.
Hay instrucciones que hacen lo mismo con cualquier carril, no solo con el primero, y dejan intactos los demás carriles:
Ambas siguen la sintaxis de enteros: p + insr/extr + el sufijo de tamaño (b, w, d o q).
En ambos casos, el registro de propósito general suele ser de 32 bits.
Solo pinsrq y pextrq necesitan un operando de 64 bits.
Un operando de memoria siempre es del tamaño de la operación: 8 bits para pinsrb/pextrb, 16 bits para pinsrw/pextrw, y así sucesivamente.
pinsrb xmm0, eax, 5 ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx
Ten en cuenta que estas instrucciones, al igual que movd y movq, simplemente mueven bytes sin procesar.
Esto significa que también se pueden usar con valores de coma flotante almacenados en memoria o en un registro de propósito general.
Escribes los bucles internos de un pipeline de imágenes de software, la etapa que prepara texturas y compone capas antes de que lleguen a la pantalla. El pipeline trabaja con píxeles bloque a bloque, aplicando la misma operación a todo el bloque.
Un píxel se compone de cuatro canales de 1 byte: rojo, verde, azul y alfa, en ese orden (RGBA).
Un bloque son 4 píxeles, es decir, 16 bytes en total.
Tienes cinco tareas.
Recibes los operandos a través de direcciones de memoria y escribes tu respuesta a través de una dirección de resultado. Todas las direcciones de memoria de este ejercicio están alineadas a 16 bytes.
Los cálculos de este ejercicio deben realizarse con instrucciones SIMD, no con operaciones escalares.
Las texturas se almacenan como RGBA, pero el framebuffer en el que dibuja este pipeline espera cada píxel en orden BGRA: los canales rojo y azul intercambiados, y los canales verde y alfa sin tocar.
Una imagen llega como una secuencia de bloques, y todos los bloques se convierten de la misma manera.
Implementa la función to_display_order, que convierte una imagen entera de RGBA a BGRA, bloque a bloque.
Deberías definir la máscara de control de reordenación de canales como una constante empaquetada en memoria y reutilizarla para cada bloque.
Esta función toma como argumentos, en este orden:
result: dirección de memoria de un búfer donde se escriben los bloques convertidos, 16 bytes por bloque.pixels: dirección de memoria de los bloques de origen, 4 píxeles por bloque, cada píxel de 4 bytes en orden RGBA.block_count: el número de bloques, siempre mayor que 0.pixels = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}
Esta función no tiene ningún valor devuelto.
Para limpiar una región o pintar un tramo uniforme, el pipeline escribe un único color en todos los píxeles de la región.
Implementa la función fill_region, que rellena una región de block_count bloques con copias de un mismo color.
Esta función toma como argumentos, en este orden:
result: dirección de memoria de un búfer donde se escriben los bloques rellenados, 16 bytes por bloque.color: dirección de memoria de un píxel, 4 bytes en orden RGBA.block_count: el número de bloques que se van a rellenar, siempre mayor que 0.color = {18, 52, 86, 120}
block_count = 2
result = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}
Esta función no tiene ningún valor devuelto.
Dos capas de un solo canal deben fusionarse en un único búfer con sus muestras entrelazadas. El resultado alterna una muestra de la primera capa y luego una de la segunda.
Implementa la función weave_scanlines, que entrelaza dos filas de 16 muestras cada una en una única fila de 32 muestras.
Esta función toma como argumentos, en este orden:
result: dirección de memoria de un búfer donde se escriben las 32 muestras entrelazadas.first: dirección de memoria de la primera fila, 16 muestras, cada una un valor de 8 bits.second: dirección de memoria de la segunda fila, 16 muestras, cada una un valor de 8 bits.first = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}
Esta función no tiene ningún valor devuelto.
Una pasada de brillo escala cada muestra con una precisión de trabajo de 16 bits, de modo que una muestra demasiado brillante puede superar 255 y una diferencia puede caer por debajo de 0.
La etapa final reduce esos valores de trabajo de nuevo a muestras de 8 bits para mostrarlas, ajustando cualquier valor por debajo de 0 hasta 0 y cualquier valor por encima de 255 hasta 255.
Implementa la función pack_samples, que reduce dos grupos de 8 valores de trabajo a una única fila de 16 muestras, en orden.
Esta función toma como argumentos, en este orden:
result: dirección de memoria de un búfer donde se escriben las 16 muestras ajustadas, cada una un valor de 8 bits.first: dirección de memoria de los primeros 8 valores de trabajo, cada uno un entero de 16 bits con signo.second: dirección de memoria de los siguientes 8 valores de trabajo, cada uno un entero de 16 bits con signo.first = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200, 255, 255, 0, 100, 50, 255, 7, 0}
Esta función no tiene ningún valor devuelto.
Los datos de texturas y vértices a menudo llegan entrelazados, con la x y la y de cada punto empaquetadas juntas.
Sin embargo, a menudo es necesario tenerlas separadas para un procesamiento eficiente: todos los valores x en un vector y todos los valores y en otro.
Implementa la función split_coordinates, que separa cuatro puntos (x, y) entrelazados en un vector de coordenadas x y un vector de coordenadas y.
Esta función toma como argumentos, en este orden:
xs: dirección de memoria de un búfer donde se escriben las 4 coordenadas x, 4 números de coma flotante de 32 bits.ys: dirección de memoria de un búfer donde se escriben las 4 coordenadas y, 4 números de coma flotante de 32 bits.first: dirección de memoria de los dos primeros puntos, 4 números de coma flotante de 32 bits, como {x0, y0, x1, y1}.second: dirección de memoria de los siguientes dos puntos, 4 números de coma flotante de 32 bits, como {x2, y2, x3, y3}.first = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}
Esta función no tiene ningún valor devuelto.
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.