对于一个 hacky 足够好的版本,we know rdi
有有效地址。很有可能的是edi
不是一个小整数,因此 2 字节mov ecx, edi
。但这并不安全,因为 RDI 可能指向刚刚超过 4GiB 边界的位置,因此很难证明它是安全的。除非您使用像 x32 这样的 ILP32 ABI,否则所有指针都低于 4GiB 标记。
因此,您可能需要使用 push rdi / pop rcx 复制完整的 RDI,各 1 个字节。但这会增加短字符串启动的额外延迟。如果您没有长度大于其起始地址的字符串,那么它应该是安全的。 (但那是似是而非的如果您有任何大型数组,则用于 .data、.bss 或 .rodata 中的静态存储;例如,Linux 非 PIE 可执行文件的加载时间约为0x401000
= 1
如果您只想让 rdi 指向终止,这非常有用0
字节,而不是实际需要计数。或者,如果您在另一个寄存器中有起始指针,那么您可以这样做sub edi, edx
或其他东西并以这种方式获取长度而不是处理rcx
结果。 (如果您知道结果适合 32 位,则不需要sub rdi, rdx
因为你知道无论如何它的高位都是零。并且高输入位不影响加/减的低输出位;进位从左到右传播。)
对于已知小于 255 字节的字符串,您可以使用mov cl, -1
(2 个字节)。这使得rcx
至少 0xFF,或者更高,具体取决于其中剩余的垃圾量。 (当读取 RCX 时,Nehalem 和更早的版本会出现部分注册停顿,否则仅依赖于旧的 RCX)。无论如何,那么mov al, -2
/ sub al, cl
获取 8 位整数的长度。这可能有用也可能没用。
根据呼叫者的不同,rcx
可能已经保存了一个指针值,在这种情况下,如果可以使用指针减法,则可以保持它不变。
在您提出的选项中
lea ecx,[rax-1]
非常好,因为你只是异或归零eax
,它是一个廉价的 1 uop 指令,具有 1 个周期延迟,可以在所有主流 CPU 上的多个执行端口上运行。
当您已经有另一个具有已知常量值的寄存器时,尤其是异或归零的 3 字节寄存器lea
如果可行的话,几乎总是创建常量的最有效的 3 字节方法。 (看有效地将CPU寄存器中的所有位设置为1 https://stackoverflow.com/questions/45105164/set-all-bits-in-cpu-register-to-1-efficiently).
我完全知道有使用现代 CPU 指令的实现,但这种传统方法似乎是最小的一种。
Yes, repne scasb
非常紧凑。在典型的 Intel CPU 上,其启动开销可能约为 15 个周期,并且根据阿格纳·雾 http://agner.org/optimize/,它发出 >=6n uops,吞吐量 >= 2n 个周期,其中n
是计数(即每个字节比较 2 个周期进行长比较,其中隐藏了启动开销),因此它的成本相形见绌lea
.
错误依赖的东西ecx
可能会延迟其启动,所以你肯定想要lea
.
repne scasb
对于你正在做的任何事情来说可能足够快,但它比pcmpeqb
/ pmovmsbk
/ cmp
。对于短的固定长度字符串,integer cmp
/ jne
is very当长度为 4 或 8 字节时很好(包括终止 0),假设您可以安全地过度读取字符串,即您不必担心""
在页面的末尾。不过,此方法的开销随字符串长度而变化。例如,对于字符串长度 = 7,您可以执行 4、2 和 1 个操作数大小,或者可以执行两个重叠 1 个字节的双字比较。喜欢cmp dword [rdi], first_4_bytes / jne
; cmp dword [rdi+3], last_4_bytes / jne
.
有关 LEA 的更多详细信息
在 Sandybridge 系列 CPU 上,lea
可以在与它和执行单元相同的周期中分派到执行单元xor
-0 被发送到无序的 CPU 核心。xor
-归零是在问题/重命名阶段处理的,因此uop以“已执行”状态进入ROB。指令不可能必须等待 RAX。 (除非异或和异或之间发生中断lea
,但即便如此,我认为在恢复 RAX 之后和之前会有一个序列化指令lea
可以执行,所以不能一直等待。)
Simple lea
可以在 SnB 上的 port0 或 port1 上运行,或在 Skylake 上的 port1 / port5 上运行(每个时钟吞吐量 2 个,但有时在不同 SnB 系列 CPU 上有不同的端口)。这是 1 个周期的延迟,因此很难做得更好。
您不太可能看到使用带来任何加速mov ecx, -1
(5 字节)可以在任何 ALU 端口上运行。
在 AMD 锐龙上,lea r32, [m]
在 64 位模式下被视为“慢”LEA,只能在 2 个端口上运行,并且具有 2c 延迟而不是 1。更糟糕的是,Ryzen 并没有消除异或归零。
您所做的微基准测试仅测量没有错误依赖性的版本的吞吐量,而不测量延迟。这通常是一个有用的衡量标准,并且您确实得到了正确的答案:lea
是最好的选择。
纯粹的吞吐量是否准确地反映了有关您的实际用例的任何信息是另一回事。如果字符串比较位于关键路径上,作为长或循环携带的数据依赖链的一部分,并且未被中断,那么您实际上可能依赖于延迟,而不是吞吐量。jcc
为您提供分支预测+推测执行。 (但是无分支代码通常更大,所以这不太可能)。
stc
/ sbb ecx,ecx
很有趣,但只有 AMD CPU 可以处理sbb
作为依赖破坏(仅取决于 CF,而不取决于整数寄存器)。在 Intel Haswell 及更早版本上,sbb
是一条 2 uop 指令(因为它有 3 个输入:2 个 GP 整数 + 标志)。它有 2c 的延迟,这就是它性能如此糟糕的原因。 (延迟是循环携带的 dep 链。)
缩短序列的其他部分
根据您正在做的事情,您也许可以使用strlen+2
也好,但要抵消另一个常数或其他东西。dec ecx
在 32 位代码中只有 1 个字节,但 x86-64 没有简写形式inc/dec
指示。所以 not /dec 在 64 位代码中并不那么酷。
After repne scas
, 你有ecx = -len - 2
(如果你开始于ecx = -1
), and not
给你-x-1
(i.e. +len + 2 - 1
).
; eax = 0
; ecx = -1
repne scasb ; ecx = -len - 2
sub eax, ecx ; eax = +len + 2