由于函數式語言需要基礎架構支持,這樣不可避免地增加了學習匯編理論知識的成本。一流的詞法閉包只有在配合垃圾回收時才能良好地工作,因為它允許值越界。
函數式語言:過度分配
請注意自己的選擇。C在基準套件中扮演最小公分母,限制是可以實現的。如果在比較C語言與函數式編程語言上有一個基準,那么它肯定是一個非常簡單的程序。按理說這么簡單,它是沒有什么實際意義的。僅僅把C作為基準的話,那么在解決更復雜的問題上,它是沒有什么實際可行的解決方案。
這方面最明顯的例子就是并行性。如今,多核已成為一種主流,甚至連我的手機也是多核的。在C語言中,要想實現多核并行是相當困難的,但是在函數式編程語言中會卻容易實現(我比較喜歡F#)。其它例子還包括持久化數據結構,例如撤消緩沖區與純函數數據結構是微不足道的,但在命令式語言上,這卻是個非常大的工作量。
函數式語言看起來比C語言要慢,因為你僅看到基準代碼在C語言里很容易去編寫并且你永遠不會明白基準比任務更耐人尋味,從函數式語言到Excel。然而,目前你已經能正確的識別出函數式語言的最大瓶頸是什么:過度的分配率。為什么函數式語言分配會如此嚴重的原因就是它被拆分成歷史和內在的。
從歷史上看,Lisp實現已經做了50年的裝箱工作。這個特點也滲透到許多其它編程語言中,類似Lisp的中間件表示。這些年來,語言實現不斷地采取裝箱這種方式快速實現并發。在面向對象語言中,默認堆分配每個對象,即使明顯可以采用堆棧分配。提高效率的負擔是推到垃圾收集器并且在建設垃圾收集器性能上做一些努力,使它能夠達到或者最大可能接近堆棧分配,通常是使用bump-allocating托管實現。我認為,應該投入更多的精力來研究函數語言設計,減少裝箱和垃圾收集器設計,從而優化不同的需求。
分代式垃圾收集器
分代式垃圾收集器對語言來說,是很棒的,因為堆可以分配很多并且他們的速度也和堆棧分配差不多一樣快,但是會增加其他地方的開銷。如今的程序越來越多地使用像隊列似地的數據結構(例如并發編程)并且讓分代式垃圾收集產生一些病態行為。如果隊列中的某項活得比第一代長,那么它們都會被做標記,然后所有引用的舊位置將會得到更新并且被收藏。這個大概要比它們需要的慢3倍(例如比C語言)。標記區域的收集器有可能解決這個問題,像Belway(2002)和Lmmix(2008),因為托管已經被替換成一個區域,可以被收集,就好像是一個托管所,如果它包含大部分可及值,可以被另一個區域替換并且留下時間,直到它包含一些遙不可及的值。
盡管已經存在的C++,還有Java的發明人采用泛型來消除這些錯誤,但這些都導致了不必要的裝箱。例如,我構建一個簡單的哈希表,在.NET上面的速度比JVM要快17倍。原因就是.NET并沒有犯這個錯誤(它采用具體化泛型)并且.NET還有值類型。實際上,我認為是Lisp讓Java變慢。
裝箱、拆箱
所有現代式的函數式語言都是過分依賴裝箱。基于JVM語言,像Clojure和Scala別無選擇,因為VM甚至不能表達值類型。Ocaml(Objective Caml)在早期就揭示了類型信息,在它的編譯過程和經常使用整數類型進行裝箱標記并且在運行時去處理多態性。因此,Ocaml常常作為私有浮點數字被裝入箱中并且一直是盒元組。在Ocaml中一個三重的字節就是一個由指針(有一個隱藏的標簽嵌入在里面并且在運行時會被反復檢查)和一個64位的堆上分配塊頭與192位的主體包含三個標記的63字節整數(3個標簽,在運行時會被反復檢查)。這顯然是瘋了。
在函數式語言上,有關拆箱優化工作已經完成但它并未真正獲得牽引力。例如Mlton編譯器對于ML標準來說,是一個全程序優化編譯器并且很擅長拆箱優化工作。不幸的是,在運行時間之前和“長”編譯時間(在現代機器上低于1秒)阻止人們使用它。
唯一的主要平臺已經打破了這個趨勢,但令人驚訝的是.NET卻是個例外。盡管有一個Dictionary類可以高度優化鍵和值。微軟的員工,比如Eric Lippert就繼續強調值類型是根據值進行傳遞的,這一點很重要并且性能特點不是來源于它們內部拆箱特征。Eric的理解似乎已經被證明是錯誤的:越來越多的.NET程序員青睞拆箱而不是值傳遞。事實上,大多數結構是不可變的,因此,引用透明在值傳遞和引用傳遞之間并沒有什么語義差別。性能是可見的并且結構可以提供大量的性能改進。性能結構甚至可以保存堆棧溢出并且結構常常用來避免GC延遲在商業軟件上面,比如Rapid Addition's。
函數式與命令式
重分配的函數式語言的另一個原因是與生俱來的。命令式數據結構像哈希表結構使用內在巨大的整體數組。如果這些巨大的內部數組一直持續使用,將需要不斷復制和更新。所以純函數式數據結構比如平衡二叉樹分裂成許多小堆,分裂成塊為了便于從一個版本集合到另一個版本。
Clojure采用了一個非常巧妙的花招來解決這個問題,當集合例如dictionaries在初始化時被寫,然后進行讀取。在這個例子中,初始化可以使用突變來建立“幕后”結構。然而,這并不會有助于增量更新并且由此產生的集合在讀取數據方面仍然比較命令式等價物慢。當然純函數式語言在數據持久化方面明顯要比命令式強。然后,很少的實際應用程序受益于持久化實踐,所以這并不算是什么優勢。因此,把非純函數式語言降到命令式風格,這樣就可以毫不費力的從總受益。