1. 程式人生 > 其它 >函式式,F#都做了哪些優化?

函式式,F#都做了哪些優化?

非函式式語言中使用函式式風格的缺點:

函式式的優點,想必大家都已經非常瞭解了。我們來看看,一般語言使用函式式風格可能帶來的問題:

  1. 變數預設是可變的。為了實現不可變性,開發者只能人為的規範不去改變變數的值,沒有明確的變數修改提示,容易因失誤改變變數的值。
  2. 為了實現不可變性,往往需要更多的變數來儲存中間結果,產生大量的資源開銷。又由於垃圾回收往往不是即時的,隨著資料規模上升,可能會出現資源消耗問題。
  3. 使用遞迴來替代迴圈,如果語言沒有對尾遞迴做優化,每次遞迴呼叫會變成真實的函式呼叫,影響效能。並且如果遞迴函式中有大量臨時變數,還可能導致堆疊空間被快速侵蝕,引起效能和資源佔用雙重問題。
  4. 為了實現高階函式,需要支援函式作為引數。在函式內呼叫函式如果不能做到內聯,也會產生不必要的效能開銷。如果函式呼叫在迴圈中,這些開銷會累積從而影響效能。
  5. 柯里化的函式,需要將每個引數都包裝成一個獨立的函式,然後利用閉包進行函式的巢狀。這導致了過度包裝,呼叫時則是層層解包,影響效能。
  6. 函式式的序列處理,如果不支援lazy模式,則會產生許多不必要的額外處理。這在結合短路模式時尤為明顯。例如呼叫了map函式,再呼叫exists函式,map會先完成完整遍歷得到全新的序列,然後交給exists做短路判斷,影響效能。

F#如何解決上述問題

值預設不可變

為了解決第一個缺點,F#對值的可變性做了強制限制。

F#的值預設是不可變的。雖然你仍然可以使用mutable關鍵詞將其設定為變數,但即使是可變的變數也採用了不同的賦值符號(<-)區別於繫結符號(=)。這樣可以很大程度上減少犯錯。

看如下例子:

let a = 1
a <- 2  //這將導致編譯器報錯,因為a不可變
a = 2  //只在let繫結中=才是繫結值,其餘位置=是比較符,相當於==。因此此處是比較a與2的值,該表示式返回bool型別。很難用錯。

尾遞迴優化和引數變數化

為了解決2和3,F#針對尾遞迴做了優化,並且將遞迴的引數編譯為變數,遞迴的呼叫實際上被編譯為了迴圈體內變數的更替。

尾遞迴函式的特點是:不儲存任何中間值,下層遞迴結果不參與本層函式的計算,在本層函式的某些分支的終點(最後一句)呼叫遞迴。

判斷一個遞迴函式是不是尾遞迴,有十分簡單的規則:

看遞迴是不是會在最後一層返回該函式的某個引數(或其運算)作為結果,並且此次返回會一路返回到最上層,不會參與到中間任何一層的運算中。

尾遞迴,很容易總結出以下性質:

  1. 尾遞迴過程中的所有變化值都在引數中。
  2. 尾遞迴最重要返回的值也在引數中
  3. 尾遞迴除了引數之外沒有任何新增的變數需要在進入下一層遞迴時保留。

因此,尾遞迴可以簡單的優化成while迴圈。而出口點則在所有沒有呼叫遞迴的分支上(相當於break)。所有有遞迴呼叫的分支,遞迴呼叫可以翻譯為對引數對應的幾個變數進行賦值,然後再次進入迴圈。

比如下面的例子:

//求1..i的和
let rec sum s i =  //s儲存的是和, s和i被編譯為變數
	match i with
	| 0 -> s //終止條件,相當於break,最終返回s
	| _ -> 
		sum (s + i) (i - 1)  //遞迴呼叫,被編譯為s <- s + i,i <- i - 1,然後繼續迴圈。當然為了考慮變數變更後值的問題,實際上還引入了臨時變數,這裡不展開了,可以看我上一篇文章。

sum 0 100  //計算從1到100的和

上述程式碼,差不多會被優化成:(示意,並不精確)

int s = 0;
int i = 100;
while (true)
{
    if (i == 0) break;
    s += i;
    i--;
}

從而既解決了遞迴巢狀的問題,又解決了臨時變數的建立和釋放問題。

FSharpFun<>

