Código de máquina x86 (MMX / SSE1), 26 bytes (4x int16_t)
Código de máquina x86 (SSE4.1), 28 bytes (4x int32_t o uint32_t)
Código de máquina x86 (SSE2), 24 bytes (4x float32) o 27B para cvt int32
(La última versión que convierte int32 en flotante no es perfectamente precisa para enteros grandes que se redondean al mismo flotante. Con la entrada flotante, el problema es el redondeador y esta función funciona correctamente si no hay NaN, identificando flotantes que se comparan == al máximo Las versiones enteras funcionan para todas las entradas, tratándolas como complemento de 2 firmado).
Todo esto funciona en modo 16/32/64 bits con el mismo código de máquina.
Una convención de llamadas de stack-args haría posible recorrer los args dos veces (encontrar max y luego comparar), posiblemente dándonos una implementación más pequeña, pero no he probado ese enfoque.
x86 SIMD tiene un mapa de bits vector-> entero como una sola instrucción ( pmovmskb
o movmskps
pd), por lo que era natural para esto a pesar de que las instrucciones MMX / SSE tienen al menos 3 bytes de longitud. Las instrucciones SSSE3 y posteriores son más largas que SSE2, y las instrucciones MMX / SSE1 son las más cortas. Se pmax*
introdujeron diferentes versiones de (max. Entero vertical empaquetado) en diferentes momentos, con SSE1 (para mmx regs) y SSE2 (para xmm regs) solo con palabra firmada (16 bits) y byte sin firmar.
( pshufw
y pmaxsw
en los registros MMX son nuevos con Katmai Pentium III, por lo que realmente requieren SSE1, no solo el bit de la función MMX CPU).
Esto se puede llamar desde C como unsigned max4_mmx(__m64)
con el i386 System V ABI, que pasa un __m64
argumento en mm0
. (No x86-64 System V, que pasa __m64
en xmm0
!)
line code bytes
num addr
1 global max4_mmx
2 ;; Input 4x int16_t in mm0
3 ;; output: bitmap in EAX
4 ;; clobbers: mm1, mm2
5 max4_mmx:
6 00000000 0F70C8B1 pshufw mm1, mm0, 0b10110001 ; swap adjacent pairs
7 00000004 0FEEC8 pmaxsw mm1, mm0
8
9 00000007 0F70D14E pshufw mm2, mm1, 0b01001110 ; swap high/low halves
10 0000000B 0FEECA pmaxsw mm1, mm2
11
12 0000000E 0F75C8 pcmpeqw mm1, mm0 ; 0 / -1
13 00000011 0F63C9 packsswb mm1, mm1 ; squish word elements to bytes, preserving sign bit
14
15 00000014 0FD7C1 pmovmskb eax, mm1 ; extract the high bit of each byte
16 00000017 240F and al, 0x0F ; zero out the 2nd copy of the bitmap in the high nibble
17 00000019 C3 ret
size = 0x1A = 26 bytes
Si hubiera un pmovmskw
, lo que habría guardado el packsswb
y el and
(3 + 2 bytes). No necesitamos and eax, 0x0f
porque pmovmskb
en un registro MMX ya ceramos los bytes superiores. Los registros MMX tienen solo 8 bytes de ancho, por lo que AL de 8 bits cubre todos los posibles bits distintos de cero.
Si supiéramos que nuestras entradas no son negativas, podríamospacksswb mm1, mm0
producir bytes con signo no negativo en los 4 bytes superiores de mm1
, evitando la necesidad de and
después pmovmskb
. Así 24 bytes.
El paquete x86 con saturación con signo trata la entrada y la salida como con signo, por lo que siempre conserva el bit de signo. ( https://www.felixcloutier.com/x86/packsswb:packssdw ). Dato curioso: el paquete x86 con saturación sin signo todavía trata la entrada como firmada. Esta podría ser la razón PACKUSDW
por la que no se introdujo hasta SSE4.1, mientras que las otras 3 combinaciones de tamaño y firma existieron desde MMX / SSE2.
O con enteros de 32 bits en un registro XMM (y en pshufd
lugar de pshufw
), cada instrucción necesitaría un byte de prefijo más, excepto para movmskps
reemplazar el paquete / y. Pero pmaxsd
/ pmaxud
necesita un byte extra extra ...
invocable desde C comounsigned max4_sse4(__m128i);
con x86-64 System V, o MSVC vectorcall ( -Gv
), ambos pasan __m128i
/ __m128d
/ __m128
args en los registros XMM que comienzan con xmm0
.
20 global max4_sse4
21 ;; Input 4x int32_t in xmm0
22 ;; output: bitmap in EAX
23 ;; clobbers: xmm1, xmm2
24 max4_sse4:
25 00000020 660F70C8B1 pshufd xmm1, xmm0, 0b10110001 ; swap adjacent pairs
26 00000025 660F383DC8 pmaxsd xmm1, xmm0
27
28 0000002A 660F70D14E pshufd xmm2, xmm1, 0b01001110 ; swap high/low halves
29 0000002F 660F383DCA pmaxsd xmm1, xmm2
30
31 00000034 660F76C8 pcmpeqd xmm1, xmm0 ; 0 / -1
32
33 00000038 0F50C1 movmskps eax, xmm1 ; extract the high bit of each dword
34 0000003B C3 ret
size = 0x3C - 0x20 = 28 bytes
O si aceptamos la entrada como float
, podemos usar las instrucciones SSE1. El float
formato puede representar una amplia gama de valores enteros ...
O si cree que eso está doblando las reglas demasiado, comience con un byte 0F 5B C0 cvtdq2ps xmm0, xmm0
para convertir, haciendo una función de 27 bytes que funcione para todos los enteros que son exactamente representables como IEEE binary32 float
, y muchas combinaciones de entradas donde algunas de las entradas obtienen redondeado a un múltiplo de 2, 4, 8 o lo que sea durante la conversión. (Por lo tanto, es 1 byte más pequeño que la versión SSE4.1 y funciona en cualquier x86-64 con solo SSE2).
Si alguna de las entradas flotantes es NaN, tenga en cuenta que maxps a,b
se implementa exactamente (a<b) ? a : b
, manteniendo el elemento del segundo operando en desordenado . Por lo tanto, es posible que esto regrese con un mapa de bits distinto de cero, incluso si la entrada contiene algo de NaN, dependiendo de dónde se encuentren.
unsigned max4_sse2(__m128);
37 global max4_sse2
38 ;; Input 4x float32 in xmm0
39 ;; output: bitmap in EAX
40 ;; clobbers: xmm1, xmm2
41 max4_sse2:
42 ; cvtdq2ps xmm0, xmm0
43 00000040 660F70C8B1 pshufd xmm1, xmm0, 0b10110001 ; swap adjacent pairs
44 00000045 0F5FC8 maxps xmm1, xmm0
45
46 00000048 660F70D14E pshufd xmm2, xmm1, 0b01001110 ; swap high/low halves
47 0000004D 0F5FCA maxps xmm1, xmm2
48
49 00000050 0FC2C800 cmpeqps xmm1, xmm0 ; 0 / -1
50
51 00000054 0F50C1 movmskps eax, xmm1 ; extract the high bit of each dword
52 00000057 C3 ret
size = 0x58 - 0x40 = 24 bytes
copiar y mezclar con pshufd
sigue siendo nuestra mejor apuesta: shufps dst,src,imm8
lee la entrada para la mitad baja dst
de dst
. Y necesitamos un copy-and-shuffle no destructivo las dos veces, por lo que 3-byte movhlps
y unpckhps
/ pd están fuera. Si estuviéramos reduciendo a un máximo escalar, podríamos usarlos, pero cuesta emitir otra instrucción antes de comparar si no tenemos el máximo en todos los elementos.
Relacionado: SSE4.1 phminposuw
puede encontrar la posición y el valor del mínimo uint16_t
en un registro XMM. No creo que sea una ganancia restar 65535 para usarlo como máximo, pero veo una respuesta SO sobre cómo usarlo para un máximo de bytes o enteros con signo.