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.
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:
eq para igualdad y gt para mayor que.
Otras variantes se construyen combinando 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 vimos:
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 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.
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 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 |
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.
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.
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.
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.
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:
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.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.
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.
Los cálculos de este ejercicio deben realizarse con instrucciones SIMD.
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.
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.
Cada puntaje obtiene un rango del 1 al 3:
50.0.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.
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.
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.
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.