x86-Maschinencode (MMX / SSE1), 26 Byte (4x int16_t)
x86-Maschinencode (SSE4.1), 28 Byte (4x int32_t oder uint32_t)
x86-Maschinencode (SSE2), 24 Byte (4x float32) oder 27B an cvt int32
(Die letzte Version, die int32 in float konvertiert, ist für große Ganzzahlen, die auf denselben float runden, nicht genau. Bei float-Eingaben ist das Runden das Problem des Aufrufers, und diese Funktion funktioniert ordnungsgemäß, wenn keine NaNs vorhanden sind, und identifiziert Floats, die == vergleichen bis zum Maximum. Die Ganzzahlversionen funktionieren für alle Eingaben und behandeln sie als vorzeichenbehaftete 2er-Ergänzung.)
Alle diese Funktionen arbeiten im 16/32/64-Bit-Modus mit demselben Maschinencode.
Eine Stack-Args-Aufrufkonvention würde es ermöglichen, die Args zweimal zu durchlaufen (max zu finden und dann zu vergleichen), was uns möglicherweise eine kleinere Implementierung geben würde, aber ich habe diesen Ansatz nicht ausprobiert.
x86 SIMD hat eine Vektor-> Ganzzahl-Bitmap als einzelnen Befehl ( pmovmskb
oder movmskps
oder pd), daher war dies natürlich, obwohl MMX / SSE-Befehle mindestens 3 Byte lang sind. SSSE3- und spätere Anweisungen sind länger als SSE2, und MMX / SSE1-Anweisungen sind die kürzesten. Verschiedene Versionen von pmax*
(gepacktes ganzzahliges vertikales Maximum) wurden zu unterschiedlichen Zeiten eingeführt, wobei SSE1 (für mmx-Regs) und SSE2 (für xmm-Regs) nur vorzeichenbehaftete Wörter (16 Bit) und vorzeichenlose Bytes enthielten.
( pshufw
und pmaxsw
auf MMX-Registern sind Katmai Pentium III neu, daher benötigen sie wirklich SSE1, nicht nur das MMX-CPU-Funktionsbit.)
Dies kann von C aus aufgerufen werden, wie unsigned max4_mmx(__m64)
beim i386 System V ABI, der ein __m64
Argument übergibt mm0
. (Nicht x86-64 System V, das geht __m64
in 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
Wenn es ein pmovmskw
gäbe, was hätte das packsswb
und das and
(3 + 2 Bytes) gespeichert . Wir brauchen nicht, and eax, 0x0f
weil pmovmskb
in einem MMX-Register die oberen Bytes bereits Nullen sind. MMX-Register sind nur 8 Byte breit, sodass 8-Bit-AL alle möglichen Nicht-Null-Bits abdeckt.
Wenn wir wüssten, dass unsere Eingaben nicht negativ sind, könnten wirpacksswb mm1, mm0
nicht negativ vorzeichenbehaftete Bytes in den oberen 4 Bytes von erzeugen mm1
, wodurch die Notwendigkeit von and
After vermieden wird pmovmskb
. Also 24 Bytes.
Das x86-Paket mit vorzeichenbehafteter Sättigung behandelt die Ein- und Ausgabe als vorzeichenbehaftet, sodass das Vorzeichenbit immer erhalten bleibt. ( https://www.felixcloutier.com/x86/packsswb:packssdw ). Unterhaltsame Tatsache: Das x86-Paket mit vorzeichenloser Sättigung behandelt die Eingabe weiterhin als signiert. Dies könnte der Grund sein, warum PACKUSDW
erst SSE4.1 eingeführt wurde, während die anderen 3 Kombinationen von Größe und Signatur seit MMX / SSE2 existierten.
Oder mit 32-Bit-Ganzzahlen in einem XMM-Register (und pshufd
anstelle von pshufw
) würde jeder Befehl ein weiteres Präfixbyte benötigen, außer zum movmskps
Ersetzen des Pakets / und. Aber pmaxsd
ich pmaxud
brauche ein zusätzliches Byte ...
aufrufbare von C alsunsigned max4_sse4(__m128i);
mit x86-64 System V oder MSVC vectorcall ( -Gv
), die beide passieren __m128i
/ __m128d
/ __m128
args in XMM regs ab 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
Wenn wir Eingaben als akzeptieren float
, können wir SSE1-Anweisungen verwenden. Das float
Format kann einen weiten Bereich von ganzzahligen Werten darstellen ...
Oder wenn Sie der Meinung sind, dass dies die Regeln zu weit biegt, beginnen Sie mit einem 3-Byte- 0F 5B C0 cvtdq2ps xmm0, xmm0
Konvertierungsprogramm, das eine 27-Byte-Funktion erstellt, die für alle Ganzzahlen funktioniert, die genau als IEEE-Binär32 dargestellt werden können float
, sowie für viele Kombinationen von Eingaben, bei denen einige der Eingaben erhalten werden gerundet auf ein Vielfaches von 2, 4, 8 oder was auch immer während der Konvertierung. (Es ist also 1 Byte kleiner als die SSE4.1-Version und funktioniert auf jedem x86-64 mit nur SSE2.)
Wenn einer der Float-Eingänge NaN ist, beachten Sie, dass maxps a,b
genau implementiert (a<b) ? a : b
wird und das Element aus dem 2. Operanden ungeordnet bleibt . Es kann also möglich sein, dass dies mit einer Bitmap ungleich Null zurückkehrt, selbst wenn die Eingabe etwas NaN enthält, je nachdem, wo sie sich befinden.
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
Kopieren und Mischen mit pshufd
ist immer noch unsere beste Wahl: shufps dst,src,imm8
Liest die Eingabe für die untere Hälfte von dst
von dst
. Und wir brauchen beide Male ein zerstörungsfreies Kopieren und Mischen, also sind 3-Byte movhlps
und unpckhps
/ pd beide aus. Wenn wir uns auf ein skalares Maximum beschränken würden, könnten wir diese verwenden, aber es kostet eine weitere Anweisung, vor dem Vergleich zu senden, wenn wir das Maximum nicht in allen Elementen bereits haben.
Verwandte: SSE4.1 phminposuw
kann die Position und den Wert des Minimums uint16_t
in einem XMM-Register finden. Ich denke nicht, dass es ein Gewinn ist, von 65535 zu subtrahieren, um es für max zu verwenden, aber siehe eine SO-Antwort über die Verwendung für max von Bytes oder vorzeichenbehafteten ganzen Zahlen.