Tracks
/
x86-64 Assembly
x86-64 Assembly
/
Ejercicios
/
Hoja de calificaciones
Hoja de calificaciones

Hoja de calificaciones

Ejercicio de aprendizaje

Introducción

SIMD: Máscaras y condiciones

El código escalar se apoya en las banderas que establecen diversas instrucciones para bifurcar en respuesta a cierta condición. Los valores empaquetados, en cambio, representan no uno sino muchos valores en paralelo. Una misma condición puede fallar en un carril y cumplirse en otro.

Por eso el código SIMD es sin bifurcaciones por defecto.

En lugar de apoyarse en las banderas, las comparaciones empaquetadas suelen producir una máscara en el operando destino. Para cada carril, la comparación llena todo el carril con unos cuando es verdadera y con ceros cuando es falsa. Leído como entero con signo, un carril verdadero es -1 y un carril falso es 0.

Esta máscara se puede combinar luego con operaciones a nivel de bits para filtrar carriles específicos.

Comparaciones empaquetadas

Un cmp escalar es genérico en el sentido de que se usa para establecer varias banderas al mismo tiempo. Otra instrucción puede entonces consumir esas banderas para bifurcar o realizar cálculos.

Sin embargo, como una comparación empaquetada verifica una condición y calcula una máscara a la vez, no es genérica. Hay que indicarle a la comparación la condición exacta que se está evaluando.

Hay dos maneras de hacerlo:

  • A las comparaciones de enteros se les da la condición como sufijo: eq para igualdad y gt para mayor que. Otras variantes se construyen combinando el resultado de una de estas.
  • A las comparaciones de punto flotante se les da la condición codificada en un inmediato. Distintos valores de ese inmediato corresponden a distintas condiciones evaluadas.

Aparte del uso de un sufijo condicional específico en las comparaciones de enteros, la sintaxis sigue la misma estructura que ya vimos:

  • Para enteros, p + cmp + condición + tamaño (b, w, d o q).
  • Para flotantes, cmp + p + tamaño (s o d). La condición se pasa en un inmediato como operando adicional.
Comparaciones de enteros

Como se mencionó, solo hay comparaciones de enteros para igualdad y mayor que:

instrucción descripción
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq igualdad por carril
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq mayor que con signo por carril
movdqa  xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0

Para crear una comparación de menor que, usa gt con los operandos intercambiados: a < b == b > a.

Ten en cuenta que la comparación es con signo. Para hacer una comparación sin signo, invierte el bit superior de ambos operandos. Esto se puede lograr con un XOR con una máscara donde solo el bit superior esté en uno.

Note

Dos expresiones útiles son:

  1. Hacer XOR de un registro consigo mismo para producir todo ceros.
  2. Comparar un registro consigo mismo para producir todo unos.

Por ejemplo:

pxor xmm4, xmm4    ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones

Todo ceros y todo unos son máscaras comunes para codificar «falso en todas partes» y «verdadero en todas partes», respectivamente. También se pueden usar para representar 0 empaquetado o -1 empaquetado, que son valores centinela comunes. Por ejemplo, el NUL que marca el final de un string es un 0.

Comparaciones de punto flotante

Los carriles de punto flotante usan una forma distinta: una sola instrucción, cmpps (y cmppd para carriles de 64 bits), con la condición como inmediato:

movaps xmm0, [rel readings]
cmpps  xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]

NASM también tiene pseudooperaciones que se asignan al inmediato correcto y son más fáciles de recordar. En todos los siguientes, x en px puede ser s (flotantes de 32 bits) o d (flotantes de 64 bits):

pseudooperación inmediato comparación
cmpeqpx 0 a == b
cmpltpx 1 a < b
cmplepx 2 a <= b
cmpunordpx 3 a es NaN o b es NaN
cmpneqpx 4 a != b
cmpnltpx 5 a >= b
cmpnlepx 6 a > b
cmpordpx 7 ni a ni b es NaN

Seleccionar con una máscara

Una máscara codifica el resultado de una condición. Luego se puede usar para elegir, carril por carril, entre dos conjuntos de valores según esa condición. Tomamos el carril de un valor donde la máscara es verdadera y de otro donde es falsa:

; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0  ; xmm0 holds the mask, keep a copy
pand   xmm2, xmm3  ; xmm2 = a AND mask: lanes of a where mask is true
pandn  xmm0, xmm4  ; xmm0 = NOT mask AND b: lanes of b where mask is false
por    xmm2, xmm0  ; combine the two halves

Fíjate que la asimetría de pandn da sus frutos aquí: la máscara está en el destino, se niega y selecciona de b en una sola instrucción.

Este patrón es la forma empaquetada de la selección sin bifurcaciones. Todos los carriles se calculan, y solo la máscara decide qué valor sobrevive, sin ningún jcc en ninguna parte.

