对于memcmp,SSE4.2字符串指令比SSE2快多少?

时间:2017-10-16 04:07:53

标签: assembly x86 sse micro-optimization sse4

这是我的代码汇编程序

你能用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;

Semple 32bit https://vk.com/doc297044195_451679410

1 个答案:

答案 0 :(得分:11)

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

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

您正在询问SSE2 pcmpeqwpcmpistrm的效果,对吗? (显式长度的SSE4.2指令,如pcmpestrm have worse throughput than the implicit-length versions,所以当你不接近字符串的末尾时,请在主循环中使用隐式长度版本。请参阅Agner Fog's instruction tables和微架构指南)。

对于memcmp(或经过精心实施的strcmp),使用SSE4.2可以做到的最好比在大多数CPU上使用SSE2(或SSSE3)做得最好。也许对于很短的字符串很有用,但对于memcmp的主循环却没有用。

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

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

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

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

在AMD Ryzen上,pcmpistri也是2c吞吐量,所以它就是废话。 (2个加载端口和每个时钟5个uop前端吞吐量(如果有任何(或全部?)6个uop来自多uop指令)意味着你可以更快。

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

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

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

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

对于具有8位字符串的strcmp,在大多数CPU上,不使用SSE4.2字符串指令会更快。有关某些基准测试(隐式长度字符串版本),请参阅strchr.com文章的评论。例如,glibc没有使用memcmp的SSE4.2字符串指令,因为它们在大多数CPU上都不会更快。尽管如此,它们可能是strcmp的胜利。

glibc有几个SSE2 / SSSE3 asm strstrmemcmp implementations。 (它的LGPLed,所以你不能只将它复制到非GPL项目中,但要看看它们的作用。)一些字符串函数(如strlen)仅按每64字节分支,并且然后回来找出缓存行中的哪个字节有命中。但他们的memcmp实现只是用movdqu / strcmp展开。您可以使用pcmpeqb,因为您想知道第一个16位元素的位置,而不是第一个字节。

您的SSE2实施可能更快。您应该使用带有movdqa的索引寻址模式,因为它不会与pcmpeqw微融合(在Intel Sandybridge / Ivybridge上;在Nehalem或Haswell +上很好),但是pcmpeqw将保持微融合而不会发生分层。 / p>

您应该展开几次以减少循环开销。您应该将指针增量与循环计数器结合使用,以便pcmpeqw xmm0, [eax]代替cmp/jb:在更多CPU上进行宏融合,并避免编写寄存器(减少寄存器重命名所需的物理寄存器数量) )。

您在Intel Sandybridge / Ivybridge上的内循环将会运行

sub/ja

这是7个融合域uops,因此它只能在主流Intel CPU的每次迭代中以最佳7/4周期从前端发出。这远远不是每个时钟2个负载的瓶颈。在Haswell以及之后,每次迭代它的周期为6/4周期,因为索引寻址模式可以与2操作数加载修改指令(如@@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) )保持微融合,但不能保留其他任何内容(如{{1} (不读取目的地)或AVX pcmpeqw(3个操作数))。请参阅Micro fusion and addressing modes

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

在指针设置代码中,如果先检查NULL指针,则可以保存pabsw xmm0, [eax+edx]。您可以vpcmpeqw xmm0, xmm0, [eax+edx] / cmp减去检查两者是否与相同的宏融合比较和分支相等。 (它只会在英特尔Sandybridge系列上进行宏观融合,只有Haswell可以在一个解码模块中进行2次宏观融合。但是Haswell / Broadwell / Skylake CPU很常见并且变得越来越常见,这对其他产品没有任何不利影响。 CPU除非等指针非常常见,否则首先进行检查很重要。)

在返回路径中:尽可能使用xor eax,eax to zero a register,而不是sub

你似乎不会避免从字符串末尾读取。您应该使用最终位于页面末尾的字符串来测试您的函数,其中下一页是未映射的。

对于早期的标量测试,

jne优于mov eax, 0xor ebx, [eax + edx]可以与jcc进行宏观融合,但cmp可以&#t}。

您加载一个掩码来处理清理,以覆盖您读取字符串末尾的情况。你可能仍然可以使用通常的cmp/jnz来找到位图中的第一个区别。我想用xor反转它以找到没有比较相等的第一个位置,并检查它是否小于剩余的字符串长度。

或者您可以使用bsfnot动态生成蒙版。或者,为了加载它,您有时可以使用滑动窗口进入mov eax, -1数组,但是您需要子字节偏移以便不起作用。 (它适用于矢量蒙版,如果你想掩盖并重做shrVectorizing with unaligned buffers: using VMASKMOVPS: generating a mask from a misalignment count? Or not using that insn at all)。

你的方式并不坏,只要它没有缓存未命中。我可能会动态生成面具。也许之前在另一个寄存器中的循环,因为你可以掩码得到...,0,0,0,-1,-1,-1,...,所以掩码生成可以与循环并行发生。