整理自陳莉君(翻譯深刻理解Linux內核的做者)老師文章。編程
阿里2015筆試中有這樣一道題目:windows
在一臺主流配置的PC上,調用f(35)所須要的時間大概是( )。函數
int f(int x){性能
int s = 0;測試
while(x++ >0) s+= f(x);spa
return max(s,1);翻譯
}3d
A.幾毫秒 B.幾秒 C.幾分鐘 D.幾小時指針
本題涉及到的知識點包括數據的表示和運算、時間複雜度。考查考生對帶符號整數的表示、遞歸調用的執行過程、計算機系統性能、虛擬存儲器、C語言語句等相關知識的理解和運用能力。blog
數學上的分析推導結果與計算機系統中的執行結果是有差別的。例如,在數學中一個數能夠無限大,但在計算機中受表示位數的限制,數的值是有限的。用數學分析的方法,本題的遞歸是能夠終止的,但受存儲容量的限制,在計算機中遞歸調用時會有棧溢出的問題,致使程序不能正常執行結束。相似的問題還有不少,這是平時編程時須要注意的。
假設題目中的函數用C語言書寫,要分析調用f(35)所需的時間,就得分析代碼執行中循環執行次數和遞歸調用次數等,下面深刻剖析f(35)執行過程當中存在的問題。
注意:如下231爲2的31次方。
(1)程序是否會終止?
調用f(35)時,入口參數x=35。從數學的角度理解while中的判斷表達式「x++ >0」,會認爲x在增量後永遠大於0,這是一個永真式,從而作出錯誤結論:程序死循環。在計算機中數值是有範圍的,int型數據用補碼錶示,佔4個字節,能表示的最大正數是231-1 = 7FFF FFFFH。231的機器數是8000 0000H,其值爲int型,能表示的最小負數-2147483648,所以當x = 8000 0000H 時,x > 0的值爲假,程序退出while循環,所以,若不考慮棧溢出,則程序能執行結束。
(2)使遞歸終止的最大x值是多少?
while(x++ >0)語句在Microsoft VC中的機器代碼以下,該語句的執行過程是:先把x的值分別保存到EDX和EAX寄存器;而後對EAX寄存器內容加1,以實現x = x+1操做;最後再用EDX的內容(x的舊值)進行x>0的條件判斷。
mov edx, dword ptr [ebp+8]
mov eax,dword ptr [ebp+8]
add eax, 1
mov dword ptr [ebp+8], eax
test edx, edx
jle f+77h (00401097)
所以,當調用f(231-1)時,x = 231-1=7FFF FFFFH,先執行x=7FFF FFFFH+1 = 8000 0000H=231,而後,用舊的x=7FFF FFFFH與0比較,比較結果爲真,故執行while循環體,在循環體中調用f(231)。
調用f(231)時,x爲231 = 8000 0000H,其真值爲負數,所以,與0比較的結果爲假,故跳出while循環體,程序結束。
綜上所述,使遞歸終止的最大x值是231,即執行f(231)時結束遞歸調用。
(3)函數f(x)的遞歸調用狀況如何?
f(x)是一個遞歸調用過程,而且遞歸調用在循環體內,所以調用關係較複雜。圖1顯示了f(231-4)執行中的遞歸調用狀況。
圖1 f(231-4)執行中的遞歸調用狀況
在f(x)執行過程當中,把執行f(x)過程體的總次數記爲f(x)執行次數,把一次遞歸調用的最大次數記爲f(x)遞歸深度。表1給出了x爲不一樣值時,執行f(x)的次數和遞歸深度。這兩個參數顯示了f(x)函數的執行過程。
表1 x爲不一樣值時,f(x)執行的次數和遞歸調用深度
f(x)執行次數 |
遞歸深度 |
|
f(231) |
1 |
1 |
f(231-1) |
2 |
2 |
f(231-2) |
4 |
3 |
f(231-3) |
8 |
4 |
f(231-4) |
16 |
5 |
…… |
…… |
…… |
f(231-n) |
2n |
n+1 |
f(35)=f(231-2147483613) |
22147483613 |
2147483614 |
(4) 遞歸調用過程的執行狀況
系統會給每個用戶進程分配存放代碼和數據的用戶空間,用戶空間中的棧區用來存放程序運行時過程調用的參數、返回地址、過程局部變量等。隨着程序的執行,棧區不斷動態地從高地址向低地址增加或向反方向減退。
用戶棧由若干個棧幀組成,每一個過程對應一個棧幀,幀指針寄存器EBP指定一個棧幀的起始地址,棧指針寄存器ESP指向棧頂,當前棧幀的範圍在EBP和ESP指向的區域之間。過程執行時,因爲不斷有數據入棧,因此棧指針ESP會動態移動,而幀指針EBP固定不變。在一個過程內對棧中信息的訪問大多經過幀指針EBP進行。
IA-32規定,寄存器EAX、ECX和EDX是調用者保存寄存器,EBX、ESI、EDI寄存器是被調用者保存寄存器。若過程P調用過程Q時,P在須要時先在本身的棧區保存EAX、ECX和EDX、入口參數和返回地址,接着跳轉到Q執行。Q在本身的棧幀中先保存P的EBP值,並設置EBP指向當前Q棧幀的棧低,根據須要保存EBX、ESI、EDI,再在棧中給Q的局部變量分配空間。
在遞歸調用執行中,每一個遞歸調用過程都有一個棧幀。棧幀中可能包含如圖2所示的信息。
…… |
調用者的EBP值 |
調用者的EBX、ESI、EDI |
過程局部變量 |
本身的EAX、ECX和EDX |
入口參數n …… 入口參數1 |
返回地址 |
圖2 一次遞歸調用中的棧幀
圖3顯示了在windows系統中f(x)函數調用時的部分機器指令。能夠看出f(x)的棧幀至少有84B。系統分配給一個進程的用戶棧只有有限的空間,所以,遞歸調用的次數是有限的。f(35)的遞歸深度是2147483614,即至少須要2147483614×84字節,即大於170GB的棧幀空間。在32位系統中,最大虛擬地址空間僅有4GB,用戶棧只是其中的一部分,因此f(35)在執行過程當中會出現棧溢出的現象。
圖3 f(x)函數調用時的部分機器指令
(5) f(35)在32位系統中的實際執行狀況
假設在Intel x86+windows+VC+C語言環境中執行f(35)。VC中默認分配棧的大小是1MB,雖然用戶能夠調整棧大小,但棧的容量是有限的。按2MB的棧空間、棧大小按80字節計算:2MB÷80B≈26214,所以f(x)遞歸調用的次數不會超過26214-1=26213次。從圖4.9中能夠看出,棧溢出時,f(x)函數體最多執行26213次。棧溢出時每一個f(x)函數體只在while語句中執行,假設每一個f(x)函數體執行100條指令,即便指令平均CPI爲3,時鐘頻率爲2.4GHz,f(35)的執行時間也只有26213×100×3÷2.4GHz ≈3.2 ms左右時間。
對f(35)的執行作了測試,在棧大小是1MB時,遞歸調用11244次後棧溢出;在棧設置爲2MB時,遞歸調用22642次後棧溢出,顯然運行時間只有幾毫秒。在Microsoft VisualStudio 2012環境中運行,出現如圖4所示結果,代表出現了棧溢出(Stack overflow)。
綜上所述,答案爲A。
以上整理自陳莉君老師的文章。對於其中不容易理解的圖1 f(231-4)執行中的遞歸調用狀況這裏給出註釋:
對於遞歸在循環體內咱們能夠把循環展開,這種狀況是先循環再遞歸:
while(x++ >0) s+= f(x);
等價寫法(當x=0x80000000-2):
if(0x80000000 -2 >0) { // if(x >0)
0x80000000-1; //x++
s+=f(0x80000000-1); //f(x)
}
if(0x80000000-1 > 0) {
0x80000000;
s+=f(0x80000000);
}