现代(流水线/超标量)处理器上x86 rep指令的性能

22

最近我在写x86汇编语言(只是为了好玩),我想知道rep前缀的字符串指令在现代处理器上是否确实具有性能优势,还是只是为了向后兼容而实现的。

我可以理解英特尔最初为处理器一次只运行一个指令而实现rep指令的原因,但现在使用它们是否有益呢?

对于编译成更多指令的循环,需要填充更多的流水线和/或按顺序发出。现代处理器是否构建为针对这些带有rep前缀的指令进行优化,或者rep指令在现代代码中使用得如此之少,以至于对制造商来说不重要呢?


我已经有五年没有研究过这个了,但是当时我的个人经验是,至少rep movsd和rep stosd比简单的循环要快,而一些扫描变体则不是。不过这可能已经发生了显著的变化。 - 500 - Internal Server Error
进行一项测试,亲自体验不同的处理器。 - Alexey Frunze
谢谢大家的意见。Alex:我可能最终会这样做,但我没有很多不同的处理器来尝试它,所以它只会在真实的处理器上运行,而不是在没有流水线的模拟器上运行。此外,我很懒,如果其他人可能已经完成了这项工作,我宁愿不去做。 :) - RyanS
相关内容:有关x86内存带宽的详细信息,NT存储与常规存储,以及单个核心无法始终饱和内存带宽的问题(请参见答案中的“延迟受限平台”)。还有一些关于rep movs / stos与向量循环的比较。 - Peter Cordes
3个回答

41
在AMD和Intel的优化指南中,给了很多关于此类问题的空间。该领域所提供的建议的有效性具有“半衰期” - 不同的CPU代数表现不同,例如:

英特尔架构优化手册中列出了各种块复制技术(包括rep stosd)的性能比较数据,适用于不同的CPU,以及最快的技术在一个CPU上可能不是在另一个CPU上表现最佳。

对于许多情况,最新的x86 CPU(具有“字符串”SSE4.2操作)可以通过SIMD单元执行字符串操作,参见此调查

要跟进所有这些(和/或在事情再次发生变化时保持更新),请阅读Agner Fog的优化指南/博客


1
“rep movs”和“rep stos”通常很好(适用于中到大型对齐缓冲区),而“repe / repne scas / cmps”通常不太好。 - Peter Cordes
回复:SSE4.2:它们可能对strstr或其他情况有用,其中您可以利用更多的完整功能,但通常不适用于strcmpstrchr,因为它们比pcmpeqb慢。[它们对于memcmp或显式长度字符串尤其糟糕](https://stackoverflow.com/questions/46762813/how-much-faster-are-sse4-2-string-instructions-than-sse2-for-memcmp/46763316#46763316)。 - Peter Cordes

10

除了FrankH的优秀回答之外,我想指出最佳方法也取决于字符串的长度、对齐方式以及长度是固定还是可变的。

对于小字符串(可能最多16字节),使用简单指令手动操作可能更快,因为它避免了更复杂技术的设置成本(对于固定大小的字符串可以轻松展开)。对于中等大小的字符串(可能从16字节到4 KiB),像“REP MOVSD”(如果可能存在不对齐,则添加一些“MOVSB”指令)等内容可能是最好的选择。

对于大于此大小的任何内容,有些人可能会尝试使用SSE / AVX和预取等技术。更好的方法是修复调用方,使得首先不需要复制(或strlen()或其他操作)。如果你足够努力,你几乎总能找到一种方法。注意:还要非常小心“所谓”的快速mempcy()例程——通常它们已在大字符串上进行了测试,并未在更可能的微小/小/中等字符串上进行测试。

还要注意,由于所有这些差异(可能的长度、对齐方式、固定或可变大小、CPU类型等),为所有非常不同的情况都拥有一个多用途的“memcpy()”的想法是目光短浅的(仅限于优化而非方便)。


2
确认。优化指南(包括英特尔/AMD以及Agner Fog的材料和许多其他材料)也提到了这些内容;在许多情况下,有以下策略:1.对于短字符串,使用内联原始指令;2.对于中等大小的字符串,使用大操作数大小的rep movs;3.对于已知的大块,使用SIMD单元。并且一定要在_您的_数据上进行测试,因为如果大多数字符串<8字节,则“超快VVX”性能将崩溃。 - FrankH.
据我所知,在现代硬件上,REP MOVSD 往往比 REP MOVSB 慢得多。这可能是因为现代 CPU 仅针对 REP MOVSB 进行了特殊优化,因为它比 REP MOVSD 更常用。 - Paul Groke
@PaulGroke:也许有一些CPU在使用rep movsb时比rep movsd更好,但大多数都实现了所有ERMSB魔法来支持rep movsd/movsq。而在IvyBridge增强Rep MovSB功能之前,rep movsb在英特尔CPU上通常是更差的。请参见Enhanced REP MOVSB for memcpy,其中有一个优秀的答案,详细介绍了x86内存带宽。 - Peter Cordes

0

由于没有人给你任何数字,我将提供一些数据,这些数据是我通过对我的垃圾收集器进行基准测试得到的,该收集器非常依赖memcpy。我要复制的对象有60%的长度为16字节,其余30%为500-8000字节左右。

  • 前提条件:dst,src和n都是8的倍数。
  • 处理器:AMD Phenom(tm)II X6 1090T处理器64位/ Linux

以下是我的三个memcpy变体:

手写while-loop:

if (n == 16) {
    *dst++ = *src++;
    *dst++ = *src++;
} else {
    size_t n_ptrs = n / sizeof(ptr);
    ptr *end = dst + n_ptrs;
    while (dst < end) {
        *dst++ = *src++;
    }
}

(ptruintptr_t的别名)。时间:101.16%

rep movsb

if (n == 16) {
    *dst++ = *src++;
    *dst++ = *src++;
} else {
    asm volatile("cld\n\t"
                 "rep ; movsb"
                 : "=D" (dst), "=S" (src)
                 : "c" (n), "D" (dst), "S" (src)
                 : "memory");
}

时间:103.22%

rep movsq

(重复移动双字节)
if (n == 16) {
    *dst++ = *src++;
    *dst++ = *src++;
} else {
    size_t n_ptrs = n / sizeof(ptr);
    asm volatile("cld\n\t"
                 "rep ; movsq"
                 : "=D" (dst), "=S" (src)
                 : "c" (n_ptrs), "D" (dst), "S" (src)
                 : "memory");
}

时间:100.00% < p > req movsq 以微小的优势获胜。

2
REP MOVS 也会改变 RCX 寄存器。 - Ross Ridge
我们如何修复上面的代码以声明对CX的更改?(声明将其设置为0?) - Cecil Ward
@CecilWard: 增强的 REP MOVSB 用于 memcpy 已经为 rsp movsb 保存了内联汇编。另一个选项是将 "+c"(n) 作为输入/输出操作数使用。如果您以后从未读取过该 C 变量,则编译器将有效地知道输入寄存器已被破坏。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接