Todas las operaciones SIMD hasta ahora han actuado carril por carril.
El valor en el 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 deseado. Por ejemplo, los carriles pueden estar desordenados para la computación que necesitamos realizar.
Hay muchas instrucciones SIMD que mueven datos entre carriles de diferentes maneras.
Un barajado reordena los bytes o carriles de un registro, tomando cada carril destino de un carril fuente que puede estar en cualquier lugar. Pueden seleccionar el mismo carril para diferentes destinos, lo que los hace capaces de también difundir valores.
Las instrucciones de barajado se comportan de manera diferente según su tamaño y dominio de ejecución.
La instrucción pshufd reorganiza los cuatro carriles de 32 bits de su fuente en su destino.
La selección es un inmediato de 8 bits, leído como cuatro campos de 2 bits, uno por carril destino. Cada campo selecciona cuál de los cuatro carriles fuente copiar en ese carril destino:
| campo | índice de carril |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
La posición del campo en el inmediato, leída de derecha a izquierda, indica dónde se inserta el carril seleccionado. Un carril fuente puede seleccionarse más de una vez, que es cómo se difunde un solo carril por 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 barajado de punto flotante, que siguen la misma sintaxis general: shuf + p + el sufijo de tamaño (ya sea s o d).
También usan un inmediato para seleccionar los carriles.
shufps usa la misma codificación de campo de 2 bits que los barajados de enteros anteriores.
Sin embargo, como solo hay 2 carriles de 64 bits en un operando de 128 bits, shufpd usa solo una codificación de campo de 1 bit.
Estas instrucciones se diferencian de sus contrapartes 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 tanto la fuente como el destino son el mismo registro, cada carril se toma de él.
pshufb es el barajado más general.
Aunque pshufb y pshufd tienen nombres muy similares, diferenciándose solo en el sufijo de tamaño, realizan operaciones muy diferentes.
Primero, mientras que pshufd usa un inmediato para seleccionar las posiciones de los carriles, pshufb usa un vector de control en el operando fuente.
Este vector de control es un registro xmm o un operando de memoria de 16 bytes.
Para cada uno de los 16 carriles destino, los cuatro bits bajos del vector de control dan un índice de byte fuente, de 0 a 15.
Si el carril i del vector de control contiene el índice de byte fuente j, entonces el carril j-th del destino se moverá al carril i-th.
Esto resalta una segunda diferencia entre las dos instrucciones.
Mientras que pshufd toma los carriles a barajar de un operando fuente diferente, pshufb realiza el barajado en el mismo lugar, 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 que eso: puede limpiar cualquiera de los carriles.
Si el bit superior está establecido en un carril i del vector de control, entonces el carril i del destino se pone a cero.
Esto hace que pshufb sea tanto una permutación arbitraria de bytes como un borrado selectivo, en una sola instrucción.
Como pshufb actúa a granularidad de byte, también se puede usar para barajar carriles de diferentes tamaños, agrupando carriles vecinos.
Por ejemplo, se puede usar para barajar 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 los dos. Hay variantes de enteros y de punto flotante y en gran medida siguen 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 otra dirección, combinando los carriles de dos operandos en carriles de la mitad de ancho del destino.
Estas instrucciones no llevan un prefijo p.
Aparte de eso, la sintaxis combina elementos que ya hemos visto:
pack.s o u para indicar si los valores de salida son con signo o sin signo, respectivamente.s para saturante, como en la aritmética saturante vista 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 como 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 destino y los carriles altos de la fuente:
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 de punto 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 o leer del carril bajo de un registro SIMD, y, al escribir, limpian todos los demás carriles.
Hay instrucciones que hacen lo mismo para cualquier carril, no solo el primero, dejando otros carriles intactos:
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 crudos.
Esto significa que también se pueden usar para valores de punto 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 en 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, o sea, 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 se deben realizar 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 mover.
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 completa de RGBA a BGRA, bloque a bloque.
Debes definir la máscara de control que reordena los canales como una constante empaquetada en memoria, y reutilizarla para cada bloque.
Esta función recibe 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: la cantidad 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 valor de retorno.
Para limpiar una región o pintar un tramo uniforme, el pipeline escribe un solo 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 recibe 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: la cantidad 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 valor de retorno.
Hay que fusionar dos capas de un solo canal 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 sola fila de 32 muestras.
Esta función recibe 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 valor de retorno.
Una pasada de brillo escala cada muestra en 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 su visualización, y limita cualquier valor por debajo de 0 a 0 y cualquier valor por encima de 255 a 255.
Implementa la función pack_samples, que reduce dos grupos de 8 valores de trabajo a una sola fila de 16 muestras, en orden.
Esta función recibe como argumentos, en este orden:
result: dirección de memoria de un búfer donde se escriben las 16 muestras limitadas, cada una un valor de 8 bits.first: dirección de memoria de los primeros 8 valores de trabajo, cada uno un entero con signo de 16 bits.second: dirección de memoria de los siguientes 8 valores de trabajo, cada uno un entero con signo de 16 bits.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 valor de retorno.
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 procesarlas de forma eficiente: todos los valores de x en un vector y todos los valores de 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 recibe 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 punto flotante de 32 bits.ys: dirección de memoria de un búfer donde se escriben las 4 coordenadas y, 4 números de punto flotante de 32 bits.first: dirección de memoria de los dos primeros puntos, 4 números de punto flotante de 32 bits, como {x0, y0, x1, y1}.second: dirección de memoria de los dos puntos siguientes, 4 números de punto 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 valor de retorno.
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.