当前版本中剩下的唯一错误是加载 8 个字节的 char 数据作为返回值,而不是仅仅进行指针数学运算,使用mov
代替lea
. (在删除了各种编辑并添加了不同的错误之后,这反映在不同的答案谈论不同的代码)。
但这过于复杂且效率低下(两个加载和索引寻址模式,当然还有额外的指令来设置 RCX)。
只需增加指针,因为这就是您想要返回的内容。
如果您要一次循环 1 个字节,而不是使用 SSE2 一次检查 16 个字节,strchr
可以简单如下:
;; BITS 64 is useless unless you're writing a kernel with a mix of 32 and 64-bit code
;; otherwise it only lets you shoot yourself in the foot by putting 64-bit machine code in a 32-bit object file by accident.
global mystrchr
mystrchr:
.loop: ; do {
movzx ecx, byte [rdi] ; c = *p;
cmp cl, sil ; if (c == needle) return p;
je .found
inc rdi ; p++
test cl, cl
jnz .loop ; }while(c != 0)
;; fell out of the loop on hitting the 0 terminator without finding a match
xor edi, edi ; p = NULL
; optionally an extra ret here, or just fall through
.found:
mov rax, rdi ; return p
ret
我在字符串结尾之前检查了匹配项,因此我仍然拥有未递增的指针,而不必在“找到”返回路径中递减它。如果我用 开始循环inc
,我可以使用[rdi - 1]
寻址模式,仍然避免使用单独的计数器。这就是为什么我将哪个分支位于循环底部的顺序与问题中的代码进行了切换。
由于我们想将字符比较两次,分别针对 SIL 和零,我将其加载到寄存器中。这可能不会在现代 x86-64 上运行得更快,每个时钟可以运行 2 个负载以及 2 个分支(只要最多使用其中一个)。
一些英特尔 CPU 可以将微融合和宏融合 cmp reg,mem / jcc
为前端的单个加载+比较和分支微指令,至少在内存寻址模式简单而不是索引时。但不是cmp [mem], imm
/ jcc
,因此我们不会通过单独加载到寄存器中来为英特尔 CPU 上的前端花费任何额外的微指令。(使用 movzx 来避免写入部分寄存器的错误依赖,例如mov cl, [rdi]
)
请注意,如果您的调用程序也是用汇编编写的,则返回多个值很容易,例如状态和指针(在未找到的情况下,终止 0 可能会很有用)。 许多 C 标准库字符串函数设计得很糟糕,尤其是strcpy
不能帮助调用者避免重做长度查找工作。
特别是在具有 SIMD 的现代 CPU 上,具有显式长度非常有用:现实世界的strchr
实现会检查对齐情况,或者检查给定指针是否不在页面末尾的 16 个字节内。但memchr
不必这样做,如果大小 >= 16:它可以只movdqu
加载和pcmpeqb
.
请参阅在 x86 和 x64 上的同一页面内读取缓冲区末尾是否安全?有关详细信息和 glibcstrlen
的手写 asm 的链接。还可以使用 simd 查找字符的第一个实例,以用于 glibc 的使用pcmpeqb
/等真实世界的实现pmovmskb
。(也许pminub
对于 0 终止符检查来展开多个向量。)
对于非小字符串,SSE2 可以比此答案中的代码快 16 倍。对于非常大的字符串,您可能会遇到内存瓶颈并且“仅”快 8 倍。