8086 中的字符串比较

string comparison in 8086

我对这个问题有疑问。我不知道它想从我这里得到什么。

问题:编写一个过程,将 DS:SI 处的源字符串与 ES:DI 处的目标字符串进行比较,并相应地设置标志。如果源小于目标,则设置进位标志。如果字符串相等,则设置零标志。如果源大于目标,零和进位标志都被清除。

我的回答:

MOV ESI , STRING1
MOV EDI, STRING2
MOV ECX, COUNT
CLD
REPE CMPSB

我还是不太确定。是真的还是我应该试试别的?

p.s:我不明白为什么人们会否决这个问题。我的问题有什么问题?我想我们都是来学习的。或不 ?有什么想念的吗?

如果问题陈述说当你被调用时指针已经在 SIDI 中,你不应该破坏它们。

16 位代码通常不会对所有函数都遵循单一的调用约定,并且在寄存器中传递(前几个)args 通常是好的(指令更少,并且避免 store/reload)。 32 位 x86 调用约定通常使用堆栈参数,但那已经过时了。 Windows x64 和 Linux/Mac x86-64 System V ABI/调用约定都使用寄存器参数。


虽然问题陈述没有提到计数。 因此,您要为以零字节结尾的字符串实现 strcmp,而不是为已知长度的内存块实现 memcmp 您不能使用单个 rep 指令,因为您需要检查不相等和字符串结尾。 如果您只是传递一些大尺寸,并且字符串 相等,repe cmpsb 将继续超过终止符。

如果您知道 either 字符串的长度,则

repe cmpsb 是可用的。例如在 CX 中取一个长度 arg 以避免 运行 在两个字符串中超过终止符的问题。

但就性能而言,repe cmpsb 无论如何都不是很快(例如在 Skylake 与 Ryzen 上每次比较 2 到 3 个周期。或者在 Bulldozer 系列上每次比较甚至 4 个周期)。只有 rep movsrep stos 在现代 CPU 上是高效的,优化的微代码一次复制或存储 16(或 32 或 64)字节。

在内存中存储字符串有 2 种主要约定:显式长度字符串(指针 + 长度),如 C++ std::string,和 隐式length strings 你只有一个指针,字符串的结尾用哨兵/终止符标记。 (就像使用 0 字节的 C char*,或使用 '$' 作为终止符的 DOS 字符串打印函数。)


一个有用的观察是您只需要检查 一个 字符串中的终止符。如果另一个字符串有终止符而这个字符串没有,那将是不匹配的。

因此,您想将一个字节从一个字符串加载到寄存器中,并检查它的终止符和另一个字符串的内存。

(如果您需要实际使用 ES:DI 而不是仅使用默认 DS 段基数的 DI,您可以使用 cmp al, [es: bx + di] (NASM 语法,根据需要进行调整,就像 cmp al, es: [bx + di] 我认为的那样)。 可能是为您准备的问题使用 lodsbscasb,因为 scasb uses ES:DI.)

