最小操作码大小x86-64 strlen实现

时间:2018-04-15 10:10:33

标签: assembly nasm x86-64 yasm

我正在调查我的代码高尔夫/二进制可执行文件的最小操作码大小 x86-64 strlen 实现,这不应该超过某个大小(想想demoscene for简单)。
总体思路来自here,来自herehere的尺寸优化提示。

输入字符串地址位于rdi,最大长度不应大于Int32

xor   eax,eax ; 2 bytes
or    ecx,-1  ; 3 bytes
repne scasb   ; 2 bytes
not   ecx     ; 2 bytes
dec   ecx     ; 2 bytes

最终结果位于ecx 11个字节总计。

问题是关于将ecx设置为-1

选项1已经陈述

or ecx,-1 ; 3 bytes

选项2

lea ecx,[rax-1] ; 3 bytes 

选项3

stc         ; 1 byte
sbb ecx,ecx ; 2 bytes

选项4,可能是最慢的

push -1 ; 2 bytes
pop rcx ; 1 byte

我理解:
选项1依赖于先前的ecx
选项2依赖于先前的rax
选项3我不确定它是否依赖于之前的ecx值?
选项4是最慢的?

这里有明显的赢家吗? 标准是保持操作码的大小尽可能小,并选择最佳性能。
我完全清楚有使用现代cpu指令的实现,但这种传统方法似乎是最小的。

2 个答案:

答案 0 :(得分:1)

对于hacky good-enough版本,我们知道rdi有一个有效的地址。 edi很可能不是一个小整数,因此2字节mov ecx, edi 。在使用之前检查这对所有呼叫站点是否安全!

如果您只是希望rdi指向终止0字节,而不是实际需要计数,那么这很好。或者,如果您在另一个寄存器中有启动指针,那么您可以执行sub edi, edx或其他操作,并获得相应的长度,而不是处理rcx结果。 (如果您知道结果符合32位,那么您不需要sub rdi, rdx因为您知道它的高位无论如何都是零。而高输入位不会影响低输出位add / sub;进行从左到右的传播。)

对于已知低于255字节的字符串,您可以使用mov cl, -1(2字节)。这使得rcx至少为0xFF,并且取决于其中剩余的高垃圾量。 (这在Nehalem上有一个部分注册失速,在读取RCX时更早,否则只是对旧RCX的依赖)。无论如何,然后mov al, -2 / sub al, cl将长度作为8位整数。这可能有用也可能没用。

根据调用者的不同,rcx可能已经持有指针值,在这种情况下,如果可以使用指针减法,则可以保持不变。

您提出的选项

lea ecx,[rax-1]非常好,因为你只是xor-zeroed eax,它是一个便宜的1 uop指令,具有1个周期延迟,可以在所有主流CPU的多个执行端口上运行。 / p>

当你已经有另一个具有已知常量值的寄存器时,特别是那个xor-zeroed,3字节lea几乎总是最有效的3字节方式来创建一个常量,如果它作品。 (见Set all bits in CPU register to 1 efficiently)。

  

我完全清楚有使用现代cpu指令的实现,但这种传统方法似乎是最小的。

是的,repne scasb非常紧凑。它的启动开销可能类似于典型Intel CPU上的15个周期,根据Agner Fog,它发出> = 6n uops,吞吐量> = 2n个周期,其中n是计数(即,对于长时间比较,每个字节有2个周期进行比较,其中启动开销被隐藏),因此它使lea的成本相形见绌。

ecx具有错误依赖性的内容可能会延迟其启动,因此您肯定需要lea

repne scasb对于您正在做的事情来说可能足够快,但它比pcmpeqb / pmovmsbk / cmp慢。对于短固定长度字符串,当长度为4或8字节(包括终止0)时,整数cmp / jne 非常良好,假设您可以安全地过度阅读您的字符串,即您不必担心页面末尾的""。但是,此方法的开销随字符串长度而变化。例如,对于字符串长度= 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 - 零被发送到无序CPU内核。 xor - 在发布/重命名阶段处理归零,因此uop在"已执行的"中进入ROB。州。指令不可能等待RAX。 (除非在xor和lea之间发生中断,但即便如此,我认为在恢复RAX之后和lea可以执行之前,它仍然是一个序列化指令,所以它无法在'等待等待。)

简单lea可以在SnB上的port0或port1上运行,或者在Skylake上运行port1 / port5(每个时钟吞吐量2个,但有时在不同的SnB系列CPU上有不同的端口)。这是1个周期的延迟,因此很难做得更好。

使用mov ecx, -1(5个字节)可以在任何ALU端口上运行时,您不太可能看到任何加速。

在AMD Ryzen上,64位模式下的lea r32, [m]被视为"慢" LEA只能在2个端口上运行,并且具有2c延迟而不是1。更糟糕的是,Ryzen并没有消除xor-zeroing。

您所做的微基准测试仅测量没有错误依赖性而非延迟的版本的吞吐量。这通常是一个有用的衡量标准,您确实得到了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位代码中并不酷。

repne scas之后,您有ecx = -len - 2(如果您开始ecx = -1), and而非gives you - x-1 (i.e. + len + 2 - 1`)

 ; eax = 0
 ; ecx = -1
repne scasb      ; ecx = -len - 2
sub   eax, ecx   ; eax = +len + 2

答案 1 :(得分:0)

我在英特尔酷睿i7 4850HQ Haswell 2,3 GHz上做了一些基准测试,发布版本没有连接调试器。在每个循环中,我测量1000个asm指令序列,并将其重复10百万次以达到平均结果。

我已经制作了100次重复asm指令的宏。

URIRef

使用inline asm for MacOS测试C代码

#define lea100 asm{xor   eax,eax};asm { lea ecx,[rax-1] }; // <== Copy pasted 100times
#define or100 asm{xor   eax,eax};asm { or ecx,-1 }; // <== Copy pasted 100times
#define sbb100 asm{xor   eax,eax};asm { stc };asm{sbb ecx,ecx}; // <== Copy pasted 100times
#define stack100 asm ("xor %eax,%eax;.byte 0x6A; .byte 0xFF ;pop %rcx;"); // <== Copy pasted 100times

<强>结果

#include <stdio.h>
#include <CoreServices/CoreServices.h>
#include <mach/mach.h>
#include <mach/mach_time.h>
int main(int argc, const char * argv[]) {
    uint64_t        start;
    uint64_t        end;
    uint64_t        elapsed;
    Nanoseconds     elapsedNano;

    uint64_t sum = 0;
    for (int i = 0; i < 10000000 ; i++) {

// this will become
// call       imp___stubs__mach_absolute_time  
// mov        r14, rax
    start = mach_absolute_time();

//10x lea100 for example for total 1000 

// call       imp___stubs__mach_absolute_time
// sub        rax, r14
    end = mach_absolute_time();

    elapsed = end - start;
    elapsedNano = AbsoluteToNanoseconds( *(AbsoluteTime *) &elapsed );
    uint64_t nano = * (uint64_t *) &elapsedNano;
        sum += nano;
    }
    printf("%f\n",sum/10000000.0);
    return 0;
}

205-216 ns

xor eax,eax
lea ecx,[rax-1]

321-355 ns

xor eax,eax
or ecx,-1

322-359 ns

xor eax,eax
push -1 
pop rcx 

612-692 ns