SSE 和 AVX 的 MoveMask 的目的是什么

What is the purpose of the MoveMask for SSE and AVX

问题

  1. MoveMask 的目的或意图是什么?
  2. 学习如何使用 x86/x86-64 assembly/SSE/AVX 的最佳位置是什么?
  3. 我可以更有效地编写代码吗?

问题原因

我有一个用 F# 为 .NET 编写的函数,它使用 SSE2。我用 AVX2 写了同样的东西,但基本问题是一样的。 MoveMask 的预期目的是什么?我知道它对我有用,我想知道为什么。

我正在遍历两个 64 位浮点数组 ab,测试它们的所有值是否匹配。我正在使用 CompareEqual 方法(我相信这是对 __m128d _mm_cmpeq_pd 的调用)一次比较多个值。然后我将该结果与 0.0Vector128 64 位浮点数进行比较。我的推理是,在值不匹配的情况下,CompareEqual 的结果将给出 0.0 值。到此为止,它是有道理的。

然后我对与零向量比较的结果使用Sse2.MoveMask方法。我以前曾研究过使用 SSEAVX 进行匹配,我看到有人使用 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-bit), movmskps (32-bit), and pmovmskb(8 位)。

这适用于 SIMD 比较,它产生的输出在谓词为假时为全零,在谓词为真的元素中为全一。如果解释为 IEEE-FP 浮点值,全一是 -QNaN 的位模式,但通常您 不会 那样做。取而代之的是 movemask,或 AND,(或 AND / ANDN / OR 或 _mm_blend_pd)或带有比较结果的类似东西。


movemask(v) != 0movemask(v) == 0x3movemask(v) == 0 是您检查条件的方式,例如比较中至少有一个元素匹配,或全部匹配,或 none 匹配,分别,其中 v_mm_cmpeq_pd 或其他任何结果。 (或者只是直接提取符号而不进行比较)。

对于其他元素大小,0xf0xffff 以匹配全部四位或全部 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 为真,产生全 1 输出,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# 使用相同的内在函数,但我自己都不使用任何一种语言。


相关回复:比较:

  • - pcmpeqb -> pmovmskb -> bsr to find the position of the last match element in a vector of compare results. Bit-scan reverse on the compare mask. Often you want to scan forward to find the first match (or invert and find first mismatch, like for memcmp). e.g.
    或者,如果您通过匹配广播字符的循环不变向量来计算出现次数,则对它们进行 popcount: - 而不是 movemask,使用比较结果作为整数 0 / -1。 SIMD 在内循环中从向量累加器中减去,然后在外循环中对整数元素进行水平求和。

  • SIMD instructions for floating point equality comparison (with NaN == NaN) - 有助于理解 NaN 工作原理的练习。

除了彼得已经指出的内容。

是的,MoveMask (movmskp) 可以很好地进行比较,如果您需要索引作为通用整数 bask,可以像 bsfpopcnt 或随便。

当你只确定非零的事实时,编译为 ptestSse41.TestZ(或 AVX 的 Avx.TestZ)可能会更好,因为它使结果成为标志直接,无需填充通用寄存器。