此文已由做者张佃鹏受权网易云社区发布。
java
欢迎访问网易云社区,了解更多网易技术产品运营经验。node
最近在项目中用到了Transient数据结构,使用该数据结构对程序执行效率会有必定的提升。刚刚接触Transient Data Stuctures,下面将本身关于对其的了解总结以下:数组
1.clojure的不可变数据特性及存储方式:sass
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(http://lampwww.epfl.ch/papers/idealhashtrees.pdf)进行存储:数据结构
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之间的存储结构以下:ide
如上图所示,在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:oop
尽管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中有些函数用到了瞬态数据结构,因此咱们尽可能在编码时使用这些函数来提升代码效率。
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 使用Prometheus+Grafana对Kubernetes进行性能监控的实践