x86机器代码(MMX / SSE1),26字节(4x int16_t)
x86机器码(SSE4.1),28字节(4x int32_t或uint32_t)
x86机器代码(SSE2),24字节(4x float32)或27B到cvt int32
(将int32转换为float的最后一个版本对于舍入到相同float的大整数并不是十分准确。使用float输入,舍入是调用者的问题,如果没有NaN,此函数将正常工作,以标识可比较==的float整数版本适用于所有输入,将其视为有符号2的补码。)
所有这些均以相同的机器代码在16/32/64位模式下工作。
使用stack-args调用约定可以使args循环两次(查找max然后进行比较),这可能为我们提供了一个较小的实现,但是我没有尝试过这种方法。
x86 SIMD将vector-> integer位图作为单个指令(pmovmskb
或movmskps
pd)使用,因此即使MMX / SSE指令的长度至少为3个字节也很自然。SSSE3和更高版本的指令比SSE2长,而MMX / SSE1指令最短。pmax*
在不同的时间引入了不同版本的(最大整数垂直压缩),其中SSE1(用于mmx regs)和SSE2(用于xmm regs)仅具有带符号字(16位)和无符号字节。
(pshufw
以及pmaxsw
MMX寄存器是Katmai Pentium III的新增功能,因此实际上它们需要SSE1,而不仅仅是MMX CPU功能位。)
unsigned max4_mmx(__m64)
与i386 System V ABI一样,它可以从C调用,后者在中传递__m64
arg mm0
。(不是x86-64 System V,它通过__m64
了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
如果有一个pmovmskw
,将保存packsswb
和和and
(3 + 2个字节)的内容。我们不需要,and eax, 0x0f
因为pmovmskb
在MMX寄存器上已经将高字节清零了。MMX寄存器只有8个字节宽,因此8位AL覆盖了所有可能的非零位。
如果我们知道输入是非负的,则可以packsswb mm1, mm0
在的高4个字节中生成非负的有符号字节,从而mm1
避免了需要and
after pmovmskb
。 因此为24个字节。
带符号饱和的x86 pack将输入和输出视为带符号,因此它始终保留符号位。(https://www.felixcloutier.com/x86/packsswb:packssdw)。有趣的事实:具有无符号饱和的x86 pack仍将输入视为有符号。这可能就是为什么PACKUSDW
直到SSE4.1才引入的原因,而自MMX / SSE2以来,存在尺寸和签名的其他3种组合。
或在XMM寄存器中使用32位整数(而pshufd
不是pshufw
),每条指令都需要再加上一个前缀字节,除了movmskps
替换pack / and。但是pmaxsd
/ pmaxud
需要一个额外的额外字节...
可调用从C作为unsigned max4_sse4(__m128i);
与x86-64的系统V,或MSVC vectorcall( -Gv
),这两者都通过__m128i
/ __m128d
/ __m128
在XMM暂存器ARGS开始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
或者,如果我们接受输入为float
,则可以使用SSE1指令。该float
格式可以表示各种整数值...
或者,如果您认为规则过分弯曲,请从3字节0F 5B C0 cvtdq2ps xmm0, xmm0
开始转换,然后制作一个27字节的函数,该函数适用于所有可完全表示为IEEE binary32的整数float
,以及许多输入组合,其中一些输入可以在转换过程中四舍五入为2、4、8或其他倍数。(因此,它比SSE4.1版本小1个字节,并且可以在仅具有SSE2的任何x86-64上运行。)
如果任何浮点输入为NaN,请注意maxps a,b
完全实现(a<b) ? a : b
,将第二个操作数中的元素保持为无序。因此,即使输入包含某些NaN(取决于它们的位置),也可能返回非零位图。
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
复制和随机播放with pshufd
仍然是我们最好的选择:shufps dst,src,imm8
读取dst
from中 下半部分的输入dst
。而且我们都需要非破坏性的复制和混洗,因此3字节movhlps
和unpckhps
/ pd都消失了。如果将范围缩小到标量最大值,则可以使用它们,但是如果我们还没有在所有元素中都具有最大值,则在比较之前要花费另一条指令进行广播。
相关信息:SSE4.1 phminposuw
可以uint16_t
在XMM寄存器中找到最小值的位置和值。我认为从65535中减去以将其用于最大数并不是一个胜利,但是看到关于将其用于最大字节数或带符号整数的SO答案。