關於字串比較的一點討論---strcmp與memcmp的效率及實現原理
阿新 • • 發佈:2019-01-13
要求寫一個比較高效的檔案比較程式,竟然發現memcmp比strcmp要快很多,於是跟蹤除錯,發現它們的實現原理:
intel/strcmp.asm:
mov edx, dword ptr [esp + 4] ;取第二個引數地址
mov ecx, dword ptr [esp + 8] ;取第一個引數地址
test edx, 3 ;edx是第二個引數的地址,這裡即檢驗該地址是否是4的倍數。
;因為如果edx&3!=0,則其最低兩位不為1,所以為4的倍數。這裡有個記憶體地址對齊的問題。
jne dopartial ;如果地址不是4的倍數,就跳到dopartial去處理。
dodwords:
mov eax, dword ptr [edx]
cmp al, byte ptr [ecx]
jne donene
or al, al ;看看字串是否結束,這就是strcmp之所以比memcmp慢的地方了。
je doneeq ;如果 al==0,則比較結束
cmp ah, byte ptr [ecx + 1]
jne donene
or ah, ah
je doneeq
shr eax, 10h ;右移16位
cmp al, byte ptr [ecx + 2]
jne donene
or al, al
je doneeq
cmp ah, byte ptr [ecx + 3]
jne donene
or ah, ah
je doneeq
add ecx, 4
add edx, 4
or ah, ah
jne dodwords
move edi, edi ;這裡一直大惑不解,不明白為什麼這裡要多出這兩個位元組來
doneeq:
xor eax, eax ;比較結果是相等,返回值為0
ret
nop ;這裡也一直大惑不解,不明白這裡為什麼要插入一條空指令,感覺和上面的mov edi, edi應該是同一個原因。
donene:
sbb eax, eax ;比較結果不相等,這裡也很經典,使用帶借位減法,eax = eax - eax -cf。
shl eax, 1 ;若不相等處是大於,則 cf == 0,eax == 0,下面加1後eax==1,返回。
inc eax ;若不相等處是小於,則 cf == 1,那麼 sbb後eax==-1,補碼為0xFFFFFFFF,左移再加1還是-1
ret
mov edi, edi ;這裡又出來了,不知道為什麼要這麼做,痛苦。
dopartial:
test edx, 1
je doword ;同樣,與1與如果為0,則地址是2的倍數,跳到doword去執行。
mov al, byte ptr [edx]
inc edx
cmp al, byte ptr [ecx]
jne donene
inc ecx
or al, al
je doneeq
test edx, 2
je dodwords
dowords:
mov ax, word ptr [edx]
add edx, 2
cmp al, byte ptr [ecx]
jne donene
or al, al
je doneeq
cmp ah, byte ptr[ecx + 1]
jne donene
or ah, ah
je doneeq
add ecx, 2
jmp dodwords
intel/memcmp.asm
memcmp函式程式碼:
引數堆疊:
offset str1 --- ebp - 4 ------------當前棧頂
offset str2 --- ebp - 8
eax (strlen(str1)的返回值)
memcmp:
mov eax, dword ptr [esp + 0ch] ;得到memcmp的第三個引數:要比較的個數
test eax, eax ; 看要比較的位元組數是否為0
je retnull ;如果要比較的位元組數為0則直接返回
mov edx, dword ptr [esp + 4] ;得到memcmp的第一個引數,即offset str1
push esi
push edi ;儲存暫存器值
mov esi, edx ;源字串地址,即offset str1
mov edi, dword ptr [esp + 10h] ;不知何意,
or edx, edi
and edx, 3 ;根據strcmp的分析,這裡依然是判斷edx地址是不是4的倍數
je dwords ;地址是4的倍數,則跳到dwords去處理
test eax, 1 ;eax中存的是字串長度。如果地址不是4的倍數,那麼看要比較的位元組數是不是2的倍數。
je mainloop ;如果要比較的記憶體位元組數是2的倍數,則轉向mainloop。
mov cl, byte ptr [esi] ;否則eax==1,比較最後一個位元組
cmp cl, byte ptr [edi]
jne not_equal
inc esi
inc edi
dec eax
je done
main_loop:
mov cl, byte ptr [esi]
mov dl, byte ptr [edi]
cmp cl, dl
jne not_equal
mov cl, byte ptr [esi+1]
mov dl, byte ptr [edi+1]
cmp cl, dl
jne not_equal
add edi, 2
add esi, 2
sub eax, 2
jne main_loop ;這裡用了jne而不是jmp,太好了,一舉兩得
done:
pop edi
pop esi
retnull:
ret
dwords:
mov ecx, eax ;eax中儲存著字串長度
and eax, 3
shr ecx, 2 ;右移兩位,等於除以字串長度除以4, 現在ecx == 100(64h)
je tail_loop_start ;迴圈移位指令不影響除CF,OF以外的其它位,
;故這裡是判斷eax是否是4的倍數,若是(eax & 3 == 0, zf = 1)則跳
repe cmps dword ptr [esi], dword ptr [edi]
;這是一條經典程式碼,cmps為串比較指令
;repe/repz:計數相等重複串操作指令功能:
; <1>如果cx==0或zf==0(比較的兩數不等),則退出repe/repz
; <2>cx = cx - 1
; <3>執行其後的串指令
; <4>重複<1>--<3>
; 對於cmpsr的功能:
; 程式碼中是用dword ptr修飾過,所以是雙字比較
; 比較完後:edi = edi +/- 4, esi = esi +/- 4
; 到底是加還是減,看df位的設定
je tail_loop_start ;看repe是如何退出的,到底是全部比較完
;了都相等退出(則zf==1,je成功跳轉),還是
;比較的中途遇到不相等的退出
mov ecx, dword ptr [esi-4] ;已知是不相等退出的了,現在看具體的大小關係
mov edx, dword ptr [edi-4] ;所以後退4個位元組比較
cmp cl, dl
jne diffrence_in_tail
cmp ch, dh
jne diffrence_in_tail
shr ecx, 10h
shr edx, 10h
cmp cl, dl
jne diffrence_in_tail
cmp ch, dh
diffrence_in_tail:
mov eax, 0
not_equal:
sbb eax, eax
pop edi
sbb eax, 0fffffffh
pop esi
ret
tail_loop_start:
test eax, eax ;現在eax是原字串長度模4後的零頭
je done ;看看eax是否為0,是則說明比較完成,eax中是零,則返回值是0,相等
mov edx, dword ptr [esi]
mov ecx, dword ptr [edi]
cmp dl, cl
jne diffrence_in_tail
dec eax
je tail_done
cmp dh, ch
jne diffrence_in_tail
dec eax
je tail_done
and ecx, 0ff0000h
and edx, 0ff0000h
cmp edx, ecx
jne diffrence_in_tail
dec eax
tail_doen:
pop edi
pop esi
ret
至此,也明白了為什麼這兩個函式會有效率的差別,strcmp比較的字串,而memcmp比較的是記憶體塊,strcmp需要時刻檢查是否遇到了字串結束的 /0 字元,而memcmp則完全不用擔心這個問題。另一個區別是
strcmp在比較四位元組是逐位元組比較,而memcmp是用了字串比較指令,感覺用字串比較指令比用逐位元組比較好,不知道strcmp為什麼比較四位元組時不用。感覺memcmp倒是可以用來實現strncmp函式的功能。
遺留的問題有記憶體位元組對齊的問題,以及兩處mov edi,edi和一處nop指令的問題。交給以後吧。
intel/strcmp.asm:
mov edx, dword ptr [esp + 4] ;取第二個引數地址
mov ecx, dword ptr [esp + 8] ;取第一個引數地址
test edx, 3 ;edx是第二個引數的地址,這裡即檢驗該地址是否是4的倍數。
;因為如果edx&3!=0,則其最低兩位不為1,所以為4的倍數。這裡有個記憶體地址對齊的問題。
jne dopartial ;如果地址不是4的倍數,就跳到dopartial去處理。
dodwords:
mov eax, dword ptr [edx]
cmp al, byte ptr [ecx]
jne donene
or al, al ;看看字串是否結束,這就是strcmp之所以比memcmp慢的地方了。
je doneeq ;如果 al==0,則比較結束
cmp ah, byte ptr [ecx + 1]
jne donene
or ah, ah
je doneeq
shr eax, 10h ;右移16位
cmp al, byte ptr [ecx + 2]
jne donene
or al, al
je doneeq
cmp ah, byte ptr [ecx + 3]
jne donene
or ah, ah
je doneeq
add ecx, 4
add edx, 4
or ah, ah
jne dodwords
move edi, edi ;這裡一直大惑不解,不明白為什麼這裡要多出這兩個位元組來
doneeq:
xor eax, eax ;比較結果是相等,返回值為0
ret
nop ;這裡也一直大惑不解,不明白這裡為什麼要插入一條空指令,感覺和上面的mov edi, edi應該是同一個原因。
donene:
sbb eax, eax ;比較結果不相等,這裡也很經典,使用帶借位減法,eax = eax - eax -cf。
shl eax, 1 ;若不相等處是大於,則 cf == 0,eax == 0,下面加1後eax==1,返回。
inc eax ;若不相等處是小於,則 cf == 1,那麼 sbb後eax==-1,補碼為0xFFFFFFFF,左移再加1還是-1
ret
mov edi, edi ;這裡又出來了,不知道為什麼要這麼做,痛苦。
dopartial:
test edx, 1
je doword ;同樣,與1與如果為0,則地址是2的倍數,跳到doword去執行。
mov al, byte ptr [edx]
inc edx
cmp al, byte ptr [ecx]
jne donene
inc ecx
or al, al
je doneeq
test edx, 2
je dodwords
dowords:
mov ax, word ptr [edx]
add edx, 2
cmp al, byte ptr [ecx]
jne donene
or al, al
je doneeq
cmp ah, byte ptr[ecx + 1]
jne donene
or ah, ah
je doneeq
add ecx, 2
jmp dodwords
intel/memcmp.asm
memcmp函式程式碼:
引數堆疊:
offset str1 --- ebp - 4 ------------當前棧頂
offset str2 --- ebp - 8
eax (strlen(str1)的返回值)
memcmp:
mov eax, dword ptr [esp + 0ch] ;得到memcmp的第三個引數:要比較的個數
test eax, eax ; 看要比較的位元組數是否為0
je retnull ;如果要比較的位元組數為0則直接返回
mov edx, dword ptr [esp + 4] ;得到memcmp的第一個引數,即offset str1
push esi
push edi ;儲存暫存器值
mov esi, edx ;源字串地址,即offset str1
mov edi, dword ptr [esp + 10h] ;不知何意,
or edx, edi
and edx, 3 ;根據strcmp的分析,這裡依然是判斷edx地址是不是4的倍數
je dwords ;地址是4的倍數,則跳到dwords去處理
test eax, 1 ;eax中存的是字串長度。如果地址不是4的倍數,那麼看要比較的位元組數是不是2的倍數。
je mainloop ;如果要比較的記憶體位元組數是2的倍數,則轉向mainloop。
mov cl, byte ptr [esi] ;否則eax==1,比較最後一個位元組
cmp cl, byte ptr [edi]
jne not_equal
inc esi
inc edi
dec eax
je done
main_loop:
mov cl, byte ptr [esi]
mov dl, byte ptr [edi]
cmp cl, dl
jne not_equal
mov cl, byte ptr [esi+1]
mov dl, byte ptr [edi+1]
cmp cl, dl
jne not_equal
add edi, 2
add esi, 2
sub eax, 2
jne main_loop ;這裡用了jne而不是jmp,太好了,一舉兩得
done:
pop edi
pop esi
retnull:
ret
dwords:
mov ecx, eax ;eax中儲存著字串長度
and eax, 3
shr ecx, 2 ;右移兩位,等於除以字串長度除以4, 現在ecx == 100(64h)
je tail_loop_start ;迴圈移位指令不影響除CF,OF以外的其它位,
;故這裡是判斷eax是否是4的倍數,若是(eax & 3 == 0, zf = 1)則跳
repe cmps dword ptr [esi], dword ptr [edi]
;這是一條經典程式碼,cmps為串比較指令
;repe/repz:計數相等重複串操作指令功能:
; <1>如果cx==0或zf==0(比較的兩數不等),則退出repe/repz
; <2>cx = cx - 1
; <3>執行其後的串指令
; <4>重複<1>--<3>
; 對於cmpsr的功能:
; 程式碼中是用dword ptr修飾過,所以是雙字比較
; 比較完後:edi = edi +/- 4, esi = esi +/- 4
; 到底是加還是減,看df位的設定
je tail_loop_start ;看repe是如何退出的,到底是全部比較完
;了都相等退出(則zf==1,je成功跳轉),還是
;比較的中途遇到不相等的退出
mov ecx, dword ptr [esi-4] ;已知是不相等退出的了,現在看具體的大小關係
mov edx, dword ptr [edi-4] ;所以後退4個位元組比較
cmp cl, dl
jne diffrence_in_tail
cmp ch, dh
jne diffrence_in_tail
shr ecx, 10h
shr edx, 10h
cmp cl, dl
jne diffrence_in_tail
cmp ch, dh
diffrence_in_tail:
mov eax, 0
not_equal:
sbb eax, eax
pop edi
sbb eax, 0fffffffh
pop esi
ret
tail_loop_start:
test eax, eax ;現在eax是原字串長度模4後的零頭
je done ;看看eax是否為0,是則說明比較完成,eax中是零,則返回值是0,相等
mov edx, dword ptr [esi]
mov ecx, dword ptr [edi]
cmp dl, cl
jne diffrence_in_tail
dec eax
je tail_done
cmp dh, ch
jne diffrence_in_tail
dec eax
je tail_done
and ecx, 0ff0000h
and edx, 0ff0000h
cmp edx, ecx
jne diffrence_in_tail
dec eax
tail_doen:
pop edi
pop esi
ret
至此,也明白了為什麼這兩個函式會有效率的差別,strcmp比較的字串,而memcmp比較的是記憶體塊,strcmp需要時刻檢查是否遇到了字串結束的 /0 字元,而memcmp則完全不用擔心這個問題。另一個區別是
strcmp在比較四位元組是逐位元組比較,而memcmp是用了字串比較指令,感覺用字串比較指令比用逐位元組比較好,不知道strcmp為什麼比較四位元組時不用。感覺memcmp倒是可以用來實現strncmp函式的功能。
遺留的問題有記憶體位元組對齊的問題,以及兩處mov edi,edi和一處nop指令的問題。交給以後吧。
再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智慧的隊伍中來!https://blog.csdn.net/jiangjunshow