Un concepto anterior presentó los valores de punto flotante empaquetados: varios números guardados en un solo registro xmm, sobre los que se opera por carril y en paralelo.
Esos mismos registros xmm también pueden contener enteros empaquetados.
Gran parte del modelo de punto flotante de SIMD se traslada sin cambios a los enteros empaquetados:
Sin embargo, la sintaxis es un poco distinta:
p que indica que la instrucción opera sobre datos empaquetados.add, mul, etc.).Los enteros empaquetados tienen cuatro tamaños de carril, y cada uno tiene su propio sufijo:
| ancho del carril | bytes | carriles en 128 bits | sufijo |
|---|---|---|---|
| byte | 1 | 16 | b |
| word | 2 | 8 | w |
| dword | 4 | 4 | d |
| qword | 8 | 2 | q |
Por ejemplo:
| instrucción | significado |
|---|---|
paddb |
add empaquetado, carriles de 8 bits (16 carriles) |
paddw |
add empaquetado, carriles de 16 bits (8 carriles) |
paddd |
add empaquetado, carriles de 32 bits (4 carriles) |
paddq |
add empaquetado, carriles de 64 bits (2 carriles) |
Algunas instrucciones toman como entrada carriles de un tamaño, pero producen carriles de otro. Siguen la misma convención general, pero con dos sufijos de tamaño. El primero indica el tamaño del carril de entrada y el segundo indica el tamaño del carril de salida:
| instrucción | significado |
|---|---|
pmovsxwd |
movsx empaquetado, de carriles de 16 bits a carriles de 32 bits |
pmuldq |
mul empaquetado, de carriles de 32 bits a carriles de 64 bits |
Dos instrucciones con nombre de entero copian 128 bits entre un registro xmm y la memoria:
| instrucción | descripción |
|---|---|
movdqa |
copia enteros empaquetados desde o hacia una ubicación alineada |
movdqu |
copia enteros empaquetados desde o hacia una ubicación no alineada |
Se comportan como movaps y movups: movdqa provoca un fallo con una dirección desalineada, mientras que movdqu acepta cualquiera.
Las cuatro copian 128 bits sin interpretarlos.
El par con nombre de entero se usa con datos enteros por convención, no por obligación.
Aquí dq significa double-qword, es decir, 128 bits (16 bytes).
La suma y la resta siguen la regla de nombres:
paddb xmm0, xmm1 ; 16 lanes: each 8-bit, xmm0 += xmm1
paddw xmm2, xmm3 ; 8 lanes: each 16-bit, xmm2 += xmm3
psubd xmm4, xmm5 ; 4 lanes: each 32-bit, xmm4 -= xmm5
psubq xmm6, xmm7 ; 2 lanes: each 64-bit, xmm6 -= xmm7
No hay una forma separada para con signo y sin signo.
En complemento a dos, la suma y la resta producen los mismos bits ya se lean los carriles con signo o sin signo, así que una sola instrucción sirve para ambos casos.
La interpretación queda a tu criterio, exactamente igual que con add y sub escalares.
Estas instrucciones dan la vuelta al desbordarse, igual que sus equivalentes escalares.
Un carril de 8 bits guarda valores módulo 256, así que un paddb de 200 + 100 produce 300 - 256 = 44, descartando los bits que no caben.
Fíjate que esos bits adicionales no se llevan al siguiente carril. Cada carril se opera por separado de los demás, aunque compartan el mismo registro.
El SIMD de enteros añade una operación que el SIMD de punto flotante no tiene: la suma y la resta saturantes, que acotan en lugar de dar la vuelta. Un resultado por encima del rango del carril se convierte en el mayor valor que el carril puede guardar; un resultado por debajo del rango se convierte en el menor.
Las formas saturantes insertan s (con signo) o us (sin signo) antes del sufijo de tamaño:
| instrucción | significado |
|---|---|
paddsb |
add, saturante, carriles de 8 bits con signo |
paddusb |
add, saturante, carriles de 8 bits sin signo |
psubsw |
sub, saturante, carriles de 16 bits con signo |
psubusw |
sub, saturante, carriles de 16 bits sin signo |
El rango de acotación es todo el rango representable para un entero del tamaño correspondiente, con o sin signo. Para un byte:
[0, 255]: un paddusb de 200 + 100 da 255, y un psubusb de 5 - 10 da 0.[-128, 127]: un paddsb de 100 + 50 da 127.La saturación importa cuando un carril guarda una cantidad acotada, como un canal de píxel o una muestra de audio. Dar la vuelta convertiría un píxel demasiado brillante en uno oscuro, mientras que la acotación lo mantiene en su máximo brillo, que es el resultado que buscas.
La suma y la resta saturantes existen solo para carriles de byte y word, no para dword ni qword.
Multiplicar dos valores de N bits puede producir un producto de 2N bits, pero el carril de destino solo tiene N bits de ancho. Las operaciones SIMD resuelven esto especificando qué mitad del producto conservar: los N bits bajos o los N bits altos.
Para carriles de 16 bits, se usan tres instrucciones:
| instrucción | significado |
|---|---|
pmullw |
mul, carriles de 16 bits, conserva los 16 bits bajos de cada producto |
pmulhw |
mul, carriles de 16 bits, conserva los 16 bits altos, operandos con signo |
pmulhuw |
mul, carriles de 16 bits, conserva los 16 bits altos, operandos sin signo |
Los 16 bits bajos de un producto son iguales ya se lean los operandos con signo o sin signo, así que hay un solo pmullw.
Los 16 bits altos, en cambio, varían según si el resultado es con signo o sin signo. Por eso la multiplicación de la mitad alta tiene formas separadas para la multiplicación con signo y sin signo:
pmulhw, para la multiplicación con signo.pmulhuw, con una u adicional, para la multiplicación sin signo.Fíjate en la sintaxis:
p, para entero empaquetado.mul.h, que indica que se están seleccionando los bits altos («high») del resultado.u opcional si el resultado debe interpretarse como sin signo (es decir, no tiene extensión de signo).w, que indica que es una operación de word (16 bits).La multiplicación empaquetada para word sigue exactamente la regla anterior.
La multiplicación de dword (32 bits) también sigue la regla, pero solo existe la variante que selecciona la mitad baja: pmulld.
No hay ninguna variante de la multiplicación de dword que seleccione los bits altos. Sin embargo, hay variantes que ensanchan la multiplicación, guardando el producto completo de 64 bits de los carriles con índice par (es decir, los carriles en las posiciones 0 y 2):
| instrucción | significado |
|---|---|
pmuludq |
multiplica los elementos de 32 bits con índice par, sin signo, en 2 productos completos de 64 bits |
pmuldq |
multiplica los elementos de 32 bits con índice par, con signo, en 2 productos completos de 64 bits |
Fíjate que la sintaxis usa dq, posiblemente con una u antes para la multiplicación sin signo.
Esto se debe a que las instrucciones toman carriles dword y producen carriles qword.
No hay división de enteros empaquetada. El código que la necesita convierte los valores a punto flotante, divide y vuelve a convertir.
Existen equivalentes empaquetados de movsx y movzx.
Siguen la misma sintaxis que mencionamos para las instrucciones que toman entradas con un tamaño de carril distinto al de su salida:
pmovsxwd xmm0, xmm1 ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1 ; 8 bytes -> 8 words, zero-extended
Fíjate que el número de carriles lo determina el ancho mayor (el de salida).
La instrucción lee esa cantidad de carriles desde la parte baja del origen.
Esta es la misma semántica que ya vimos para las instrucciones cvt empaquetadas entre flotantes de precisión simple y doble.
Hay instrucciones para convertir entre enteros de 32 bits con signo y carriles de punto flotante.
Siguen la sintaxis habitual de las instrucciones cvt y cvtt, pero en lugar de si un entero empaquetado se representa con dq:
cvtdq2ps xmm0, xmm1 ; convert 32-bit signed integers in xmm1 to 32-bit floats in xmm0
cvtdq2pd xmm2, xmm3 ; convert 32-bit signed integers in xmm3 to 64-bit floats in xmm2
cvtps2dq xmm4, xmm5 ; convert 32-bit floats in xmm5 to 32-bit signed integers in xmm4
Las instrucciones que usan pi para representar enteros empaquetados escriben en registros mmx heredados y están efectivamente obsoletas en x86-64.
Prefiere las formas dq, que usan registros xmm.
Los mismos comentarios que hicimos para la conversión escalar entre números de punto flotante y enteros son válidos aquí.
Los números de punto flotante se redondean según un registro especial llamado MXCSR, cuyo modo no puedes dar por sentado al entrar en una función.
Hay una variante cvtt (con una t adicional) que siempre trunca el resultado.
También es posible usar round para tener los valores de punto flotante empaquetados en un estado conocido antes de convertir.
El valor de control de round es el mismo que el del round escalar, y la sintaxis también es la misma para los valores de punto flotante empaquetados:
roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats
Estás escribiendo el núcleo digital de una pequeña consola de mezclas en un estudio de grabación.
El sonido llega como un flujo de muestras. Cada muestra es un número que describe la posición del cono de un altavoz en un instante. Una consola procesa un bloque completo de muestras a la vez, aplicando la misma operación a cada una.
Tienes cinco tareas, y cada una opera sobre un bloque de muestras que está en memoria. Recibes los operandos a través de direcciones de memoria y escribes tu respuesta a través de una dirección de resultado.
En este ejercicio, los cálculos deben realizarse usando instrucciones SIMD, no operaciones escalares.
A veces el estudio graba los instrumentos en pistas distintas. Para reproducirlos por los mismos altavoces, sumas las dos pistas en una.
Implementa la función mix_tracks, que suma dos pistas, muestra por muestra. Cada pista es un bloque de 8 muestras de 16 bits con signo. Una suma que desborde el rango de 16 bits con signo debe saturarse en lugar de dar la vuelta.
Esta función recibe como argumentos, en este orden:
result: dirección de memoria alineada a 16 bytes de un búfer donde se escriben las 8 muestras mezcladas.track_a: dirección de memoria alineada a 16 bytes de la primera pista, con 8 muestras de 16 bits con signo.track_b: dirección de memoria alineada a 16 bytes de la segunda pista, con 8 muestras de 16 bits con signo.track_a = {10000, 30000, 20000, -30000, -20000, -100, 100, 0}
track_b = { 5000, 5000, 20000, -5000, -20000, 100, -100, 0}
result = {15000, 32767, 32767, -32768, -32768, 0, 0, 0}
Esta función no tiene valor de retorno.
Cuando dos micrófonos graban a la vez, cada uno capta una copia tenue de la fuente del otro. A esta copia no deseada se le llama sangrado. Una vez que se mide ese sangrado, hay que restarlo para limpiar la pista.
Implementa la función remove_bleed, que resta el bloque de sangrado del bloque de la pista, muestra por muestra. Cada bloque tiene 8 muestras de 16 bits con signo, y la resta debe saturar.
Esta función recibe como argumentos, en este orden:
result: dirección de memoria donde se escriben las 8 muestras limpias (no se garantiza la alineación).track: dirección de memoria alineada a 16 bytes de la pista grabada, 8 muestras de 16 bits con signo.bleed: dirección de memoria del sangrado que hay que eliminar, 8 muestras de 16 bits con signo (no se garantiza la alineación).track = {10000, -30000, 5000, 32767, 0, 200, -200, 15000}
bleed = { 2000, 5000, 5000, -5, 0, 300, 300, 5000}
result = { 8000, -32768, 0, 32767, 0, -100, -500, 10000}
Esta función no tiene valor de retorno.
Encima de cada canal hay un medidor de nivel, una barrita cuyo brillo indica qué tan fuerte es ese canal. El panel frontal tiene un medidor maestro que debe brillar tanto como los dos canales combinados. Sumas los dos bloques de medidores para controlarlo.
Ten en cuenta que un medidor brillante se queda al máximo brillo en lugar de dar la vuelta hasta apagarse, así que la suma satura.
Implementa la función combine_meters, que suma dos bloques de medidores muestra por muestra. Esta función recibe como argumentos, en este orden:
result: dirección de memoria alineada a 16 bytes donde se escriben los 16 valores combinados.meter_a: dirección de memoria alineada a 16 bytes del bloque de medidor del primer canal, con 16 valores de 8 bits sin signo empaquetados.meter_b: dirección de memoria alineada a 16 bytes del bloque de medidor del segundo canal, con 16 valores de 8 bits sin signo empaquetados.Cada bloque tiene 16 valores de 8 bits sin signo. Hay que sumarlos, saturando al rango sin signo.
meter_a = {200, 100, 50, 255, 0, 128, 10, 240, 1, 2, 3, 4, 5, 6, 7, 8}
meter_b = {100, 100, 200, 1, 0, 128, 10, 20, 1, 2, 3, 4, 5, 6, 7, 8}
result = {255, 200, 250, 255, 0, 255, 20, 255, 2, 4, 6, 8, 10, 12, 14, 16}
Esta función no tiene valor de retorno.
La canción no debe detenerse de golpe, sino desvanecerse al final. Tienes un bloque de coeficientes de ganancia, uno por muestra, que van bajando cada vez más de volumen.
Cada coeficiente es una fracción expresada sobre 65536. Por ejemplo, si una muestra tiene un valor de 16384, en realidad expresa 16384 / 65536, que es 1 / 4. El mayor valor utilizable, 32767, es un poco menos de la mitad.
Como 65536 es 2^16, multiplicar una muestra por su coeficiente y quedarte con la mitad alta del producto da sample * gain / 65536.
Implementa la función apply_fade, que escala cada muestra según su coeficiente. Esta función recibe como argumentos, en este orden:
result: dirección de memoria alineada a 16 bytes donde se escriben todas las muestras escaladas.track: dirección de memoria alineada a 16 bytes de la pista, con n muestras de 16 bits con signo.gains: dirección de memoria alineada a 16 bytes de los coeficientes de ganancia, con n valores de 16 bits con signo, cada uno en el rango de 0 a 32767.n: la cantidad de muestras, que siempre es distinta de cero y múltiplo de 8, como un entero de 64 bits sin signo.Ten en cuenta que la pista es un array que contiene n muestras de 16 bits con signo, donde n nunca es cero y siempre es múltiplo de 8. Para cada muestra, el resultado escalado son los 16 bits altos del producto con signo: sample * gain.
track = {20000, 20000, 20000, 20000, -20000, -20000, -20000, -20000}
gains = {32767, 24576, 16384, 8192, 32767, 24576, 16384, 8192}
result = { 9999, 7500, 5000, 2500, -10000, -7500, -5000, -2500}
Quedarte con la mitad alta redondea hacia menos infinito.
Por eso 20000 * 32767 se escala a 9999, mientras que -20000 * 32767 se escala a -10000 en lugar de -9999.
Esta función no tiene valor de retorno.
El baterista se grabó demasiado fuerte. Para bajar toda la pista, divides cada muestra entre una constante.
Implementa la función attenuate_track. Esta función recibe como argumentos, en este orden:
result: dirección de memoria alineada a 16 bytes donde se escriben los resultados, como enteros de 32 bits con signo.samples: dirección de memoria alineada a 16 bytes de las muestras, n valores de 16 bits con signo.divisor: dirección de memoria alineada a 16 bytes de 4 copias idénticas del divisor, como enteros de 32 bits.n es la cantidad de muestras, que siempre es distinta de cero y múltiplo de 4, como un entero de 64 bits sin signo.Ten en cuenta que samples es un array que contiene n muestras de 16 bits con signo, donde n nunca es cero y siempre es múltiplo de 4.
El divisor se proporciona como 4 copias del mismo entero de 32 bits, ya replicadas para que pueda llenar los cuatro carriles a la vez.
El SIMD de enteros no tiene división empaquetada, así que el trabajo se hace en punto flotante empaquetado y luego se convierte de nuevo a enteros de 32 bits empaquetados para guardarlos en result. Convierte de vuelta truncando hacia cero, así que un cociente de 2.3 se convierte en 2 y -2.3 se convierte en -2.
samples = {300, 150, 90, -120, 100, 50, -50, 7}
divisor = {3, 3, 3, 3}
result = {100, 50, 30, -40, 33, 16, -16, 2}
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.