遞歸調用在循環體內: 把循環展開, 這種狀況是先循環再遞歸

整理自陳莉君(翻譯深刻理解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);

}

相關文章
相關標籤/搜索