El código escalar se apoya en las banderas que establecen varias instrucciones para ramificar en respuesta a una condición determinada. Los valores empaquetados, sin embargo, representan no uno sino muchos valores en paralelo. Una sola condición puede fallar para un carril y cumplirse para otro.
Por eso el código SIMD es sin ramificaciones por defecto.
En lugar de apoyarse en las banderas, las comparaciones empaquetadas suelen producir una máscara en el operando de destino.
Para cada carril, la comparación llena todo el carril de unos cuando es verdadera y de ceros cuando es falsa.
Leído como un entero con signo, un carril verdadero es -1 y un carril falso es 0.
Esta máscara se puede componer entonces con operaciones a nivel de bit para filtrar carriles específicos.
Un cmp escalar es genérico en el sentido de que se usa para establecer varias banderas a la vez.
Otra instrucción puede entonces consumir esas banderas para ramificar o para realizar cálculos.
Sin embargo, como una comparación empaquetada comprueba una condición y calcula una máscara al mismo tiempo, no es genérica. A la comparación hay que darle la condición exacta que se está comprobando.
Hay dos formas de hacerlo:
eq para igualdad y gt para mayor que.
Las demás variantes se construyen componiendo el resultado de una de estas.Aparte del uso de un sufijo condicional específico en las comparaciones de enteros, la sintaxis sigue la misma estructura que ya hemos visto:
p + cmp + condición + tamaño (b, w, d o q).cmp + p + tamaño (s o d).
La condición se pasa en un inmediato como operando adicional.Como se ha mencionado, 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 realizar una comparación sin signo, invierte el bit más alto de ambos operandos. Esto se puede hacer con un XOR con una máscara en la que solo el bit más alto esté activado.
Dos expresiones útiles son:
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.
Los carriles de coma 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 pseudoinstrucciones que se corresponden con el inmediato correcto y son más fáciles de recordar.
En todo lo siguiente, la x de px puede ser s (flotantes de 32 bits) o d (flotantes de 64 bits):
| pseudoinstrucció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 |
Una máscara codifica el resultado de una condición. Se puede usar entonces para elegir, carril a 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
Ten en cuenta que la asimetría de pandn resulta ventajosa 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 ramificaciones.
Todos los carriles se calculan, y solo la máscara decide qué valor sobrevive, sin ningún jcc.
Hay instrucciones que realizan esa misma selección directamente, leyendo un bit por elemento de un registro de máscara. Se llaman instrucciones de mezcla:
| 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 |
Ten en cuenta 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, la mezcla conserva el destino cuando el bit más alto del elemento correspondiente de la máscara está a cero, y toma el origen cuando está a uno.
Solo se consulta ese bit más alto, que una máscara de comparación cumple, ya que sus carriles son todo unos o todo ceros.
El registro de máscara es siempre 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.
Estas instrucciones añaden 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á activado.
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 más alto de cada carril a un registro de propósito general:
| instrucción | recoge | ancho del resultado |
|---|---|---|
pmovmskb |
el bit más alto de cada uno de 16 bytes | 16 bits |
movmskps |
el bit más alto de cada uno de 4 dwords | 4 bits |
movmskpd |
el bit más alto de cada uno de 2 qwords | 2 bits |
Si se usan después de una comparación, cada bit activado representa un carril «verdadero» y cada bit a cero, un carril «falso».
Este resultado se puede manipular entonces 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.
También hay una variante empaquetada de la instrucción escalar test: ptest.
Es similar a su equivalente 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 único carril y por eso no lleva prefijo de tamaño.
Si el resultado de una operación AND es 0, se activa ZF, y si la operación ANDN da como resultado 0, es CF la que se activa.
Esto significa que ptest se puede usar para comprobar tanto una máscara todo unos como todo ceros:
ptest sobre un registro consigo mismo activa ZF solo si el registro es todo ceros.
Esto imita la expresión escalar habitual de usar test sobre un registro consigo mismo para comprobar si es 0.ptest sobre un registro con una máscara todo unos activa CF solo si el registro es todo unos.
Además, activa ZF solo si el registro es todo ceros, lo que permite comprobar 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 ramificar o en instrucciones sin ramificaciones como setcc o cmovcc, como de costumbre.
Gestionas el puesto de corrección de un colegio, donde se puntúan los resultados de la clase bloque a bloque.
Cada bloque contiene 4 resultados, y el puesto aplica la misma operación a todos los resultados del bloque. Una puntuación es un número en coma flotante de 32 bits. Varios pasos trabajan con una máscara: un bloque de 4 carriles donde cada carril es o bien todo unos (un sí para ese resultado) o bien 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.
Los cálculos de este ejercicio deben realizarse con instrucciones SIMD.
El primer paso evalúa cada resultado con respecto a un umbral. Un resultado supera el listón cuando su puntuación es estrictamente mayor que el umbral. Cualquier puntuación menor o igual que el umbral no lo supera.
Implementa la función flag_above_threshold, que construye una máscara con un carril todo unos para cada puntuación por encima de su umbral y un carril todo 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 las puntuaciones, con 4 números en coma flotante normales de 32 bits (nunca NaN).thresholds: dirección de memoria del umbral de cada carril, con 4 números en coma 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 devuelve ningún valor.
Un informe aparte destaca los resultados perfectos, aquellos que alcanzaron la puntuación máxima posible.
Implementa la función flag_perfect, que construye una máscara con un carril todo unos para cada puntuación igual a su máximo y un carril todo 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 las puntuaciones, con 4 números en coma flotante normales de 32 bits (nunca NaN).maxima: dirección de memoria de la puntuación máxima de cada carril, con 4 números en coma 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 devuelve ningún valor.
Cada puntuación obtiene un rango del 1 al 3:
50.0.Implementa la función assign_ranks, que escribe el rango de cada puntuación.
Debes definir el umbral de aprobado y los valores de los rangos como constantes empaquetadas en memoria. Puedes reutilizar las funciones de las dos tareas anteriores: una puntuación es al menos de rango 2 cuando está por encima del umbral, y de rango 3 cuando es igual al 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 las puntuaciones, con 4 números en coma flotante normales de 32 bits (nunca NaN).maxima: dirección de memoria de la puntuación máxima de cada carril, con 4 números en coma 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 devuelve ningún valor.
A lo largo del año, cada estudiante acumula un rango total. El puesto cuenta cuántos rangos de toda la cohorte quedan por debajo de un umbral de aprobado, para planificar cuántas clases extra impartir.
Implementa la función count_failures, que devuelve cuántos rangos, en todos los bloques, están estrictamente por debajo del umbral de aprobado.
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 aprobado, 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 recuento como un entero con signo de 32 bits.
Antes de archivar los registros, el puesto comprueba si la cohorte está limpia: se considera aprobada cuando en ningún bloque ha fallado ni un solo resultado.
Implementa la función all_passed, que devuelve 1 si todos los estudiantes han aprobado, 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.
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.