问题
- MoveMask 的目的或意图是什么?
- 学习如何使用 x86/x86-64 程序集/SSE/AVX 的最佳地点是哪里?
- 我可以更高效地编写代码吗?
问题原因
我有一个用 F# 为 .NET 编写的函数,它使用 SSE2。我用 AVX2 写了同样的东西,但基本问题是一样的。 MoveMask
的预期用途是什么?我知道它适合我的目的,我想知道为什么。
我正在遍历两个 64 位 float 组,a
和 b
,测试它们的所有值是否匹配。我正在使用 CompareEqual
方法(我认为它包装了对 __m128d _mm_cmpeq_pd
的调用)一次比较多个值。然后我将该结果与 0.0
64 位 float 的 Vector128
进行比较。我的理由是,在值不匹配的情况下,CompareEqual
的结果将给出一个 0.0
值。到目前为止,这是有道理的。
然后我对与零向量的比较结果使用 Sse2.MoveMask
方法。我以前曾致力于使用 SSE
和 AVX
进行匹配,我看到人们使用 MoveMask
来测试非零值的示例值。我相信此方法使用的是 int _mm_movemask_epi8
Intel 内在函数。我包含了 F# 代码和 JIT 程序集。
这真的是 MoveMask
的意图,还是它用于这些目的只是一个巧合。我知道我的代码有效,我想知道为什么它有效。
F#代码
#nowarn "9" "51" "20" // Don't want warnings about pointers
open System
open FSharp.NativeInterop
open System.Runtime.Intrinsics.X86
open System.Runtime.Intrinsics
open System.Collections.Generic
let sseFloatEquals (a: array<float>) (b: array<float>) =
if a.Length = b.Length then
let mutable result = true
let mutable idx = 0
if a.Length > 3 then
let lastBlockIdx = a.Length - (a.Length % Vector128<float>.Count)
let aSpan = a.AsSpan ()
let bSpan = b.AsSpan ()
let aPointer = && (aSpan.GetPinnableReference ())
let bPointer = && (bSpan.GetPinnableReference ())
let zeroVector = Vector128.Create 0.0
while idx < lastBlockIdx && result do
let aVector = Sse2.LoadVector128 (NativePtr.add aPointer idx)
let bVector = Sse2.LoadVector128 (NativePtr.add bPointer idx)
let comparison = Sse2.CompareEqual (aVector, bVector)
let zeroTest = Sse2.CompareEqual (comparison, zeroVector)
// The line I want to understand
let matches = Sse2.MoveMask (zeroTest.AsByte ())
if matches <> 0 then
result <- false
idx <- idx + Vector128.Count
while idx < a.Length && idx < b.Length && result do
if a.[idx] <> b.[idx] then
result <- false
idx <- idx + 1
result
else
false
发出的程序集
; Core CLR 5.0.921.35908 on amd64
_.sseFloatEquals$cont@11(System.Double[], System.Double[], Microsoft.FSharp.Core.Unit)
L0000: push rdi
L0001: push rsi
L0002: push rbp
L0003: push rbx
L0004: sub rsp, 0x28
L0008: vzeroupper
L000b: mov eax, 1
L0010: xor r8d, r8d
L0013: mov r9d, [rcx+8]
L0017: cmp r9d, 3
L001b: jle short L008e
L001d: mov r10d, r9d
L0020: and r10d, 1
L0024: mov r11d, r9d
L0027: sub r11d, r10d
L002a: lea r10, [rcx+0x10]
L002e: mov esi, r9d
L0031: test rdx, rdx
L0034: jne short L003c
L0036: xor edi, edi
L0038: xor ebx, ebx
L003a: jmp short L0043
L003c: lea rdi, [rdx+0x10]
L0040: mov ebx, [rdx+8]
L0043: xor ebp, ebp
L0045: test esi, esi
L0047: je short L004c
L0049: mov rbp, r10
L004c: xor r10d, r10d
L004f: test ebx, ebx
L0051: je short L0056
L0053: mov r10, rdi
L0056: vxorps xmm0, xmm0, xmm0
L005a: cmp r8d, r11d
L005d: jge short L008e
L005f: mov esi, eax
L0061: test esi, esi
L0063: je short L008e
L0065: movsxd rsi, r8d
L0068: vmovupd xmm1, [rbp+rsi*8]
L006e: vmovupd xmm2, [r10+rsi*8]
L0074: vcmpeqpd xmm1, xmm1, xmm2
L0079: vcmpeqpd xmm1, xmm1, xmm0
L007e: vpmovmskb esi, xmm1
L0082: test esi, esi
L0084: je short L0088
L0086: xor eax, eax
L0088: add r8d, 4
L008c: jmp short L005a
L008e: cmp r9d, r8d
L0091: jle short L00c8
L0093: cmp [rdx+8], r8d
L0097: jle short L00c8
L0099: mov r10d, eax
L009c: test r10d, r10d
L009f: je short L00c8
L00a1: cmp r8d, r9d
L00a4: jae short L00d1
L00a6: movsxd r10, r8d
L00a9: vmovsd xmm0, [rcx+r10*8+0x10]
L00b0: cmp r8d, [rdx+8]
L00b4: jae short L00d1
L00b6: vucomisd xmm0, [rdx+r10*8+0x10]
L00bd: jp short L00c1
L00bf: je short L00c3
L00c1: xor eax, eax
L00c3: inc r8d
L00c6: jmp short L008e
L00c8: add rsp, 0x28
L00cc: pop rbx
L00cd: pop rbp
L00ce: pop rsi
L00cf: pop rdi
L00d0: ret
L00d1: call 0x00007ffcef38a370
L00d6: int3
_.sseFloatEquals(System.Double[], System.Double[])
L0000: mov r8d, [rcx+8]
L0004: cmp r8d, [rdx+8]
L0008: jne short L0012
L000a: xor r8d, r8d
L000d: jmp 0x00007ffc99000480
L0012: xor eax, eax
L0014: ret
最佳答案
MoveMask
只是将每个元素的高位提取到整数位图中。您有 3 个元素大小选项:movmskpd
(64 位),movmskps
(32 位)和 pmovmskb
(8 位)。
这适用于 SIMD 比较,当谓词为假时产生的输出全为零,谓词为真时元素中的位为全 1。如果解释为 IEEE-FP 浮点值,全一是 -QNaN
的位模式,但通常您不要那样做。取而代之的是 movemask,或 AND,(或 AND/ANDN/OR 或 _mm_blend_pd
)或带有比较结果的类似东西。
movemask(v) != 0
、movemask(v) == 0x3
或 movemask(v) == 0
是这样的您检查条件,例如比较中的至少一个元素分别匹配、全部匹配或不匹配,其中 v
是 _mm_cmpeq_pd
或其他任何结果。 (或者只是直接提取符号而不进行比较)。
对于其他元素大小,0xf
或 0xffff
以匹配全部四位或全部 16 位。或者对于 AVX 256 位向量,两倍的位数,直到用 vpmovmskb eax, ymm0
填充整个 32 位整数。
你在做什么真的很奇怪,使用 0.0/NaN 比较结果作为另一个比较的输入 vcmpeqpd xmm1, xmm1, xmm2
/vcmpeqpd xmm1, xmm1, xmm0
。对于第二次比较,只有 == 0.0
(即 +-0.0)的元素才为真,因为 x == NaN
对于每个 都是假的x
.
如果第二个向量是常量零(let zeroTest = Sse2.CompareEqual (comparison, zeroVector)
,那是没有意义的,您只是反转比较结果,您可以通过检查不同的整数条件或针对不同的常量,不进行运行时比较。(0.0 == 0.0
为真,产生全一输出,0.0 == -NaN
为假,产生全零输出。)
要了解有关内部函数和 SIMD 的更多信息,请参阅示例 Agner Fog's optimization guide ;他的 asm 指南有一章是关于 SIMD 的。此外,他的 C++ VectorClass 库有一些有用的包装器,出于学习目的,了解这些包装器函数如何实现一些基本功能可能很有用。
要了解实际做的事情,请参阅Intel's intrinsics guide .您可以通过 asm 指令或 C++ 内部名称进行搜索。
我认为 MS 有他们的 C# System.Runtime.Intrinsics.X86 的文档,我假设 F# 使用相同的内在函数,但我自己不使用任何一种语言。
相关回复:比较:
Get the last line separator - pcmpeqb -> pmovmskb ->
bsr
查找比较结果向量中最后一个匹配元素的位置。比较掩码上的位扫描反转。通常,您希望向前扫描以找到第一个匹配项(或反转并找到第一个不匹配项,例如memcmp
)。例如Compare 16 byte strings with SSE
或者,如果您通过匹配广播字符的循环不变向量来计算出现次数,则对它们进行 popcount:How can I count the occurrence of a byte in array using SIMD? - 而不是 movemask,使用比较结果作为整数 0/-1。 SIMD 在内部循环中从向量累加器中减去,然后在外部循环中对整数元素进行水平求和。SIMD instructions for floating point equality comparison (with NaN == NaN) - 有助于理解 NaN 工作原理的练习。
关于.net-core - SSE 和 AVX 的 MoveMask 的目的是什么,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69878534/