除法終於整理結束了,這篇開始整理取模運算,對於取模運算來說,個別的情況還是需要使用除法的,所以除法運算的基礎還是得牢靠。
取模也叫取餘,表達爲 a % b。下面需要對b的情況進行區分來分析
1. 變量
2. 常量
2.1 無符號
2.1.1 2的冪
2.1.2 非2的冪
2.2 有符號
2.1.1 2的冪
2.1.1.1 正數
2.1.1.2 負數
2.1.2 非2的冪
首先,這裏和除法一樣,對於爲變量時候,都是沒有優化的,所以直接會使用div/idiv指令,可以據此判斷出有無符號,取模的結果在edx中。
下面主要探討常量的情況,先來補充一點基礎知識,先看如下代碼
printf("%d\r\n",8%3);
printf("%d\r\n",8%-3);
printf("%d\r\n",-8%3);
printf("%d\r\n",-8%-3);
上面的運算結果
2
2
-2
-2
如果對於結果有誤的可以看下面的公式
設被除數爲a,除數爲b,商爲q,餘數爲r
a / b = q ..餘.. r
=> a = q*b + r
=> r = a - q*b; //求餘公式
也就是說餘數等於被除數減去商乘以除數,這裏應該比較好理解。這裏對於某些情況中,取模也會使用該公式進行優化。
我們挑最後第三個來套一下公式看是否正確,剩餘幾個同理可求得
-8%3
已知除法向零取整,-8/-3可得商q=-2
r = a - q*b
= -8 - -2*3
= -8 + 6
= -2
OK,其實對於上面的結果來說,其實我們還可以得出一個規律,就是餘數的符號跟着被除數走,這裏的應用具體後面會提。
好了,下面來看2.1.1情況,也就是無符號中2的冪情況,對於無符號的情況,debug都不優化,主要來看release中的情況
int main(unsigned int argc, char* argv[])
{
printf("%d",argc%8);
return 0;
}
對應彙編代碼
.text:00401043 mov eax, [ebp+argc]
.text:00401046 and eax, 7
.text:00401049 push eax
可以發現,其取模的結果就用了一個and 7,下面我們來分析一下
假設一個數爲2的冪(2^n),那麼其二進制的後n位肯定是0
這裏同理,2的倍數後1位肯定是0,4的倍數後2位肯定是0 ...
爲什麼呢?
2的二進制爲10,那麼此後對於2的冪而言,都是*2*2*2..
那麼每乘一個2,其實相當於左移一位(右邊補0)
4 = 2 * 2 = 10 << 1 = 100
8 = 4 * 2 = 100 << 1 = 1000
7的二進制 111
8的二進制 1000
由二進制可發現,對於and操作,也就是對後n位進行取值的操作
爲什麼呢?
對於2的冪而言,對其取餘範圍肯定是[0,2^n-1]
而2的冪二進制的其後n位肯定爲0,所以直接獲取後n位即爲餘數
假設對8取餘,那麼其餘數肯定是小於8的,也就是範圍 [0,7]
8的二進制1000,假設某一個數爲1xyz,那麼xyz必定爲餘數(xyz非0即1)
所以對於這種情況求餘,就是一個
and reg,2^n-1
下面來看2.1.2的情況,在vc6.0上測試是沒有優化的,而在高版本vs2015(自己使用的版本)中是有優化,其他版本未測,所以我們就來討論一下這個高版本中優化的情況吧,其實優化的原理很簡單,就是上面的求餘公式
int main(unsigned int argc, char* argv[])
{
printf("%d",argc%5);
return 0;
}
對應彙編代碼
.text:00401046 mov eax, 0CCCCCCCDh
.text:0040104B mul ecx
.text:0040104D shr edx, 2 ;計算商 q = edx
.text:00401050 lea eax, [edx+edx*4] ;q*5 = edx*5 此時的5就是除數5(常量)
.text:00401053 sub ecx, eax ;r = a - q*b = ecx - edx*5
.text:00401055 push ecx
對於上面的求餘公式而言,我們是要知道商的多少的,所以此時比然會涉及到除法運算,而除法運算是可以優化的,所以說上上面的彙編代碼中纔沒有看到一行的除法運算。如果除法都掌握的話,這裏就很簡單了。
下面看有符號數取模,還是先看除數爲2的冪的情況,首先對於debug和release的優化情況一致。然後又分正數和負數..這裏可能很多人就會有疑問了,上面不是說餘數的符號是跟着被除數走的麼,我們現在討論的都是除數的情況,爲什麼還要區分正負呢?
其實這裏區分正負並不是說影響其結果,而是這裏對於vs的低版本系列,微軟提供了兩套方案,雖然結果是一致的,只是對於正數和負數其產生的彙編代碼不一致。而在高版本系列中,好像都優化成正數的這種方案了,具體大家自己測試吧。
下面先來看正數的情況
int main(int argc, char* argv[])
{
printf("%d",argc%4);
return 0;
}
對應彙編代碼
.text:00401043 mov eax, [ebp+argc]
.text:00401046 and eax, 80000003h
.text:0040104B jns short loc_401052
.text:0040104D dec eax
.text:0040104E or eax, 0FFFFFFFCh
.text:00401051 inc eax
.text:00401052
.text:00401052 loc_401052: ; CODE XREF: _main+B↑j
.text:00401052 push eax
大致看一下上面的彙編代碼,可以發現有一個分支跳轉,對於分支跳轉,那麼我們就對於分支的情況獨立進行分析,這樣子就能看懂上面那段彙編代碼了。
對於jns指令來說,是不爲負則跳轉,那麼跳轉條件其實就是正數或者負數,先來看正數的情況分析
.text:00401043 mov eax, [ebp+argc]
;這裏的and很明顯是和之前無符號的目的是一樣的,只是保留符號位
;保留符號位是因爲前面說了餘數的符號位和被除數是一致的
;正數不影響,相當於無符號數的處理 and 2^n-1
.text:00401046 and eax, 10000000000000000000000000000011b ;轉化二進制
.text:0040104B jns short loc_401052
;省略負數的情況
.text:00401052
.text:00401052 loc_401052: ; CODE XREF: _main+B↑j
.text:00401052 push eax ;直接得到結果
OK,正數分支的情況其實就是和上面無符號的情況處理一致,下面分析一下負數分支
;首先,先不看第一行的第三行的代碼,這裏爲了一種特殊情況
;這裏爲什麼需要or呢?因爲前面說了餘數的符號是跟着被除數,那麼當餘數爲負數時,我們肯定得補回中間的1
;因爲只有補回中間的1,此時表達的纔是一個補碼
.text:0040104E or eax, 11111111111111111111111111111100b
;下面來看一下加上一三行代碼的情況,其實這裏爲的是一種特殊情況,就是當餘數爲0的時候
;當餘數爲零,dec eax,eax = 0xFFFFFFFF
;.text:0040104D dec eax
;這裏-1去or任何值最後肯定還是-1
.text:0040104E or eax, 11111111111111111111111111111100b
;此時加1後變回0
;.text:0040104D dec eax
;而對於其他正常情況來說,第一行代碼減一,第三行代碼加一,其實相當於結果沒有變化
上面應該解釋的比較清楚了。
下面來看一下2.1.1.2的情況,注意這裏與上面正數情況的結果是一致的,只是這裏算是微軟對於正數和負數都給了一套方案。在看這套方案之前,我們需要先來看一個取絕對值的案例。
int main(int argc, char* argv[])
{
printf("%d",abs(argc));
return 0;
}
對應的彙編代碼,這裏使用了無分支判斷
.text:00401043 mov eax, [ebp+argc]
.text:00401046 cdq ;if eax >= 0 edx = 0 else edx = 0xFFFFFFFF
.text:00401047 xor eax, edx;if eax >= 0 eax = eax else eax = ~eax
.text:00401049 sub eax, edx;if eax >= 0 eax -= 0 else eax -= -1
.text:0040104B push eax
對於上面的彙編代碼,只需分eax是否大於等於0,也就是當該數爲正數時,其值不發生變化,而其值爲負數時,其值做取反加一,這樣子就變成了取絕對值的操作。
int main(int argc, char* argv[])
{
printf("%d", argc % -4);
return 0;
}
對應的彙編代碼,這段彙編代碼其實對於上面正數的情況,優點其實是無分支
.text:00401043 mov eax, [ebp+argc]
.text:00401046 cdq
.text:00401047 xor eax, edx
.text:00401049 sub eax, edx ;這裏做了一個絕對值
.text:0040104B and eax, 3
.text:0040104E xor eax, edx
.text:00401050 sub eax, edx
.text:00401052 push eax
前四行代碼很明顯應該就是做絕對值的操作,那麼後面的代碼做什麼,我們先來分析一下,其實就是前面說的餘數的符號是跟着被除數走的原理。
r = |a| % |b|
if a < 0
r = -r
所以40104B這行代碼表示的就是按照無符號的方式獲取餘數(and 2^n-1),獲取餘數後,如果被除數爲負數的話,那麼我們是不是需要取個負(取反加一)呢,所以下面兩行代碼又使用了一個無分支
;此時edx之前已經保存了被除數的符號位
;如果爲正,那麼結果不變,如果爲負,那麼對其取反加一(求負)
.text:0040104E xor eax, edx
.text:00401050 sub eax, edx
好了,下面就剩下最後這2.1.2的情況了,這裏和2.1.2原理一樣,就當記錄過一下,低版本也是沒有優化的,高版本使用求餘公式進行優化
int main(int argc, char* argv[])
{
printf("%d", argc % 7);
return 0;
}
對應的彙編代碼分析
.text:00401044 mov esi, [ebp+argc]
.text:00401047 mov eax, 92492493h
.text:0040104C imul esi
.text:0040104E add edx, esi
.text:00401050 sar edx, 2
.text:00401053 mov ecx, edx
.text:00401055 shr ecx, 1Fh
.text:00401058 add ecx, edx ;上面一段都是除法的優化 ecx就是商的結果
.text:0040105A lea eax, ds:0[ecx*8] ;eax = ecx * 8
.text:00401061 sub eax, ecx ;eax - ecx = ecx*8-ecx =ecx*7
.text:00401063 sub esi, eax ;q = a - qb = esi - ecx*7