;; inputs:  str1 pointer in DI, str2 pointer in SI
;; outputs: BX = mismatch index, or one-past-the-terminators.
;;          FLAGS:  ZF=1 for equal strings (je),  ZF=0 for mismatch (jne)
;; clobbers: AL (holds str1's terminator or mismatching byte on return)
strcmp:
    xor    bx, bx
.innerloop:                 ; do { 
    mov    al, [si + bx]        ; load a source byte
    cmp    al, [di + bx]        ; check it against the other string
    jne   .mismatch             ; if (str1[i] != str2[i]) break;

    inc    bx                   ; index++

    test   al, al               ; check for 0.  Use cmp al, '$' for a $ terminator
    jnz   .innerloop        ; }while(str1[i] != terminator);

    ; fall through (ZF=1) means we found the terminator
    ; in str1 *and* str2 at the same position, thus they match

.mismatch:               ; we jump here with ZF=0 on mismatch
    ; sete  al           ; optionally create an integer in AL from FLAGS
    ret

用法:把指针放在SI/DI、call strcmp/je match,因为匹配/不匹配状态在FLAGS中。如果你想把条件变成一个整数,386和以后的CPU允许sete al根据equals条件(ZF==1)在AL中创建一个0或1。

使用 sub al, [mem] 而不是 cmp al, [mem],我们将得到 al = str1[i] - str2[i],仅当字符串匹配时才给我们一个 0。如果您的字符串仅包含 0..127 的 ASCII 值,则不会导致有符号溢出,因此您可以将其用作带符号的 return 值,它实际上告诉您哪个字符串排序 before/after 另一个。 (但是如果字符串中可能有高 ASCII 128..255 字节,我们需要零或符号扩展到 16 位 first 以避免符号溢出像 (unsigned)5 - (unsigned)254 = (signed)+7 因为 8 位环绕。

当然,有了我们的 FLAGS return 值,调用者已经可以使用 jajb(未签名的比较结果),或者 jg / jl 如果他们想将字符串视为持有 signed char。无论输入字节的范围如何,这都有效。

或者内联这个循环,这样jne mismatch直接跳转到有用的地方

16-bit addressing modes are limited,但BX可以作为基数,SI和DI都可以作为索引。我使用索引增量代替 inc siinc di。使用 lodsb 也是一种选择,甚至可能 scasb 将其与其他字符串进行比较。 (然后检查终止符。)


性能

索引寻址模式在某些现代 x86 CPU 上可能会更慢,但这确实可以在循环中保存指令(因此对于真正的 8086 代码大小很重要)。尽管要真正调整 8086,我认为 lodsb / scasb 将是您最好的选择,替换 mov 负载和 cmp al, [mem],以及 inc bx。如果您的调用约定不能保证,请记住在循环外使用 cld

如果您关心现代 x86,请使用 movzx eax, byte [si+bx] 打破对 EAX 旧值的错误依赖,对于不单独重命名部分寄存器的 CPU。 (如果你使用 sub al, [str2],打破错误的 dep 尤其重要,因为这会通过 EAX 将它变成一个 2 周期循环携带的依赖链,在 PPro 以外的 CPU 上通过 Sandybridge。IvyBridge 和后来的不重命名 AL与 EAX 分开,所以 mov al, [mem] 是一个微融合加载+合并 uop。)

如果 cmp al,[bx+di] 微融合负载,并与 jne 宏融合为一个比较和分支微指令,则整个循环在 Haswell 上总共只有 4 微指令,并且对于大输入,可以 运行 每个时钟迭代 1 次。最后的分支预测错误会使小输入性能变差,除非分支每次都对足够小的输入起作用。参见 https://agner.org/optimize/。最近的 Intel 和 AMD 每个时钟可以执行 2 个负载。

展开可以摊销 inc bx 成本,但仅此而已。在循环内有一个采用 + 不采用的分支,当前的 CPU 都不能 运行 比每次迭代 1 个周期更快。 (有关 do{}while 循环结构的更多信息,请参阅 )。为了更快,我们需要一次检查多个字节。

与使用 SSE2 的每 1 或 2 个周期 16 个字节相比,即使是 1 个字节/周期也非常慢(使用一些巧妙的技巧来避免读取可能出错的内存)。

有关使用 x86 SIMD 进行字符串比较以及 glibc 的 SSE2 和更高版本的优化字符串函数的更多信息,请参阅 https://www.strchr.com/strcmp_and_strlen_using_sse_4.2


GNU libc's 后备标量 strcmp 实现看起来不错(从 AT&T 翻译成 Intel 语法,但保留了 C 预处理器宏和其他内容。L() 制作了一个本地标签)。

只有在 SSE2 或更高版本不可用时才使用它。有一些技巧可以检查整个 32 位寄存器中是否有任何零字节,即使没有 SIMD,这也能让你走得更快,但对齐是个问题。 (如果终止符可能在任何地方,您在一次加载多个字节时必须小心,不要从您不 sure 包含至少 1 个字节的有效数据的任何内存页中读取,否则你可能会犯错。)

strcmp:
         mov    ecx,DWORD PTR [esp+0x4]
         mov    edx,DWORD PTR [esp+0x8]     # load pointer args

L(oop): mov     al,BYTE PTR [ecx]          # movzx eax, byte ptr [ecx] would be avoid a false dep
        cmp     al,BYTE PTR [edx]
        jne     L(neq)
        inc     ecx
        inc     edx
        test    al, al
        jnz     L(oop)

        xorl    eax, eax
        /* when strings are equal, pointers rest one beyond
           the end of the NUL terminators.  */
        ret

L(neq): mov    eax,  1
        mov    ecx, -1
        cmovb  eax, ecx
        ret