Blends dedicados

Hay instrucciones que realizan esa misma selección directamente, leyendo un bit por elemento desde un registro de máscara. Se llaman instrucciones blend:

instrucción elemento origen de la máscara
pblendvb byte xmm0 implícito
blendvps carril de 32 bits xmm0 implícito
blendvpd carril de 64 bits xmm0 implícito

Fíjate que la primera instrucción sigue la sintaxis de enteros, mientras que las otras dos siguen la sintaxis de flotantes. Sin embargo, como estas instrucciones simplemente seleccionan bytes sin procesar, cualquiera de ellas se puede usar tanto con enteros como con flotantes.

Para cada elemento, el blend conserva el destino cuando el bit superior del elemento de máscara correspondiente está en cero, y toma el origen cuando está en uno. Solo se consulta ese bit superior, algo que una máscara de comparación cumple, ya que sus carriles son todo unos o todo ceros. El registro de máscara siempre es xmm0, que es implícito:

movaps   xmm0, [rel mask]  ; the selecting mask must be in xmm0
movaps   xmm1, [rel b]     ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a]     ; source: taken where the mask bit is set

También es posible usar pblendvb para seleccionar carriles de una máscara de comparación para cualquier otro tamaño. Como todos los bytes de un carril verdadero son todo unos, pblendvb los selecciona todos.

Note

Estas instrucciones agregan una v después de la operación que se realiza (blend). Esa v significa variable, porque la selección no es estática: depende de un registro.

También hay variantes sin v, que seleccionan según un inmediato. Siguen el mismo patrón: seleccionan un carril i si el bit i del inmediato está en uno.

De una máscara de vuelta a un escalar

Aunque es potente, el código SIMD carece de buena parte de la flexibilidad del código escalar. En muchas situaciones, es necesario pasar de un registro empaquetado de vuelta al mundo de las instrucciones escalares.

La familia de instrucciones movmsk sirve de puente entre los dos mundos. Estas instrucciones extraen el bit superior de cada carril hacia un registro de propósito general:

instrucción recoge ancho del resultado
pmovmskb el bit superior de cada uno de 16 bytes 16 bits
movmskps el bit superior de cada uno de 4 dwords 4 bits
movmskpd el bit superior de cada uno de 2 qwords 2 bits

Si se usa después de una comparación, cada bit en uno representa un carril «verdadero» y cada bit en cero, un carril «falso». Ese resultado se puede manipular luego como de costumbre con instrucciones escalares. Por ejemplo, un popcnt cuenta el número de coincidencias y tzcnt encuentra la primera.

El registro de propósito general puede ser de 32 o 64 bits de ancho.

Probar un vector completo

También hay una variante empaquetada de la instrucción escalar test: ptest.

Es similar a su contraparte escalar en que realiza una operación AND entre dos operandos, sin modificarlos. A diferencia de test, ptest también realiza una operación ANDN, negando el primer operando.

Así, ptest se puede concebir como una versión no destructiva de pand y pandn que establece banderas según el resultado. De manera muy similar a estas dos instrucciones, ptest trata todo el registro SIMD como un solo carril y por eso no lleva prefijo de tamaño.

Si el resultado de una operación AND es 0, se establece ZF, y si la operación ANDN da 0, se establece CF. Esto significa que ptest se puede usar para verificar tanto una máscara de todo unos como una de todo ceros:

  1. Usar ptest en un registro consigo mismo establece ZF solo si el registro es todo ceros. Esto imita la expresión escalar común de usar test en un registro consigo mismo para verificar si es 0.
  2. Usar ptest en un registro con una máscara de todo unos establece CF solo si el registro es todo unos. Además, establece ZF solo si el registro es todo ceros, lo que permite verificar ambas máscaras a la vez.
pxor    xmm0, xmm0   ; all zeros
pcmpeqb xmm1, xmm1   ; all ones
pcmpeqb xmm2, xmm2

ptest xmm0, xmm0     ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1     ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1     ; CF is set only if xmm2 is all ones

El resultado de un ptest se puede usar para bifurcar o en instrucciones sin bifurcaciones como setcc o cmovcc, como de costumbre.

Instrucciones

Administras la estación de calificación de una escuela, que puntúa los resultados de la clase bloque por bloque.

Cada bloque contiene 4 resultados, y la estación aplica la misma operación a cada resultado del bloque. Un puntaje es un número de punto flotante de 32 bits. Varios pasos trabajan con una máscara: un bloque de 4 carriles donde cada carril es o todo unos (un sí para ese resultado) o todo ceros (un no).

Tienes cinco tareas. Recibes los operandos a través de direcciones de memoria. Algunas tareas escriben su respuesta en una dirección de resultado, mientras que otras la devuelven directamente.

