最近我在写x86汇编语言(只是为了好玩),我想知道rep前缀的字符串指令在现代处理器上是否确实具有性能优势,还是只是为了向后兼容而实现的。
我可以理解英特尔最初为处理器一次只运行一个指令而实现rep指令的原因,但现在使用它们是否有益呢?
对于编译成更多指令的循环,需要填充更多的流水线和/或按顺序发出。现代处理器是否构建为针对这些带有rep前缀的指令进行优化,或者rep指令在现代代码中使用得如此之少,以至于对制造商来说不重要呢?
最近我在写x86汇编语言(只是为了好玩),我想知道rep前缀的字符串指令在现代处理器上是否确实具有性能优势,还是只是为了向后兼容而实现的。
我可以理解英特尔最初为处理器一次只运行一个指令而实现rep指令的原因,但现在使用它们是否有益呢?
对于编译成更多指令的循环,需要填充更多的流水线和/或按顺序发出。现代处理器是否构建为针对这些带有rep前缀的指令进行优化,或者rep指令在现代代码中使用得如此之少,以至于对制造商来说不重要呢?
英特尔架构优化手册中列出了各种块复制技术(包括rep stosd
)的性能比较数据,适用于不同的CPU,以及最快的技术在一个CPU上可能不是在另一个CPU上表现最佳。
对于许多情况,最新的x86 CPU(具有“字符串”SSE4.2操作)可以通过SIMD单元执行字符串操作,参见此调查。
要跟进所有这些(和/或在事情再次发生变化时保持更新),请阅读Agner Fog的优化指南/博客。
strstr
或其他情况有用,其中您可以利用更多的完整功能,但通常不适用于strcmp
或strchr
,因为它们比pcmpeqb
慢。[它们对于memcmp
或显式长度字符串尤其糟糕](https://stackoverflow.com/questions/46762813/how-much-faster-are-sse4-2-string-instructions-than-sse2-for-memcmp/46763316#46763316)。 - Peter Cordes除了FrankH的优秀回答之外,我想指出最佳方法也取决于字符串的长度、对齐方式以及长度是固定还是可变的。
对于小字符串(可能最多16字节),使用简单指令手动操作可能更快,因为它避免了更复杂技术的设置成本(对于固定大小的字符串可以轻松展开)。对于中等大小的字符串(可能从16字节到4 KiB),像“REP MOVSD”(如果可能存在不对齐,则添加一些“MOVSB”指令)等内容可能是最好的选择。
对于大于此大小的任何内容,有些人可能会尝试使用SSE / AVX和预取等技术。更好的方法是修复调用方,使得首先不需要复制(或strlen()或其他操作)。如果你足够努力,你几乎总能找到一种方法。注意:还要非常小心“所谓”的快速mempcy()例程——通常它们已在大字符串上进行了测试,并未在更可能的微小/小/中等字符串上进行测试。
还要注意,由于所有这些差异(可能的长度、对齐方式、固定或可变大小、CPU类型等),为所有非常不同的情况都拥有一个多用途的“memcpy()”的想法是目光短浅的(仅限于优化而非方便)。
rep movs
;3.对于已知的大块,使用SIMD单元。并且一定要在_您的_数据上进行测试,因为如果大多数字符串<8字节,则“超快VVX”性能将崩溃。 - FrankH.REP MOVSD
往往比 REP MOVSB
慢得多。这可能是因为现代 CPU 仅针对 REP MOVSB
进行了特殊优化,因为它比 REP MOVSD
更常用。 - Paul Grokerep movsb
时比rep movsd
更好,但大多数都实现了所有ERMSB魔法来支持rep movsd
/movsq
。而在IvyBridge增强Rep MovSB功能之前,rep movsb
在英特尔CPU上通常是更差的。请参见Enhanced REP MOVSB for memcpy,其中有一个优秀的答案,详细介绍了x86内存带宽。 - Peter Cordes由于没有人给你任何数字,我将提供一些数据,这些数据是我通过对我的垃圾收集器进行基准测试得到的,该收集器非常依赖memcpy。我要复制的对象有60%的长度为16字节,其余30%为500-8000字节左右。
以下是我的三个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++;
}
}
(ptr
是uintptr_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");
}
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");
}
req movsq
以微小的优势获胜。
rsp movsb
保存了内联汇编。另一个选项是将 "+c"(n)
作为输入/输出操作数使用。如果您以后从未读取过该 C 变量,则编译器将有效地知道输入寄存器已被破坏。 - Peter Cordes
rep movs
/stos
与向量循环的比较。 - Peter Cordes