2

这是我的代码的汇编程序

你能把它嵌入c ++并检查SSE4吗?在速度

我很想看看如何步入SSE4的发展。还是根本不担心他?让我们检查一下(SSSE3以上我没有支持)

{ sse2 strcmp WideChar 32 bit }
function CmpSee2(const P1, P2: Pointer; len: Integer): Boolean;
asm
    push ebx           // Create ebx
    cmp EAX, EDX      // Str = Str2
    je @@true        // to exit true
    test eax, eax   // not Str
    je @@false     // to exit false
    test edx, edx // not Str2
    je @@false   // to exit false
    sub edx, eax              // Str2 := Str2 - Str;
    mov ebx, [eax]           // get Str 4 byte
    xor ebx, [eax + edx]    // Cmp Str2 4 byte
    jnz @@false            // Str <> Str2 to exit false
    sub ecx, 2            // dec 4
    { AnsiChar  : sub ecx, 4 }
    jbe @@true           // ecx <= 0 to exit true
    lea eax, [eax + 4]  // Next 4 byte
    @@To1:
    movdqa xmm0, DQWORD PTR [eax]       // Load Str 16 byte
    pcmpeqw xmm0, DQWORD PTR [eax+edx] // Load Str2 16 byte and cmp
    pmovmskb ebx, xmm0                // Mask cmp
    cmp ebx, 65535                   // Cmp mask
    jne @@Final                     // ebx <> 65535 to goto final
    add eax, 16                    // Next 16 byte
    sub ecx, 8                    // Skip 8 byte (16 wide)
    { AnsiChar  : sub ecx, 16 }
    ja @@To1                     // ecx > 0
    @@true:                       // Result true
    mov eax, 1                 // Set true
    pop ebx                   // Remove ebx
    ret                      // Return
    @@false:                  // Result false
    mov eax, 0             // Set false
    pop ebx               // Remove ebx
    ret                  // Return
    @@Final:
    cmp ecx, 7         // (ebx <> 65535) and (ecx > 7)
    { AnsiChar : cmp ecx, 15 }
    jae @@false       // to exit false
    movzx ecx, word ptr @@mask[ecx * 2 - 2] // ecx = mask[ecx]
    and ebx, ecx                           // ebx = ebx & ecx
    cmp ebx, ecx                          // ebx = ecx
    sete al                              // Equal / Set if Zero
    pop ebx                             // Remove ebx
    ret                                // Return
    @@mask: // array Mersenne numbers
    dw $000F, $003F, $00FF, $03FF, $0FFF, $3FFF
    { AnsiChar
    dw 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383
    }
end;

示例 32 位 https://vk.com/doc297044195_451679410

4

1 回答 1

13

你调用了你的函数strcmp,但你实际实现的是一个对齐要求memcmp(const void *a, const void *b, size_t words)。如果指针不是 16B 对齐的,两者都会movdqa出错。pcmpeqw xmm0, [mem](实际上,如果a+4不是 16B 对齐,因为您执行前 4 个标量并增加 4 个字节。)

使用正确的启动代码 和movdqu,您可以处理任意对齐(达到要用作内存操作数的指针的对齐边界pcmpeqw)。为方便起见,您可以要求两个指针从宽字符对齐开始,但您不需要(特别是因为您只是返回真/假,而不是negative / 0 / positive作为排序顺序。)


您是在询问 SSE2pcmpeqw与的性能pcmpistrm,对吗?(显式长度 SSE4.2 指令pcmpestrm的吞吐量比隐式长度版本差,因此当您未接近字符串末尾时,请在主循环中使用隐式长度版本。请参阅Agner Fog 的指令表和微架构指南)。

对于 memcmp(或精心实现的 strcmp),在大多数 CPU 上,您可以使用 SSE4.2 做的最好的事情比使用 SSE2(或 SSSE3)做的最好的事情要慢。可能对非常短的字符串有用,但不适用于 memcmp 的主循环。

在 Nehalem 上:pcmpistri是 4 uop,2c 吞吐量(带有内存操作数),因此没有其他循环开销,它可以跟上内存。(Nehalem 只有 1 个装载端口)。 pcmpestri吞吐量为 6c:慢 3 倍。

在通过 Skylake 的 Sandybridge 上,pcmpistri xmm0, [eax]吞吐量为 3c,因此速度太慢了 3 倍,无法跟上每个时钟 1 个向量(2 个加载端口)。 pcmpestri其中大多数的吞吐量为 4c,因此并没有那么糟糕。(可能对最后一个部分向量有用,但在主循环中没有)。

在 Silvermont/KNL 上,pcmpistrm它是最快的,每 14 个周期运行一个吞吐量,所以对于简单的东西来说,它完全是垃圾。

在 AMD Jaguar 上,pcmpistri吞吐量为 2c,因此它实际上可能可用(只有一个加载端口)。 pcmpestri是 5c 吞吐量,所以很烂。