Todas las direcciones de memoria de este ejercicio están alineadas a 16 bytes.

Note

Los cálculos de este ejercicio deben realizarse con instrucciones SIMD.

1. Marca los puntajes por encima del umbral

El primer paso califica cada resultado contra un umbral. Un resultado supera el límite cuando su puntaje es estrictamente mayor que el umbral. Un puntaje menor o igual que el umbral no lo supera.

Implementa la función flag_above_threshold, que construye una máscara con un carril de puros unos para cada puntaje por encima de su umbral y un carril de puros ceros en caso contrario.

Esta función recibe como argumentos, en este orden:

  • result: dirección de memoria de un búfer donde se escriben los 4 carriles de la máscara.
  • scores: dirección de memoria de los puntajes, con 4 números de punto flotante normales de 32 bits (nunca NaN).
  • thresholds: dirección de memoria del umbral de cada carril, con 4 números de punto flotante normales de 32 bits (nunca NaN).
scores     = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result     = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Esta función no tiene valor de retorno.

2. Marca los puntajes perfectos

Un informe aparte destaca los resultados perfectos, aquellos que alcanzaron la calificación máxima posible.

Implementa la función flag_perfect, que construye una máscara con un carril de puros unos para cada puntaje igual a su máximo y un carril de puros ceros en caso contrario.

Esta función recibe como argumentos, en este orden:

  • result: dirección de memoria de un búfer donde se escriben los 4 carriles de la máscara.
  • scores: dirección de memoria de los puntajes, con 4 números de punto flotante normales de 32 bits (nunca NaN).
  • maxima: dirección de memoria de la calificación máxima de cada carril, con 4 números de punto flotante normales de 32 bits (nunca NaN).
scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Esta función no tiene valor de retorno.

3. Asigna un rango

Cada puntaje obtiene un rango del 1 al 3:

  • Rango 1 para un puntaje igual o inferior al umbral de aprobación de 50.0.
  • Rango 2 para un puntaje por encima de ese umbral pero menor que el máximo.
  • Rango 3 para un puntaje perfecto, uno que iguala el máximo.

Implementa la función assign_ranks, que escribe el rango de cada puntaje.

Debes definir el umbral de aprobación y los valores de los rangos como constantes empaquetadas en memoria. Las funciones de las dos tareas anteriores se pueden reutilizar: un puntaje es al menos rango 2 cuando está por encima del umbral, y rango 3 cuando iguala el máximo.

Esta función recibe como argumentos, en este orden:

  • result: dirección de memoria de un búfer donde se escriben los 4 rangos, cada uno un entero sin signo de 32 bits.
  • scores: dirección de memoria de los puntajes, con 4 números de punto flotante normales de 32 bits (nunca NaN).
  • maxima: dirección de memoria de la calificación máxima de cada carril, con 4 números de punto flotante normales de 32 bits (nunca NaN).
scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}

Esta función no tiene valor de retorno.

4. Cuenta los fallos

A lo largo del año, cada estudiante acumula un rango total. La estación cuenta cuántos rangos de toda la cohorte quedan por debajo de un umbral de aprobación, para planificar cuántas clases adicionales impartir.

Implementa la función count_failures, que devuelve cuántos rangos, en todos los bloques, están estrictamente por debajo del umbral de aprobación. El umbral se da como un bloque de 4 carriles idénticos, así que puedes cargarlo una vez y reutilizarlo para cada bloque.

Esta función recibe como argumentos, en este orden:

  • ranks: dirección de memoria de los rangos, un número entero de bloques de 4 carriles, cada rango un entero sin signo de 32 bits.
  • block_count: el número de bloques de 4 carriles, siempre mayor que 0.
  • pass_threshold: dirección de memoria del umbral de aprobación, con 4 enteros idénticos de 32 bits.
ranks          = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count    = 2
pass_threshold = {2, 2, 2, 2}
// => 3

Esta función devuelve el conteo como un entero con signo de 32 bits.

5. ¿Aprobaron todos?

Antes de archivar los registros, la estación verifica si la cohorte está limpia: pasa cuando ni un solo resultado falló en ningún bloque.

Implementa la función all_passed, que devuelve 1 si todos los estudiantes aprobaron, y 0 en caso contrario. Un estudiante aprueba cuando su carril correspondiente en el array failing es todo ceros.

Esta función recibe como argumentos, en este orden:

  • failing: dirección de memoria de las máscaras de fallo, un número entero de bloques de 4 carriles, cada carril todo unos o todo ceros.
  • block_count: el número de bloques de 4 carriles, siempre mayor que 0.
failing     = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
               0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1

Esta función devuelve la respuesta como un entero con signo de 32 bits, ya sea 1 o 0.

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

¿Todo listo para empezar Hoja de calificaciones?

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