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 ou uint32_t)
código de máquina x86 (SSE2), 24 bytes (4x float32) ou 27B para cvt int32
(A última versão que converte int32 em float não é perfeitamente precisa para números inteiros grandes que arredondam para o mesmo float. Com a entrada float, o arredondamento é o problema do chamador e essa função funciona corretamente se não houver NaNs, identificando os floats que comparam == ao máximo. As versões inteiras funcionam para todas as entradas, tratando-as como um complemento assinado de 2.)
Tudo isso funciona no modo 16/32/64 bits com o mesmo código de máquina.
Uma convenção de chamada stack-args tornaria possível fazer um loop sobre os argumentos duas vezes (localizando max e depois comparando), possivelmente nos fornecendo uma implementação menor, mas ainda não tentei essa abordagem.
O x86 SIMD possui bitmap vetor-> inteiro como uma única instrução ( pmovmskb
ou movmskps
pd); portanto, era natural para isso, embora as instruções MMX / SSE tenham pelo menos 3 bytes de comprimento. As instruções SSSE3 e posteriores são mais longas que SSE2 e as instruções MMX / SSE1 são as mais curtas. Versões diferentes de pmax*
(máximo vertical inteiro compactado) foram introduzidas em momentos diferentes, com SSE1 (para mmx regs) e SSE2 (para xmm regs) tendo apenas palavra assinada (16 bits) e byte não assinado.
( pshufw
e pmaxsw
nos registros MMX são novos no Katmai Pentium III, eles realmente requerem SSE1, não apenas o bit de recurso da CPU MMX.)
É possível chamar a partir de C, como unsigned max4_mmx(__m64)
no i386 System V ABI, que passa um __m64
argumento mm0
. (Não x86-64 System V, que passa __m64
em 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
Se houvesse um pmovmskw
, o que teria salvo o packsswb
e o and
(3 + 2 bytes). Não precisamos, and eax, 0x0f
porque pmovmskb
em um registrador MMX já zeros os bytes superiores. Os registros MMX têm apenas 8 bytes de largura, portanto, o AL de 8 bits cobre todos os bits diferentes de zero possíveis.
Se soubéssemos que nossas entradas não eram negativas, poderíamospacksswb mm1, mm0
produzir bytes assinados não negativos nos 4 bytes superiores de mm1
, evitando a necessidade de and
depois pmovmskb
. Assim, 24 bytes.
O pacote x86 com saturação assinada trata a entrada e a saída como assinadas, preservando sempre o bit de sinal. ( https://www.felixcloutier.com/x86/packsswb:packssdw ). Curiosidade: o pacote x86 com saturação não assinada ainda trata a entrada como assinada. Talvez por isso PACKUSDW
não tenha sido introduzido até o SSE4.1, enquanto as outras três combinações de tamanho e assinatura existiam desde o MMX / SSE2.
Ou com números inteiros de 32 bits em um registro XMM (e em pshufd
vez de pshufw
), todas as instruções precisariam de mais um byte de prefixo, exceto para movmskps
substituir o pacote / e. Mas pmaxsd
/ pmaxud
preciso de um byte extra extra ...
pode ser chamado de C comounsigned max4_sse4(__m128i);
no x86-64 System V ou MSVC vectorcall ( -Gv
), sendo que ambos passam __m128i
/ __m128d
/ __m128
args nos registros XMM começando com 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
Ou, se aceitarmos a entrada como float
, podemos usar as instruções SSE1. O float
formato pode representar uma ampla gama de valores inteiros ...
Ou se você acha que isso está distorcendo as regras demais, comece com 3 bytes 0F 5B C0 cvtdq2ps xmm0, xmm0
para converter, criando uma função de 27 bytes que funciona para todos os números inteiros que são exatamente representáveis como IEEE binary32 float
e muitas combinações de entradas nas quais algumas das entradas são obtidas . arredondado para um múltiplo de 2, 4, 8 ou o que for durante a conversão. (Portanto, é 1 byte menor que a versão SSE4.1 e funciona em qualquer x86-64 com apenas SSE2.)
Se alguma das entradas flutuantes for NaN, observe que é maxps a,b
implementado exatamente (a<b) ? a : b
, mantendo o elemento do 2º operando em desordenado . Portanto, pode ser possível retornar com um bitmap diferente de zero, mesmo que a entrada contenha algum NaN, dependendo de onde estejam.
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 e shuffle com pshufd
ainda é nossa melhor aposta: shufps dst,src,imm8
lê a entrada para a metade baixa dst
da dst
. E precisamos de uma cópia e reprodução aleatória não destrutiva as duas vezes, para que 3 bytes movhlps
e unpckhps
/ pd estejam fora. Se reduzíssemos a um máximo escalar, poderíamos usá-lo, mas custa outra instrução para transmitir antes da comparação, se ainda não tivermos o máximo em todos os elementos.
Relacionado: O SSE4.1 phminposuw
pode encontrar a posição e o valor do mínimo uint16_t
em um registro XMM. Eu não acho que é uma vitória subtrair do 65535 para usá-lo no máximo, mas veja uma resposta SO sobre como usá-lo para o máximo de bytes ou números inteiros assinados.