8086中的字符串比较

时间:2015-11-08 09:47:48

标签: string assembly x86 x86-16 strcmp

我对这个问题有疑问。我不知道它对我的要求。

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

我的回答:

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

我仍然不确定。是真的还是我应该尝试其他的东西?

p.s:我不明白为什么人们会对这个问题投反对票。我的问题出了什么问题?我想我们都在这里学习。或不 ?我想念一下吗?

1 个答案:

答案 0 :(得分:3)

如果问题陈述表明,当您被调用时,指针已经在SIDI,那么您就不应该破坏它们。

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

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

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

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

但是对于表现来说,repe cmpsb无论如何都不快(比如每次比较2至3个周期,Skylake vs. Ryzen。或者Bulldozer家族每个比较甚至4个周期)。只有rep movsrep stos在现代CPU上才有效,优化的微码一次可复制或存储16个(或32或64个)字节。

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

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

所以你想从一个字符串中将一个字节加载到一个寄存器中,并检查它是否为teminator而另一个字符串的内存。

如果您需要实际使用ES:DI 而不是仅使用默认DS细分基础的DI,您可以使用cmp al, [es: bx + di](NASM语法,根据需要进行调整,如{{1} 1}}我认为。可能是您使用cmp al, es: [bx + di]lodsb 的问题,因为scasb uses ES:DI。)

scasb

用法:将指针放入SI / 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 / call strcmp ,因为匹配/不匹配状态位于FLAGS中。如果要将条件转换为整数,386及更高版本的CPU允许je match根据sete al quals条件(ZF == 1)在AL中创建0或1。

使用e代替sub al, [mem],我们获得cmp al, [mem],仅在字符串匹配时才给我们0。如果您的字符串仅保留0..127的ASCII值,则不会导致签名溢出,因此您可以将其用作有符号的返回值,该值实际上告诉您哪个字符串在另一个之前/之后排序。 (但如果字符串中可能存在高位ASCII 128..255字节,我们需要将零或符号扩展到16位第一以避免签名溢出喜欢(无符号)5 - (无符号)254 =(签名)+7,因为8位环绕。

当然,使用我们的FLAGS返回值,来电者已经可以使用al = str1[i] - str2[i]ja(无符号比较结果),或jb / {{ 1}}如果他们想将字符串视为持有jg。无论输入字节的范围如何,这都有效。

或者内联此循环,以便jl直接跳转到某个有用的地方

16-bit addressing modes are limited,但BX可以是基础,SI和DI都可以是索引。我使用了索引增量而不是signed charjne mismatch。使用inc si也是一个选项,甚至可以inc di将其与其他字符串进行比较。 (然后检查终结符。)

效果

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

如果您关心现代x86,请使用inc bx来打破对EAX的旧值的错误依赖,对于不单独重命名部分寄存器的CPU。 (如果你使用cld,那么打破false dep尤其重要,因为这会通过EAX将其变成一个2循环循环的依赖链,而不是通过Sandybridge的PPro以外的CPU.IvyBridge以后不会将E重命名为EAX,因此movzx eax, byte [si+bx]是微融合加载+合并uop。)

如果sub al, [str2]微量加载,并与mov al, [mem]进行宏融合到一个比较和分支uop中,整个循环在Haswell上总共只有4个uop,并且可以运行每个时钟1次迭代,用于大输入。最后的分支错误预测会使小输入性能变差,除非每次输入足够小的分支都会使用分支。见https://agner.org/optimize/。最近英特尔和AMD每个时钟可以进行2次加载。

展开可以分摊cmp al,[bx+di]费用,但这就是全部。在循环内部采用+ not-taken分支,没有当前的CPU可以比每次迭代的1个循环更快地运行。 (有关do {} while循环结构的更多信息,请参阅Why are loops always compiled into "do...while" style (tail jump)?)。为了加快速度,我们需要一次检查多个字节。

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

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

GNU libc's后备标量strcmp实现看起来不错(从AT& T转换为英特尔语法,但使用C预处理器宏和剩下的东西。jne生成本地标签)。

仅当SSE2或更好的版本不可用时才使用此功能。对于任何零字节检查整个32位寄存器都有一些比特,即使没有SIMD也可以让你更快,但是对齐是一个问题。 (如果终结符可以在任何地方,则在一次加载多个字节时不得不小心,不要从任何内存页面读取,而您确定包含至少1个字节的有效数据,否则你可能会有错。)

inc bx