Scheme語言中的「不可變數據」會產生性能問題嗎?

既然函數式編程語言一個特點是「不可變數據」,那麼函數傳參時和遍歷list或是map時是否會導致大量數據複製而產生性能問題?如果是,set!的出現是不是為了彌補這一問題?


Racket的Lists才是默認immutable的,所以它沒有set-car!和set-cdr!

append等操作的複雜度和List內部實現有關,通常(append a b)的複雜度是O(n),n是a的長度,把a全複製一遍得到新列表。雖然不能「就地更新」,但是新、舊數據可以共享部分結構(此例中為b)。

類似(fun1 (fun2 (fun3 lst)))的以及高階函數、函數組合中,確實可能導致lst元素被複制多次,Haskell等語言有fusion等優化,immutable正是類似優化的基礎。

另:各種對List的改進主要是利用其他結構(樹、隊列、堆等)實現List,如Random Access List的nth可以實現O(log n),Catenable List的append是O(1)..

總結:有些問題和imutalbe/mutalbe無關;immutable的實現有考量,不會無腦複製;immutable會有性能問題,也利於某些優化。


不要總覺得你寫了複製的代碼他就真的會複製,其實大部分的複製都是不會產生的。


immutable本身不會產生性能上的問題,更多是取決於你的實現比如在SML中

fun rev [] = []
| rev (x::xs) = (rev xs) @ [x];

就比

fun rev2 ([], ys) = ys
| rev2 (x::xs, ys) = rev2 (xs, x::ys)

要慢,前者是O(n^2),後者是O(n)的。

關於mutable和immutable,我們可以簡單從append進行比較

這是命令式語言中的做法,我們簡單的改變xs的尾指針將xs與ys連接成zs,這個操作時間複雜度是O(1)的,雖然高效,但是我們失去了原有的xs與ys,因此我們在需要使用xs和ys時得重新構建。

這種是函數式語言的做法,我們copy了xs中的節點,將它尾指針指向到ys,時間複雜度是O(n),比前面的慢點,但是我們沒有破壞原有的xs和ys,並且我們不用copy ys中的節點,而是將它們與新的zs共享。

PS: PFDS提到append有一個O(1)實現,不過我還沒看到。


函數式編程語言不一定「不可變數據」,Scheme也不是所謂函數式編程語言,實現得好的Scheme沒有性能問題,普通list和map遍歷傳「引用」不複製「大量數據」,現有實現無法滿足需要的性能時可以用C、C++、Java,非要用Scheme可以自己用低級語言擴展自己的Scheme實現,set!解決的是別的問題。


如果頻繁修改容器內容,卻不頻繁追加新元素,避免大量數據複製,可以用 scheme的 vector,沒人逼著你必須用 list,比如:

(define x (vector 1 2 3 4)) ; 定義一維數組
(vector-set! x 1 100) ; 修改 1位置的元素
(display x)
(newline)
(define y (vector (vector 1 2) (vector 3 4))) ; 定義二維數組
(list-&>vector a) ; 類型轉換
(vector-&>list x) ; 類型轉換

而且 vector在內存空間裡面是連續的,list是離散的,seek操作 vector是 O(1), list是 O(n)

再者,如果頻繁 append 新元素,一般情況下,不要往list尾部 append,而是頭部追加,比如x為 (3 2 1),那麼 (cons 4 x) 就能得到 (4 3 2 1),這是 O(1)的操作,然後你還可以倒過來方便遍歷(看具體遍歷的方向)。

---


一般來說:會,但不是很嚴重

因為

01 當一個值只作為一個函數的參數時,調用並不會複製,而是直接傳遞引用

02 當修改列表末尾的元素時開銷最大,因此這種場合常常使用樹來代替表

03 某些大開銷的操作,比如append兩個表,會被改成右結合

但是,有時候不可變數據開銷會更低,原因在於gc

因為很多時候變更數據,gc都會做一個原子操作,若是變更將一個次堆被主堆對象所引用,那麼將對其進行登記,這個操作是有開銷的,當此類操作數量大到一定程度的時候,性能還不如複製新的對象

最後,從scheme的角度來看,set! 可能不是為了改進性能,而是為了讓scheme在語義上更完整


都不可變了還複製個毛啊,複製是因為可變才會需要的保護動作。


用mutable才容易出現性能問題。

copy一個imutable又不是真的全部複製


scheme里的list壓根就不是不可變的,你可以用set-car!

函數傳參怎麼會複製。

函數式風格的操作是創建新的list而不是複製。


就算是我大C++還有copy-on-write的collection實現呢【逃


有沒有性能問題,要看實現,比如傳參時,Scheme標準沒規定是傳對象抑或傳引用 ,傳引用性能好,而且如果是不可變值的,相當於C++裡面的const T,無後顧之憂,此時不可變是反而是優點。還有map時亦可用惰性求值來避免遍歷,類似於C++的迭代器效果,此時不可變也是優點,因爲不用害怕迭代器失效。


推薦閱讀:

函數式編程如何優雅的處理很多 多個函數都要用到的 參數?
如何掌握函數式編程?
以函數編程語言作為入門的編程語言有什麼好處?
聲明式編程和命令式編程有什麼區別?
大家對於徐昊的《對象已死?》這篇文章怎麼看?

TAG:函數式編程 | Scheme |