Byte Swap com SSE2
Encontrava-me hoje na Internet descansado quando o raxx7 começou a falar de optimizar uma função simples, mas engraçada -- inverter a ordem dos bytes de um inteiro de 64 bits. Vou desde já assumir x86, visto que em amd64 existe uma instrução nativa que faz isto em apenas 4 ciclos (latência no core2) e é provavelmente rápido o suficiente. Mas em x86 precisamos de 2 instruções destas e 2+2 acessos à memória. Assim, peguei na identidade simples:
BSWAP64(x) = BSWAP32(x>>32)|BSWAP32(x&0xFFFFFFFF)
BSWAP32(x) = ((ROTL32((x), 8) & 0x00FF00FF) | (ROTL32((x), 24) & 0xFF00FF00))
,onde ROTL32 significa uma rotação de n bits à esquerda. Infelizmente, as extensões SSE2 da arquitectura x86 não possuem instruções específicas de rotação, forçando-nos a utilizar outra identidade bastante útil:
ROTL32(x, n) = ((x< < n) | (x >> (32-n)))
Neste momento, já temos tudo o que precisamos para efectuar a operação desejada em SSE2. No entanto, parece haver aqui um excesso de computação que torna este método demasiado ineficiente: 10 operações lógicas -- 5 booleanas, 4 shifts e 1 shuffle. Mas a pipeline do Core 2 é extremamente boa: conseguimos efectuar 3 operações booleanas por ciclo, 1 shift por ciclo e 1 shuffle por ciclo. Se conseguirmos esconder a latência dos shifts com as operações booleanas, vamos obter uma contagem de ciclos não muito superior à dos 2 bswap de 32 bits. Além do mais, efectuamos 2 inversões simultâneas, dado que os registos XMM têm 128 bits; seria um desperdício não o fazer. Os acessos à memória também são mais eficientes, com acessos contíguos de 16 bytes (com o unrolling correcto processamos uma cache line (64 bytes) inteira de uma só vez).
Fiz então uma função para completar o exercício:
BITS 32
%define UNROLL_COUNT (4)
section .data
mask1: dd 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff
mask2: dd 0xff00ff00, 0xff00ff00, 0xff00ff00, 0xff00ff00
section .text
global _sse2_bswap64
_sse2_bswap64:
push ebp
mov edx, [esp+8] ; buffer -- assumed aligned 16
mov ecx, [esp+12] ; length in #words
test ecx, ecx
jz _end
and ecx, -(UNROLL_COUNT*2) ; make ecx even
jz _finalize
movdqa xmm6, [mask1]
movdqa xmm7, [mask2]
align 16
_loop:
sub ecx, UNROLL_COUNT*2
%assign i 0
%rep UNROLL_COUNT
movdqa xmm0, [edx] ; p2
movdqa xmm1, xmm0 ; p0/p1/p5
pslld xmm0, 8 ; p0
movdqa xmm2, xmm1 ; p0/p1/p5
psrld xmm1, 24; ; p0
movdqa xmm3, xmm2 ; p0/p1/p5
pslld xmm2, 24 ; p0
por xmm0, xmm1 ; p0/p1/p5
psrld xmm3, 8 ; p0
por xmm2, xmm3 ; p0/p1/p5
pand xmm0, xmm6 ; p0/p1/p5
pand xmm2, xmm7 ; p0/p1/p5
por xmm0, xmm2 ; p0/p1/p5
pshufd xmm0, xmm0, 10110001b ; p5 + p0/p1
movdqa [edx], xmm0 ; p2
lea edx, [edx+16] ; p0
%assign i i + 1
%endrep
jnz _loop ; no dependency, all flags were
; computed in the beginning of the loop
_finalize:
mov ebp, [esp+8]
and ebp, (UNROLL_COUNT*2)-1 ; ebp = count mod UNROLL
jz _end
_endloop:
mov eax, [edx]
bswap eax ; p0+p5
mov ecx, [edx+4]
bswap ecx
mov [edx], ecx
mov [edx+4], eax
sub ebp, 1
lea edx, [edx+8]
jnz _endloop
_end:
pop ebp
ret
Este código pode ser usado com o yasm ou nasm (o yasm em win32 não acertava com os endereços das masks, não sei se era bug no exportador para COFF ou AIDS). Esta função, junto com código para testá-la encontra-se aqui.
A performance obtida não foi tão boa como esperado, mas mantém-se competitiva com a alternativa: no Core 2 (65 nm) onde testei, a versão SSE2 era ~2% mais rápida. A utilidade de todo este exercício é, assim, discutível.
Bem-haja.
Adoro o ROTL32 ;-)