在 AMD Ryzen 上, pcmpistri也是 2c 吞吐量,所以那里很垃圾。(2 个加载端口和每时钟 5 微指令的前端吞吐量(或 6 微指令,如果有的话(或全部?)来自多指令指令)意味着您可以走得更快。

在 AMD Bulldozer 系列上,pcmpistri吞吐量为 3c,直到 Steamroller 为 5c。 pcmpestri有 10c 的吞吐量。它们被微编码为 7 或 27 m-ops,因此 AMD 并没有在它们上花费大量的芯片。

在大多数 CPU 上,只有在您充分利用它们来处理您无法仅使用pcmpeq/pmovmskb的东西时,它们才值得。但是,如果您可以使用 AVX2 或特别是 AVX512BW,即使在更宽的向量上使用更多指令,即使做复杂的事情也可能会更快。(没有更广泛的 SSE4.2 字符串指令版本。)也许 SSE4.2 字符串指令对于通常处理短字符串的函数仍然有用,因为宽向量循环通常需要更多的启动/清理开销。此外,在一个不会在 SIMD 循环中花费太多时间的程序中,在一个小函数中使用 AVX 或 AVX512 仍会在下一毫秒左右降低您的最大涡轮时钟速度,并且很容易造成净损失。


一个好的内部循环应该是负载吞吐量的瓶颈,或者尽可能接近。 movqdu/// macro pcmpeqw [one-register]- pmovmskbfused-cmp+jcc 只有 4 个 fused-domain uops,所以这在 Sandybridge 系列 CPU 上几乎可以实现


有关实现和一些基准,请参见https://www.strchr.com/strcmp_and_strlen_using_sse_4.2,但这是针对必须检查0字节的 C 样式隐式长度字符串。看起来您正在使用显式长度字符串,因此在检查长度是否相等后,它只是memcmp. (或者我想如果你需要找到排序顺序而不是相等/不相等,你必须把 memcmp 放到较短字符串的末尾。)

对于带有 8 位字符串的 strcmp,在大多数 CPU 上,不使用 SSE4.2 字符串指令会更快。请参阅 strchr.com 文章的评论以获取一些基准(该隐式长度字符串版本)。例如 glibc 不使用 SSE4.2 字符串指令strcmp,因为它们在大多数 CPU 上并不快。不过,他们可能是一场胜利strstr


glibc 有几个 SSE2/SSSE3 asmstrcmpmemcmpimplementations。(它是 LGPL 的,所以你不能只是将它复制到非 GPL 项目中,而是看看它们做了什么。)一些字符串函数(如 strlen)仅每 64 个字节分支,然后再回来整理缓存行中的哪个字节被命中。但是他们的 memcmp 实现只是使用 movdqu / 展开pcmpeqb。您可以使用pcmpeqw,因为您想知道不同的第一个 16 位元素的位置,而不是第一个字节。


您的 SSE2 实施可能会更快。您应该将索引寻址模式与 movdqa 一起使用,因为它不会与 pcmpeqw 进行微融合(在 Intel Sandybridge/Ivybridge 上;在 Nehalem 或 Haswell+ 上很好),但pcmpeqw xmm0, [eax]会保持微融合而不会解压。

您应该展开几次以减少循环开销。您应该将指针增量与循环计数器结合起来,这样您就cmp/jb不会sub/ja:在更多 CPU 上进行宏融合,并避免写入寄存器(减少寄存器重命名所需的物理寄存器数量)。

您在英特尔 Sandybridge/Ivybridge 上的内部循环将运行

@@To1:
movdqa xmm0, DQWORD PTR [eax]       // 1 uop
pcmpeqw xmm0, DQWORD PTR [eax+edx] // 2 uops on Intel SnB/IvB, 1 on Nehalem and earlier or Haswell and later.
pmovmskb ebx, xmm0                // 1 uop
cmp ebx, 65535
jne @@Final                     // 1 uop  (macro-fused with cmp)
add eax, 16                    // 1 uop
sub ecx, 8
{ AnsiChar  : sub ecx, 16 }
ja @@To1                     // 1 uop (macro-fused with sub on SnB and later, otherwise 2)

这是 7 个融合域微指令,因此在主流 Intel CPU 上,每次迭代最多只能从前端发出 7/4 个周期。这与每个时钟 2 个负载的瓶颈相去甚远。在 Haswell 及更高版本上,每次迭代需要 6/4 个周期,因为索引寻址模式可以与 2 操作数加载修改指令保持微融合pcmpeqw,但不能与其他任何指令(如pabsw xmm0, [eax+edx](不读取目标)或 AVX vpcmpeqw xmm0, xmm0, [eax+edx](3 个操作数))。请参阅微融合和寻址模式


这对于具有更好设置/清理的小字符串也可能更有效。

在您的指针设置代码中,cmp如果您首先检查 NULL 指针,则可以保存 a 。您可以sub/使用相同的宏融合比较和分支jne来减去检查两者是否相等。(它只会在 Intel Sandybridge 系列上进行宏融合,并且只有 Haswell 可以在单个解码块中进行 2 个宏融合。但是 Haswell/Broadwell/Skylake CPU 很常见并且变得越来越普遍,这对于其他CPU,除非等指针非常普遍,以至于首先进行检查很重要。)


在您的返回路径中:始终尽可能使用xor eax,eax将寄存器归零mov eax, 0,而不是.

您似乎没有避免从字符串末尾读取。您应该使用在页面末尾结束的字符串来测试您的函数,在该页面未映射下一页。

xor ebx, [eax + edx]与早期标量测试相比具有零优势cmpcmp/jnz可以与 jcc 进行宏融合,但xor不能。


您加载一个掩码来处理清理,以覆盖您在字符串末尾阅读的情况。您可能仍然可以使用通常bsf的方法来查找位图中的第一个差异。我猜想将它反转not以找到第一个不相等的位置,并检查它是否小于剩余的字符串长度。

或者,我认为您可以使用mov eax, -1and即时生成蒙版。shr或者为了加载它,您有时可以在...,0,0,0,-1,-1,-1,...数组中使用滑动窗口,但您需要子字节偏移量,因此这不起作用。(它适用于矢量掩码,如果您想掩码并重做。使用未对齐的pmovmskb缓冲区 进行矢量化:使用 VMASKMOVPS:从未对齐计数生成掩码?或者根本不使用该 insn)。

只要不缓存未命中,您的方式还不错。我可能会去动态生成面具。也许在另一个寄存器中的循环之前,因为您可以屏蔽 get count % 8,所以屏蔽生成可以与循环并行发生。

于 2017-10-16T05:10:22.590 回答