你只需使用
inc %rbx
来增加指针值。
(%rbx)
会对该寄存器进行解引用,使用其值作为内存地址。在x86上,每个字节都有自己的地址(这个属性称为“按字节寻址”),并且地址仅是适合寄存器的整数。
ASCII字符串中的字符都是1个字节宽,所以将指针增加1就可以移到ASCII字符串中的下一个字符。(在UTF-8中包含1..127范围外代码点的字符的一般情况下,这不成立,但ASCII是UTF-8的子集。)
术语:ASCII代码
0
称为NUL(一个L),而不是NULL。在C中,NULL是一个指针概念。 C风格的隐式长度字符串可以描述为0终止或NUL终止,但“null-terminated”的使用术语是错误的。
你应该选择一个不需要推/弹回函数的寄存器(调用破坏的寄存器)。由于代码不生成任何函数调用,因此没有必要将归纳变量保留在保存调用的寄存器中。
我在其他SO Q&A中没有找到一个好的简单示例。它们要么在循环内有2个分支(包括一个无条件jmp),就像我在评论中链接的那个,要么浪费指令递增指针和计数器。在循环内使用索引寻址模式并不可怕,但在某些CPU上效率较低,因此我仍然建议在循环后执行指针递增->从结束-开始减去。
这是我编写的一个最小strlen示例,它只检查1个字节(缓慢而简单)。我保持了循环本身的简洁,这在我看来是编写循环的好方法的合理示例。通常,使代码紧凑可以更容易地以asm形式理解函数。(给它取一个不同于
strlen
的名称,这样您就可以测试它而不需要
gcc -fno-builtin-strlen
或其他内容。)
.globl simple_strlen
simple_strlen:
lea -1(%rdi), %rax # p = start-1 to counteract the first inc
.Lloop: # do {
inc %rax # ++p
cmpb $0, (%rax)
jne .Lloop # }while(*p != 0)
# RAX points at the terminating 0 byte = one-past-end of the real data
sub %rdi, %rax # return length = end - start
ret
strlen
的返回值是
0
字节的数组索引,也就是数据长度(不包括终止符)。
如果你手动内联这个函数(因为只有3条指令的循环),你通常只需要终止符的指针,所以你不必费心去做减法,只需在循环结束时使用
RAX
即可。
避免第一次加载之前的LEA/INC操作(在第一个cmp之前花费了2个周期的延迟)可以通过削离第一次迭代或通过
jmp
在inc后进入循环。参见
为什么循环总是被编译成“do...while”样式(尾调用)?。
在cmp/jcc之间使用LEA增加指针(例如
cmp;lea1(%rax),%rax;jne
)可能会更糟,因为它会破坏cmp/jcc的宏融合,使其成为单个微操作。(实际上,类似Skylake这样的英特尔CPU上根本不会发生
cmp $imm,(%reg)
/jcc的宏融合。但是,
cmp
会融合存储器操作数。也许AMD会融合cmp/jcc。)此外,循环结束时,
RAX
会比预期的高1。
所以(在英特尔Sandybridge系列上),将一个字节转换为
%ecx
并将其扩展为零的
movzx
(又名
movzbl
),以及将其作为循环条件进行测试
test %ecx,%ecx
/
jnz
,效率与LEA相同。但代码更大。
大多数CPU将每个时钟周期运行我的循环一次。通过一些循环展开,我们可以接近每个时钟周期检查2个字节(仍然只是单独检查每个字节)。
对于大型字符串,逐个字节检查比使用SSE2慢约16倍。如果你不追求代码大小和简易性,请参见
为什么这段代码启用优化后会慢6.5倍?,其中介绍了一种使用XMM寄存器的简单SSE2 strlen。 SSE2是x86-64的基线,因此应该在需要手动编写汇编程序的情况下始终使用它以提高速度。
关于你更新的问题,使用从为什么在这种情况下rax和rdi起着相同的作用?中实现有缺陷的端口。
RDI和RBX都保存指针。将它们加在一起不会得到有效的地址!在尝试端口的代码中,RCX(索引)在循环之前被初始化为零。但是,你没有使用xor %ebx,%ebx
,而是使用了mov %rdi,%rbx
。在单步执行代码时使用调试器来检查寄存器的值。
inc %rbx
,但是使用RAX可以让您在不必保存/恢复RBX的情况下完成相同的操作。否则,您不应该使用32位寄存器来存储指针。 - Peter Cordes