探索c 之尾遞迴編譯器優化

2021-12-29 21:02:06 字數 1591 閱讀 9717

遞迴運用

乙個函式直接或間接的呼叫自身,這個函式即可叫做遞迴函式。

遞迴主要功能是把問題轉換成較小規模的子問題,以子問題的解去逐漸逼近最終結果。

遞迴最重要的是邊界條件,這個邊界是整個遞迴的終止條件。  

static int recfact(int x)

recfact(10);

上面是個經典階乘函式的實現。這裡分2步:

轉換,把10的階乘轉化成10*9!,10(9*8!)....每次轉換規模就變的更小。

逼近,轉換到最小規模時0!,求解1。開始逆向合併逐漸逼近到10,得出解。

這裡的x==0就是我們的邊界條件(即終止條件),也有的依賴外部變數為邊界。

如果乙個遞迴函式沒有邊界,也就無法停止(無限迴圈至記憶體溢位),當然這樣也沒什麼意義。

recfact呼叫堆疊:  

常見使用場景:

階乘/斐波那契數列/漢諾塔

遍歷硬碟檔案

innerexceptions異常撲捉(exception.innerexception==null)

尾遞迴優化

當邊界不明確的時候,遞迴就很容易出現溢位問題。

在階乘過程中,堆疊需要儲存每次(recfact)呼叫的返回位址及當時所有的區域性變數狀態,期間堆疊空間是無法釋放的(即容易出現溢位)。

為了優化堆疊占用問題,從而提出尾遞迴優化的辦法。  

static void recurse(int x)

recurse(0);

上面這個遞迴和階乘那個區別在於沒有返回值,也就是說堆疊可以不用儲存上一次的返回位址/狀態值,這就是尾遞迴優化的思想。

尾遞迴可以解決遞迴過深而引起的溢位問題,因為遞迴期間堆疊是可以釋放/再利用的。

編譯器優化

尾遞迴優化,看起來是蠻美好的,但在net中卻有點亂糟糟的感覺。

net在c#語言中是jit編譯成彙編時進行優化的。

net在il上,有個特殊指令tail去實現尾遞迴優化的(f#中)。

我們執行 recurse(0)(x==1000000) 得出如下結論:

c#/64位/release是有jit編譯器進行尾遞迴優化的(非c#編譯器優化)。  

c#/32位或c#/debug模式中jit是不進行優化的。  

f#在優化尾遞迴也分2種情況:

1、 簡單的尾遞迴優化成while迴圈,如下:

let rec recurse(x) =

if (x = 1000) then true

else recurse(x + 1)

(方便演示c#呈現)優化成:  

public static bool recurse(int x)

return true;

}2、 複雜的尾遞迴,f#編譯器會生成il指令tail進行優化,如下:

let recurse2 a cont = cont (a + 1)  

優化成:  

如何定義複雜的尾遞迴呢?通常是後繼傳遞模式(cps)。

f#中在debug模式下,需要在編譯時配置:  

總結在c#語言(過程式/物件導向程式設計思想)中,優先考慮的是迴圈,而不是遞迴/尾遞迴。 但在函式式程式設計思想當中,遞迴/尾遞迴使用則是主流用法,就像在c#使用迴圈一樣。

尾遞迴與編譯器優化

在計算機中,程式執行中的函式呼叫是借助棧實現的 每當進入乙個新的函式呼叫,棧就會增加一層棧幀,每當函式返回,棧就會減少一層棧幀。這個棧的大小是有限的 貌似是1m或者2m 所以在執行遞迴的過程中遞迴的次數是有限度的,超過某個不是很大的值就會爆棧 棧溢位 以求解fabonacci問題為例 使用遞迴的方式...

JavaScript尾遞迴優化探索

function f function fibonacci n 此函式沒有進行任何的優化,當我們在控制台去執行此函式的時候,fibonacci 40 就已經出現了明顯的響應慢的問題,fibonacci 50 的時候瀏覽器卡死。2.優化function fibonacci n,ac1,ac2 func...

C 編譯器優化之RVO

寫這篇文章純屬意外收穫.先看一段 在這段 中定義了建構函式,拷貝建構函式,移動建構函式,賦值構造,移動賦值構造,以及析構函式.include using namespace std classaa const a a a a a a operator const a a else return th...