Tracks
/
x86-64 Assembly
x86-64 Assembly
/
Ejercicios
/
Consola de mezclas
Consola de mezclas

Consola de mezclas

Ejercicio de aprendizaje

Introducción

SIMD: enteros

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.

Sintaxis

Gran parte del modelo de punto flotante de SIMD se traslada sin cambios a los enteros empaquetados:

  • Un registro de 128 bits se divide en carriles
  • Las instrucciones actúan en paralelo sobre los carriles que están en la misma posición
  • Los operandos de memoria siguen las mismas reglas de alineación de 16 bytes

Sin embargo, la sintaxis es un poco distinta:

  1. Un prefijo p que indica que la instrucción opera sobre datos empaquetados.
  2. La operación que se realiza, con el mismo nombre que su equivalente no SIMD (también llamado escalar) (por ejemplo, add, mul, etc.).
  3. Un sufijo que indica el tamaño de cada carril.

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

Movimientos de memoria

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.

Note

Aquí dq significa double-qword, es decir, 128 bits (16 bytes).

Suma y resta

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.

Suma y resta saturantes

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:

  • Los bytes sin signo se acotan a [0, 255]: un paddusb de 200 + 100 da 255, y un psubusb de 5 - 10 da 0.
  • Los bytes con signo se acotan a [-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.

Note

La suma y la resta saturantes existen solo para carriles de byte y word, no para dword ni qword.

Multiplicación

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:

  1. pmulhw, para la multiplicación con signo.
  2. pmulhuw, con una u adicional, para la multiplicación sin signo.

Fíjate en la sintaxis:

  1. Una p, para entero empaquetado.
  2. La operación que se realiza, mul.
  3. Una h, que indica que se están seleccionando los bits altos («high») del resultado.
  4. Una u opcional si el resultado debe interpretarse como sin signo (es decir, no tiene extensión de signo).
  5. Por último, el sufijo de tamaño 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.

División

No hay división de enteros empaquetada. El código que la necesita convierte los valores a punto flotante, divide y vuelve a convertir.

Ensanchamiento de enteros

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.

Conversión entre enteros y flotantes

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
Caution

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

Instrucciones

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.

Note

En este ejercicio, los cálculos deben realizarse usando instrucciones SIMD, no operaciones escalares.

1. Mezcla dos pistas

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.

2. Elimina el sangrado

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.

3. Combina los medidores

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.

4. Aplica un fundido

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}
Note

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.

5. Atenúa una pista

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.

Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
x86-64 Assembly Exercism

¿Todo listo para empezar Consola de mezclas?

Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.