此文已由做者張佃鵬受權網易雲社區發佈。
html
歡迎訪問網易雲社區,瞭解更多網易技術產品運營經驗。java
最近在項目中用到了Transient數據結構,使用該數據結構對程序執行效率會有必定的提升。剛剛接觸Transient Data Stuctures,下面將本身關於對其的瞭解總結以下:node
1.clojure的不可變數據特性及存儲方式:數據庫
clojure中的數據結構具備不可變特性(Persistent),也就是對一個數據結構添加元素、刪除元素、更改元素,返回的是一個新的數據結構,而原來的數據結構不會變:數組
;;;定義一個向量
(def data [1 2 3 4 5])
;;=> #'user/data;;更改向量中的元素,返回新的向量,而原有向量不變
(update data 3 str)
;;=> [1 2 3 "4" 5]
data
;;=> [1 2 3 4 5]
;;給向量增長一個元素,返回新的向量,原有向量的數據結構不變
(assoc data 5 6)
;;=> [1 2 3 4 5 6]
data
;;=> [1 2 3 4 5]複製代碼
以上對data=[1 2 3 4 5]做增長和更改操做,data數據自己都沒有變,而是生成了新的數據,這樣的數據不可變性很是有利於數據的安全性,不會出現更改對象帶來的反作用。這樣的特性必然對數據的存儲方式有很高的要求,clojure中的數據結構採起idea hash trees(lampwww.epfl.ch/papers/idea…sass
vector中的全部元素都放在Leaf node中,而Internal node不存放元素,只是存放指向兒子節點的指針,用於尋找葉子節點,其中有個Head指針指向樹的根節點,該Head指針存放着數據爲該vector的大小,根據vector的size,咱們即可以沿着Internal node找到存放在任何序號的元素。 clojure中的vector數據結構具備不可變性,因此爲了減小複製的成本,clojure對其存儲採起高效的共享模式:安全
;;
(def brown [0 1 2 3 4 5 6 7 8])
(def blue (assoc brown 5 'beef))複製代碼
上面定義了一個brown的向量,而後更改brown的第6個元素生成新的向量blue,brown和blue之間的存儲結構以下:bash
如上圖所示,在brown數據結構上更改元素後,原有的brown數據結構從其head指針開始徹底沒有改變,每個vector都有本身的head指針,所以blue必須構造本身的head指針,在構造的過程當中,儘可能共享brown已有的數據結構,只是新增長了一個被更改的葉子節點,減小了不必的存儲空間的浪費。 這樣理想的存儲方式很是有利於不可變數據結構增刪改操做,時間複雜度是O(log2 n),實際存儲中,clojure採起不是2個子節點的存儲方式,而是32個子節點的存儲方式,相應的時間複雜度是 O(log32 n)。咱們知道對於n很是大的狀況下,O(log32 n)和O(log n)的複雜度是同樣的,可是對於相對較小的數據來講,O(log32 n)能夠近似O(1),這也是爲何clojure爲何說本身對於vector的增刪改操做接近常數時間的緣由。數據結構
2.爲何要有Transient Data Structures:併發
儘管vector數據結構的存儲方式效率已經很高了,可是它依然須要頻繁的分配存儲空間和對存儲空間進行垃圾回收,好比咱們執行如下操做:
;;以此將0到9加入到一個vector中
(reduce conj [] (range 10))
;;=> [0 1 2 3 4 5 6 7 8 9]複製代碼
在將這10個數依次加入到數組中,每加入一個數便生產一個帶有head指針的新的vector,又由於前面一個vector已經不會再被用到,系統須要對其空間進行垃圾回收,雖然先後數據結構中的存儲空間有必定的共享,可是這樣的操做仍是會有必定時間的浪費,對於效率要求比較高的代碼難以接受。 所以爲了提升效率,clojure增長了一種Transient數據類型,transient使clojure的數據結構能夠改變,transient不只可使用在vector中,還能夠在set和map中使用,可是不能用於list中。下面經過更新一個vector中的元素操做來對比transient與persistent數據類型的區別,將[1 2 3 4 5 6]更新爲[1 2 F 4 5 6],兩種不一樣數據類型之間的變化過程以下:
persistent更新操做後,具備兩個head指針,也就兩個不一樣的vector,而transient更新操做後,只是在原有數據結構的基礎上,更改了一個葉子節點,head指針不變,原有的vector中存放的內容發生了改變,因此transient在必定程度上減小了存儲空間的浪費,提升了代碼執行效率。
3.Transient Data Structures的相關操做函數:
對於只讀操做,由於不會改變數據內容,transient data和persistent data共享一套只讀操做函數,好比:nth, get, count等函數,可是對於更改數據的函數,會有另一套操做函數,下面是關於transient data structures數據結構相關操做函數的詳解:
transient函數:
該函數是將一個persistent數據格式轉換爲transient的數據格式,該操做的時間複雜度接近於O(1),若是咱們對轉換後的作更改操做,不會影響原有數據內容,原有數據依然是persistent。
persistent!函數:
該函數剛好與transient函數相反,將一個transient的數據格式轉換爲persistent格式,不一樣的是:轉換後會影響原有的transient數據,使原有的transient數據變爲不可用:
(def a [1 2 3])
;;=> #'insight.main/a
;;用transient函數生成transient格式的數據
(def a' (transient a)) ;;=> #'insight.main/a' ;;獲取其中的函數 (nth a' 2)
;;=> 3
;;增長數據
(conj! a' 4) ;;用persistent!函數返回不可變數據格式內容 (persistent! a')
;=> [1 2 3 4]
;;這個時候原有的a'數據將變爲不可用數據,對其讀寫都會拋出異常 (nth a' 2)
IllegalAccessError Transient used after persistent! call clojure.lang.PersistentVector$TransientVector.ensureEditable (PersistentVector.java:548)
(conj! a' 4) IllegalAccessError Transient used after persistent! call clojure.lang.PersistentVector$TransientVector.ensureEditable (PersistentVector.java:548)複製代碼
相關「寫」操做函數:
對於transient相關「寫」操做函數有:assoc!/conj!/disassoc!/pop!/disj!,這些寫操做函數只是在對於persistent相關函數後加上「!」,他們的函數參數格式與去掉「!」後的函數如出一轍,下面列舉了相關操做代碼:
;;將0到9以此加入到一個transient類的vector中,每次加入一個元素時,不會創建新的vector,
(loop [i 0 v (transient [])]
(if (< i 10)
(recur (inc i) (conj! v i))
(persistent! v)))
;;=> [0 1 2 3 4 5 6 7 8 9]複製代碼
特別須要注意的地方:
雖然在增長元素時,是在原有結構上增長元素,可是這也並不意味着原有數據結構的頭指針(Head)必定不變,若是增長的元素特別多的狀況下,須要重新調整數據層次結構,那麼頭指針就會發生改變,而原有數據結構的頭指針與該數據結構的名字一一對應,因此對該transient數據進行操做時,必定要將操做後的數據賦值給原有數據的名字:
;;連續8次給t添加key-value對,返回結果是正確的
(let [t (transient {})]
(dotimes [i 8]
(assoc! t i i))
(persistent! t))
;;=> {0 0, 1 1, 2 2, 3 3, 4 4, 5 5, 6 6, 7 7}
;;當連續9次給t添加key-value對時,便返回錯誤的結果,由於當9次添加元素時,該map的頭指針發生了變化,因此新的數據內容不是之前的t
(let [t (transient {})]
(dotimes [i 9]
(assoc! t i i))
(persistent! t))
;;=> {0 0, 1 1, 2 2, 3 3, 4 4, 5 5, 6 6, 7 7}複製代碼
正確的使用添加方式應該以下:
;;可使用reduce函數,每次添加元素是在assoc!函數的返回結果上進行添加,這樣便會返回正確的內容
(persistent!
(reduce (fn [t i] (assoc! t i i))
(transient {})
(range 10)))
;;=>{0 0, 7 7, 1 1, 4 4, 6 6, 3 3, 2 2, 9 9, 5 5, 8 8}複製代碼
4.clojure.core庫中使用transient data相關函數及其效率:
在什麼狀況下會適用Transient Data Structurese呢?首先,咱們只在意更改後的數據,原始數據對咱們來講不重要,也不會再被用到。其次,Transient Data Structures適用於單線程的程序,由於每一個線程共享相同的數據時,同時更改會形成併發問題,這也是clojure爲何採用persistent數據結構的緣由之一;最後,瞬態數據結構主要爲了提升代碼效率而設計,因此對於屢次連續添加元素,能夠考慮使用transient數據格式。 通過對clojure.core庫中相關函數定義源碼的搜索,找出了該中使用了transient data structure相關函數有:set函數/into函數/mapv函數/filterv函數/group-by函數/frequencies函數,咱們能夠發現這些函數的特色都是對一個序列進行更改操做,可是並不關心原始數據的內容,如下咱們用criterium.core庫中的quick-bench函數來測試代碼運行時間,從而證實transient data的效率:
;;into函數的效率提升效果:
;;用concat函數合併兩個一個vector和list,將合併結果轉換爲vector,平均消耗時間爲:4.354145 µs
(quick-bench (vec (concat [1 2 3] (range 100 200))))
;;Evaluation count : 154098 in 6 samples of 25683 calls.
;;Execution time mean : 4.354145 µs
;;直接使用into函數將一個list函數中的內容插入到vector中,平均消耗時間只須要1.549213 µs
(quick-bench (into [1 2 3] (range 100 200)))
;;Evaluation count : 382944 in 6 samples of 63824 calls.
;;Execution time mean : 1.549213 µs
;;mapv函數的效率提升效果:
;;咱們本身定義個mapv'函數與mapv函數操做效果同樣 (defn mapv' [f coll]
(loop [result [] r coll]
(if (nil? (seq r))
result
(recur (conj result (f (first r))) (rest r))
)))
;;使用咱們本身定義的函數,平均消耗時間爲794.484682 µs
(quick-bench (mapv' inc (range 10000))) ;;Evaluation count : 780 in 6 samples of 130 calls. ;;Execution time mean : 794.484682 µs ;;使用系統的mapv函數,平均消耗時間爲197.386935 µs (quick-bench (mapv inc (range 10000))) ;;Evaluation count : 3090 in 6 samples of 515 calls. ;;Execution time mean : 197.386935 µs ;;使用map和vec操做函數,平均消耗時間爲418.949804 µs (quick-bench (vec (map inc (range 10000)))) ;;Evaluation count : 1482 in 6 samples of 247 calls. ;;Execution time mean : 418.949804 µs複製代碼
經過以上對函數的對比,咱們發現,transient函數在操做大數據的狀況下,確實會給咱們節省不少時間,因此在平時寫代碼時必定要養成好的習慣:爲了提升代碼效率,儘可能使用以上提過的函數。
5.總結:
今天主要對剛剛學習的transient data structures進行了概括總結,瞬態數據結構對於代碼效率的提升有很大的做用,該數據類型能夠應用到map,vector,map上,若是咱們對原始數據絕不關心,則關心改變後的數據,尤爲是連續屢次的進行這樣的操做,那麼咱們就能夠考慮使用瞬態數據結構,clojure.core中有些函數用到了瞬態數據結構,因此咱們儘可能在編碼時使用這些函數來提升代碼效率。
更多網易技術、產品、運營經驗分享請點擊。
相關文章:
【推薦】 基於Redis+Kafka的首頁曝光過濾方案
【推薦】 數據庫路由中間件MyCat - 使用篇(6)
【推薦】 abtest-system後臺系統設計與搭建