為了解決4和5:

F#中定義的函式是自動柯里化的。對於大於一個引數的柯里化的函式定義,F#採用了專門的泛型類來描述,那就是FSharpFun。同樣如果函式作為引數,它的型別也是FSharpFun。其內部針對柯里化做出了優化,使得柯里化的函式可以用過InvokeFast方法進行優化呼叫。如果該函式呼叫存在優化版本,則相當於只調用了一次Invoke,從而避免了Invoke之後再Invoke的連鎖呼叫。FSharpFun通過泛型來定義每個引數和返回值的型別。從而解決了柯里化的過度包裝和效能消耗問題。這麼做的副作用是隻能支援有限個數的柯里化引數的函式,不過這不是什麼大問題。

在F#中,函式可以被顯式的宣告為inline,從而可以內聯到其他函式中。在F#6.0中,還引入了InlineIfLambda特性,來標記引數,使得lambda描述的匿名方法可以自動內聯。不過該特性只能用在類的方法上,而不能用於一般的let繫結的函式。

Seq和Lazy

解決6其實是手到擒來的事情。大家都知道.Net架構有迭代器,而迭代器就是Lazy的,具有延時執行的特點。F#中針對序列處理的模組Seq,就等同於.Net中的IEnumerable。而Seq模組的函式,其本質就是迭代器。因而Seq的函式大都具有lazy的特性。

這裡最好直接拿一個樣例來舉例,該例子中,takeWhile是取元素,直到不滿足條件,exists是遍歷直到出現第一個滿足條件的。兩個函式都是短路的。我們看看F#如何執行,會不會正確的短路掉。

//求n以內的質數
let primes n =
    let rec loop list i =
        match i with
        | _ when i > n ->
            list
        | _ ->
            let h = float i |> sqrt |> int
            list
            |> Seq.takeWhile (fun j -> j <= h)
            |> Seq.exists (fun j -> i % j = 0)
            |> function
                | true -> loop list (i + 1)
                | false -> loop (list @ [i]) (i + 1)
    loop [] 2

printfn "%A" (primes 100)

上面的程式碼列印了100以內的質數。其中list儲存了遞迴至今的質數,i是當前測試的數。h是i的平方根。針對i要檢測list中是否有可以整除的元素,遍歷list的時候在元素大於h時應當終止本輪遍歷並返回false(無整除元素),遍歷到任何可以整除的元素時應當返回true。這兩個短路條件返回的一個是true一個是false,因而無法寫進同一個短路函式中。takeWhile和exists就作為兩個短路函式先後呼叫了。

我們使用Seq的函式,實現lazy的執行。為了展示具體執行過程,我們新增一些列印內容。修改程式碼如下:

let primes n =
    let rec loop list i =
        match i with
        | _ when i > n ->
            list
        | _ ->
            printf "lv.%d --- " i
            let h = float i |> sqrt |> int
            list
            |> Seq.takeWhile (fun j -> printf "A-%d; " j; j <= h)
            |> Seq.exists (fun j -> printf "B-%d; " j; i % j = 0)
            |> function
                | true -> printfn ""; loop list (i + 1)
                | false -> printfn ""; loop (list @ [i]) (i + 1)
    loop [] 2

printfn "%A" (primes 100)

可以看出,僅僅增加了一下列印輸出而已。我們看看執行結果:

