kod maszynowy x86 (MMX / SSE1), 26 bajtów (4x int16_t)
kod maszynowy x86 (SSE4.1), 28 bajtów (4x int32_t lub uint32_t)
kod maszynowy x86 (SSE2), 24 bajty (4x float32) lub 27B na cvt int32
(Ostatnia wersja, która konwertuje int32 na liczbę zmiennoprzecinkową, nie jest idealnie dokładna dla dużych liczb całkowitych, które zaokrąglają do tej samej liczby zmiennoprzecinkowej. Przy wprowadzaniu liczby zmiennoprzecinkowej zaokrąglanie jest problemem osoby wywołującej, a funkcja ta działa poprawnie, jeśli nie ma NaN, identyfikując liczby zmiennoprzecinkowe, które = maksymalnie. Wersje całkowite działają dla wszystkich danych wejściowych, traktując je jak uzupełnienie ze znakiem 2).
Wszystkie działają w trybie 16/32/64-bit z tym samym kodem maszynowym.
Konwencja wywoływania stosu-argumentów umożliwia dwukrotne zapętlenie argumentów (znalezienie maksimum, a następnie porównanie), co może dać nam mniejszą implementację, ale nie próbowałem tego podejścia.
SIM86 x86 ma bitmapę wektor-> liczba całkowita jako pojedynczą instrukcję ( pmovmskb
lub movmskps
pd), więc było to naturalne, mimo że instrukcje MMX / SSE mają co najmniej 3 bajty. Instrukcje SSSE3 i nowsze są dłuższe niż SSE2, a instrukcje MMX / SSE1 są najkrótsze. Różne wersje pmax*
(maksymalne upakowane pionowe maksimum) zostały wprowadzone w różnym czasie, przy czym SSE1 (dla rejestrów mmx) i SSE2 (dla rejestrów xmm) miały tylko podpisane słowo (16-bit) i bajt bez znaku.
( pshufw
I pmaxsw
na MMX rejestry są nowe z Pentium III Katmai, tak naprawdę wymagają one SSE1, a nie tylko funkcji bitowy procesor MMX).
Można to wywołać z poziomu C, podobnie jak unsigned max4_mmx(__m64)
w systemie ABI systemu i386 V, który przekazuje __m64
argument mm0
. (Nie x86-64 System V, który przechodzi __m64
w 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
Gdyby istniał pmovmskw
, co zaoszczędziłoby packsswb
i and
(3 + 2 bajty). Nie potrzebujemy, and eax, 0x0f
ponieważ pmovmskb
w rejestrze MMX już zeruje górne bajty. Rejestry MMX mają tylko 8 bajtów szerokości, więc 8-bitowy AL obejmuje wszystkie możliwe niezerowe bity.
Gdybyśmy wiedzieli, że nasze dane wejściowe są nieujemne, moglibyśmypacksswb mm1, mm0
wygenerować nieujemne bajty ze znakiem w górnych 4 bajtach mm1
, unikając potrzeby and
późniejszego pmovmskb
. Zatem 24 bajty.
Pakiet x86 z podpisanym nasyceniem traktuje wejście i wyjście jako podpisane, więc zawsze zachowuje bit znaku. ( https://www.felixcloutier.com/x86/packsswb:packssdw ). Ciekawostka: pakiet x86 z niepodpisanym nasyceniem nadal traktuje dane wejściowe jako podpisane. Być może dlatego PACKUSDW
został wprowadzony dopiero w SSE4.1, podczas gdy pozostałe 3 kombinacje rozmiaru i podpisu istniały od MMX / SSE2.
Lub z 32-bitowymi liczbami całkowitymi w rejestrze XMM (i pshufd
zamiast pshufw
) każda instrukcja wymagałaby jeszcze jednego bajtu prefiksu, z wyjątkiem movmskps
zastąpienia pack / i. Ale pmaxsd
/ pmaxud
potrzebujesz dodatkowego dodatkowego bajtu ...
wywoływalne z C jakunsigned max4_sse4(__m128i);
w x86-64 System V lub MSVC vectorcall ( -Gv
), z których oba przekazują __m128i
/ __m128d
/ __m128
args w regach XMM, zaczynając od 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
Lub jeśli zaakceptujemy dane wejściowe jako float
, możemy użyć instrukcji SSE1. float
Format może reprezentować szeroki zakres liczb całkowitych ...
Lub jeśli uważasz, że to wygina reguły zbyt daleko, zacznij od 3-bajtowego 0F 5B C0 cvtdq2ps xmm0, xmm0
do konwersji, tworząc 27-bajtową funkcję, która działa dla wszystkich liczb całkowitych, które są dokładnie reprezentowane jako binarne IEEE32 float
, oraz dla wielu kombinacji danych wejściowych, do których trafiają niektóre dane wejściowe zaokrąglona do wielokrotności 2, 4, 8 lub dowolnej innej wartości podczas konwersji. (Więc jest o 1 bajt mniejszy niż wersja SSE4.1 i działa na każdym x86-64 z tylko SSE2.)
Jeśli którekolwiek z danych zmiennoprzecinkowych to NaN, zauważ, że maxps a,b
dokładnie to implementuje (a<b) ? a : b
, utrzymując element z drugiego argumentu na nieuporządkowanym . Może więc być możliwe zwrócenie z niezerową bitmapą, nawet jeśli wejście zawiera trochę NaN, w zależności od tego, gdzie się znajdują.
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
kopiowanie i tasowanie pshufd
jest nadal naszym najlepszym wyborem: shufps dst,src,imm8
odczytuje dane wejściowe dla dolnej połowy dst
z dst
. I potrzebujemy nieniszczącego kopiowania i tasowania za każdym razem, więc 3-bajtowe movhlps
i unpckhps
/ pd są wyłączone. Gdybyśmy zawężali do skalarnego maksimum, moglibyśmy je wykorzystać, ale kosztuje to kolejną instrukcję do nadania przed porównaniem, jeśli nie mamy jeszcze maksimum we wszystkich elementach.
Powiązane: SSE4.1 phminposuw
może znaleźć pozycję i wartość minimum uint16_t
w rejestrze XMM. Nie sądzę, aby wygrywanie odejmowało od 65535, aby używać go dla maksimum, ale zobacz odpowiedź SO na temat używania go dla maks. Bajtów lub liczb całkowitych ze znakiem.