为何使用rsi/rdi的strcpy实现耗时更长?x86-64字符串处理咨询
你遇到的这个问题很有意思——明明用了“标准”的字符串寄存器,结果性能反而不如手动操作指针的实现,核心原因出在你的RSI/RDI版本代码存在逻辑冗余和不必要的内存访问,再加上现代CPU对简单指令序列的优化反而比传统字符串指令更友好。
你的RSI/RDI实现的性能瓶颈分析
先仔细拆解你的第一个实现:
custom_strcpy proc mov rsi, rdx mov rdi, rcx mov rax, rdi _loop: movsb ; 复制[rsi]到[rdi],自动inc rsi/rdi mov r8d, [rsi] ; 读取RSI当前指向的4字节(注意是下一个字节开始) cmp r8d, 0 jne _loop _end: mov byte ptr[rdi], 0 ; 手动加终止0 ret custom_strcpy endp
这里有几个拖慢性能的关键问题:
- 不必要的4字节读取:
mov r8d, [rsi]读取了4字节,但你只需要判断其中最低1字节是否为0。这种宽内存访问可能会读取字符串末尾之外的未初始化内存,不仅可能触发缓存行的额外加载,还会因为不对齐访问(如果RSI此时未对齐到4字节边界)带来性能惩罚。 - 冗余的终止操作:你的循环只复制了字符串的非0字节,最后还要手动写一个0到目标地址。而第二个实现的循环直接把终止0也复制了,少了一条额外的指令。
- 指令依赖与流水线阻塞:
movsb刚修改了RSI,紧接着就读取[rsi],这种数据依赖会让CPU流水线无法并行执行后续指令,降低了执行效率。 - 字符串指令的现代开销:
movsb这类传统字符串指令,在早期CPU上是专门优化的,但现代CPU对简单的mov + inc + cmp序列的优化更好——这些指令解码更快,流水线冲突更少,尤其在处理短字符串(比如你测试的"Hello, world!")时,优势更明显。
x86-64处理字符串的推荐方案
寄存器选择
RSI(源指针)和RDI(目标指针)确实是x86-64中字符串操作的标准寄存器,这是System V AMD64 ABI规定的——函数调用中第一个参数(dst)存RDI,第二个参数(src)存RSI,刚好匹配字符串操作指令的要求,所以使用它们是符合规范的。
指令选择
根据字符串长度不同,推荐的实现方式也不同:
短字符串(几十字节以内):用手动的字节/字复制循环,比如修正后的RSI/RDI版本:
custom_strcpy proc mov rsi, rdx ; src -> RSI mov rdi, rcx ; dst -> RDI mov rax, rdi ; 保存目标地址作为返回值 _loop: mov al, [rsi] ; 读取源字节 mov [rdi], al ; 写入目标 inc rsi inc rdi test al, al ; 判断是否为终止0 jne _loop ret custom_strcpy endp这个版本逻辑正确,没有冗余操作,性能会和你的第二个实现相当甚至更好。
长字符串:使用
rep movsb(重复字符串复制),现代CPU(比如Intel的Fast String技术)对rep前缀的字符串指令做了深度优化,能利用内存带宽实现高速复制。不过需要先计算字符串长度:custom_strcpy proc mov rsi, rdx mov rdi, rcx mov rax, rdi ; 保存返回地址 ; 计算字符串长度(包括终止0) mov rcx, -1 xor al, al repne scasb ; 从RDI(临时用一下)找终止0,RCX变为 -(长度+1) neg rcx ; RCX = 字符串总长度(含0) mov rdi, rax ; 恢复目标指针 rep movsb ; 复制RCX字节 ret custom_strcpy endp更高效的宽字节复制:对于长字符串,还可以先用
movq(8字节)、movdqu(16字节)等宽指令批量复制,再处理剩余的字节,进一步减少循环次数。
总结
你的第一个实现性能差不是因为RSI/RDI不好,而是代码逻辑存在冗余和不合理的内存访问。修正后的RSI/RDI版本完全可以达到甚至超越手动指针操作的性能。在x86-64中,RSI/RDI是字符串处理的标准寄存器,选择指令时要根据字符串长度和场景来决定——短字符串用简单循环,长字符串用优化过的rep前缀指令。
内容的提问来源于stack exchange,提问作者paxbun