lv.2 ---
lv.3 --- A-2;
lv.4 --- A-2; B-2;
lv.5 --- A-2; B-2; A-3;
lv.6 --- A-2; B-2;
lv.7 --- A-2; B-2; A-3;
lv.8 --- A-2; B-2;
lv.9 --- A-2; B-2; A-3; B-3;
lv.10 --- A-2; B-2;
lv.11 --- A-2; B-2; A-3; B-3; A-5;
lv.12 --- A-2; B-2;
lv.13 --- A-2; B-2; A-3; B-3; A-5;
lv.14 --- A-2; B-2;
lv.15 --- A-2; B-2; A-3; B-3;
lv.16 --- A-2; B-2;
lv.17 --- A-2; B-2; A-3; B-3; A-5;
lv.18 --- A-2; B-2;
lv.19 --- A-2; B-2; A-3; B-3; A-5;
lv.20 --- A-2; B-2;
lv.21 --- A-2; B-2; A-3; B-3;
lv.22 --- A-2; B-2;
lv.23 --- A-2; B-2; A-3; B-3; A-5;
lv.24 --- A-2; B-2;
lv.25 --- A-2; B-2; A-3; B-3; A-5; B-5;
lv.26 --- A-2; B-2;
lv.27 --- A-2; B-2; A-3; B-3;
lv.28 --- A-2; B-2;
lv.29 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7;
lv.30 --- A-2; B-2;
lv.31 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7;
lv.32 --- A-2; B-2;
lv.33 --- A-2; B-2; A-3; B-3;
lv.34 --- A-2; B-2;
lv.35 --- A-2; B-2; A-3; B-3; A-5; B-5;
lv.36 --- A-2; B-2;
lv.37 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7;
lv.38 --- A-2; B-2;
lv.39 --- A-2; B-2; A-3; B-3;
lv.40 --- A-2; B-2;
lv.41 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7;
lv.42 --- A-2; B-2;
lv.43 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7;
lv.44 --- A-2; B-2;
lv.45 --- A-2; B-2; A-3; B-3;
lv.46 --- A-2; B-2;
lv.47 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7;
lv.48 --- A-2; B-2;
lv.49 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7;
lv.50 --- A-2; B-2;
lv.51 --- A-2; B-2; A-3; B-3;
lv.52 --- A-2; B-2;
lv.53 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.54 --- A-2; B-2;
lv.55 --- A-2; B-2; A-3; B-3; A-5; B-5;
lv.56 --- A-2; B-2;
lv.57 --- A-2; B-2; A-3; B-3;
lv.58 --- A-2; B-2;
lv.59 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.60 --- A-2; B-2;
lv.61 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.62 --- A-2; B-2;
lv.63 --- A-2; B-2; A-3; B-3;
lv.64 --- A-2; B-2;
lv.65 --- A-2; B-2; A-3; B-3; A-5; B-5;
lv.66 --- A-2; B-2;
lv.67 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.68 --- A-2; B-2;
lv.69 --- A-2; B-2; A-3; B-3;
lv.70 --- A-2; B-2;
lv.71 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.72 --- A-2; B-2;
lv.73 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.74 --- A-2; B-2;
lv.75 --- A-2; B-2; A-3; B-3;
lv.76 --- A-2; B-2;
lv.77 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7;
lv.78 --- A-2; B-2;
lv.79 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.80 --- A-2; B-2;
lv.81 --- A-2; B-2; A-3; B-3;
lv.82 --- A-2; B-2;
lv.83 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.84 --- A-2; B-2;
lv.85 --- A-2; B-2; A-3; B-3; A-5; B-5;
lv.86 --- A-2; B-2;
lv.87 --- A-2; B-2; A-3; B-3;
lv.88 --- A-2; B-2;
lv.89 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.90 --- A-2; B-2;
lv.91 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7;
lv.92 --- A-2; B-2;
lv.93 --- A-2; B-2; A-3; B-3;
lv.94 --- A-2; B-2;
lv.95 --- A-2; B-2; A-3; B-3; A-5; B-5;
lv.96 --- A-2; B-2;
lv.97 --- A-2; B-2; A-3; B-3; A-5; B-5; A-7; B-7; A-11;
lv.98 --- A-2; B-2;
lv.99 --- A-2; B-2; A-3; B-3;
lv.100 --- A-2; B-2;
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59; 61; 67; 71; 73; 79; 83; 89; 97]

這裡A表示進入了takeWhile的條件函式,B表示進入了exists的條件函式。由於是延遲執行的,所以我們看到A和B成功被合併到一次迭代中,A、B交替執行,而不是一股腦執行完A再一股腦執行B。從而只要A或者B有一個達到了短路條件,整個過程都會短路返回。

例如:

  • 對於4,我們遍歷已存在的質數列表[2; 3],遍歷2時,進入takeWhile的條件,發現它不小於4的平方根,不滿足短路條件;繼而進入exists,發現2可以整除4,滿足短路條件,於是返回false,不再遍歷剩下的元素3。
  • 對於5,我們遍歷已存在的質數列表[2; 3],進入A發現滿足2<根號5,進入B發現2不能整除5;繼續遍歷3,發現3已經不滿足小於根號5了,短路觸發,從而B不再執行,返回true。

利用上面特性,F#也成功解決了序列處理的短路的問題。