Erlang抽象数据结构简介
Upcoming SlideShare
Loading in...5
×
 

Erlang抽象数据结构简介

on

  • 2,228 views

介绍Erlang自带的抽象数据结构实现细节及性能评测,方便开发人员根据应用场景进行合理选择

介绍Erlang自带的抽象数据结构实现细节及性能评测,方便开发人员根据应用场景进行合理选择

Statistics

Views

Total Views
2,228
Views on SlideShare
2,227
Embed Views
1

Actions

Likes
12
Downloads
103
Comments
1

1 Embed 1

https://www.linkedin.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Erlang抽象数据结构简介 Erlang抽象数据结构简介 Presentation Transcript

  • Erlang 数据结构模块 array - 动态数组 dict - 动态散列表实现的字典 sets - 动态散列表实现的集合 queue - 双向队列 gb_trees - 平衡二叉查找树,可作为有序字典使用 gb_sets - 平衡二叉查找树实现的有序集合 orddict - 列表实现的有序字典 ordsets - 列表实现的有序集合
  • array 考虑到函数式语言数据不可变的特性,array 使用基数为 10 的 tuple tree 形式实现,将元素更改发生时需要复制的数据限制在很小的范围内,以便提高效率。 为了平衡存储空间和动态性,tuple tree 是惰性展开的,仅在某个下标被使用时才会创建出对应的 tuple 结构进行存储,。到 R14B02 版本为止的实现里,tuple tree 都是 只增大不减小的,对 array 进行 resize/2 操作时仅仅是更新了其 size 属性而已; array 的下标是从 0 开始的,同 tuple、list 等结构起始下标不同! array 有两种遍历方式:普通遍历和稀疏遍历,区别在于稀疏遍历会跳过所有未定义(取值为默认值)的元素,而普通遍历则不会跳过。
  • array array 内部结构如下图所示: 其中记录字段含义为: size - 当前数组中已被用户存储过数据的元素个数; max - 当前数组中能够保存的最大元素个数; default - 未存储数据元素的默认值,未指定时默认为 undefined; elements - 保存数据的 tuple tree
  • array 例子:顺序向一个空 array 的 9、0、100 下标处分别存储 a、b、c 数据,array 结构变化如下:
  • dict dict 基于论文 The Design and Implementation of Dynamic Hashing for Sets and Tables in Icon 论文中提出的动态散列技术构建,通过在散列 bucket 数量同 存储元素数量之间维持一定比例,实现较高的内存使用率及访问效率。 同 array 结构采用的策略相似,整个散列 bucket 数组也被划分为若干个固定长度的区段(segments),以便在更新特定 bucket 内容时不需要整体复制数据。目前每个 segment 的大小设定为 16,是反复测试后选择的最佳值。 dict 的扩张和紧缩都依赖于一定的元素数量阈值触发,该阈值基于当前活动的 bucket 数量按比例计算得出,目前设定为元素数量超过 bucket_no*5 时触发扩张操作,低于 bucket_no*3 时触发紧缩操作。可能导致元素数量减少的操作都会在操作完成后尝试紧缩 bucket 区段;可能导致元素数量增加的操作都会在操作进行前尝试扩张 bucket 区 段; bucket 中的多个键值对以无序列表形式存储,对 dict 的访问操作最终都会对特定的 bucket 进行遍历。只读操作的遍历是尾递归形式,bucket 较大时附加开销很小;但更新 操作的遍历是普通递归形式,附加开销较大,所幸通常情况下 dict 的算法可以保证 bucket 不会很长。 dict 中的键值对本身以 improper list 形式保存,主要是希望以相同的代码高效处理 K-V(1对1) 和 K-Bag(1对多) 两种数据形式。
  • dict dict 内部结构如下图所示:
  • dict 其中字段含义为: size - 散列表中已存元素数量,初始为 0; n - 散列表中活动 bucket 数量,初始为 16; maxn - 散列表中当前允许的最大 bucket 数量,扩张操作需要据此判断是否要增加新的 bucket 区段,初始为 16; bso - 动态散列算法中计算一个 bucket 及其伙伴 bucket 的分隔下标,以此为界将 bucket 数组划分为对称的两半,初始为 8; exp_size - 触发扩张操作的元素数量阈值,根据活动 bucket 数量和加载因子计算得出,初始为 16*5=80; con_size - 触发紧缩操作的元素数量阈值,根据活动 bucket 数量和加载因子计算得出,初始为 16*3=48; empty - 用于快速创建新 bucket 区段的区段模板,为 16 元 tuple,每个元素都是空列表; segs - 散列 bucket 区段组,其下保存了所有的元素数据;
  • dict 紧缩发生条件: 操作完成后元素数量小于 dict 紧缩阈值 dict.con_size 满足前者的条件下,dict 活动 bucket 数量 dict.n 大于 16 紧缩过程:每次紧缩掉一个 bucket,bucket 数量足够少时减半 bucket 区段 计算出最后一个 bucket(dict.n) X 的伙伴 bucket Y 的位置(dict.n-dict.bso); 将 X 和 Y 的列表合并,替换掉 Y 列表,并清空 X 列表; 活动 bucket 数量 dict.n 减 1,同时根据加载因子重新计算紧缩与扩充阈值; 若活动 bucket 数量等于分隔下标 dict.bso,则将 bucket 区段、最大 bucket 数量 dict.maxn 和分隔下标 dict.bso 都减半。
  • dict 扩张发生条件: 操作进行前后元素数量大于 dict 扩张阈值 dict.exp_size 扩张过程:每次增加一个 bucket,bucket 数量增长到最大值时倍增 bucket 区段 若活动 bucket 数量 dict.n 等于最大 bucket 数量 dict.maxn,则将 bucket 区段、最大 bucket 数量 dict.maxn 和分隔下标 dict.bso 都倍增; 活动 bucket 数量 dict.n 加 1,作为新增 bucket X 的下标,同时根据加载因子重新计算紧缩与扩充阈值; 计算出新增 bucket X 的伙伴 bucket Y 的位置(dict.n-dict.bso); 将 Y 的列表重新散列,分配到 X 和 Y 两个 bucket 里;
  • setssets 同 dict 所用实现方法一致,只是存储元素从键值对变为了键本身,这里不再赘述。
  • queue 为了实现快速的双向入队、出队操作,queue 基于论文 Purely Functional Data Structures 中描述的算法构建; queue 被表示为 2 元 tuple,第一个元素是反向排列的尾部元素列表,第二个元素是正向排列的头部元素列表,二者合在一起就是完整的队列; queue 内部结构如下图所示:
  • orddict orddict 用按 key 排序的列表表示字典结构,数据量较大时访问开销很大,只适用于数据量较小的情况; 例如:向 orddict 顺序插入 z->1、a->2、c->3 后,orddict 结构为:[{a,2}, {c,3}, {z,1}]
  • ordsets ordsets 同 orddict 结构一样,只是存储的是 key 而不是键值对; 例如:向 ordsets 顺序插入 z、a、c 后,ordsets 结构为:[a, c, z]
  • gb_trees gb_trees 是一种平衡二叉查找树结构,基于论文 General Balanced Trees 中描述的算法构建,它可以在不额外在结点上记录数据的情况下,通过简单的平衡策略达到平均 状况下接近 AVL 树或红黑树的访问效率; 原文中每个 GB 树需要记录 2 个全局数据:树中结点个数 |T| 和自上次全局重平衡后的结点删除次数 d(T)。基于这 2 个数据可定义重平衡策略如下: 发生结点插入时,若新增结点 v 的高度 h(v) 满足条件 h(v) > ceil(c*log[2](|T|+d(T))),则沿插入路径回溯寻找高度最小的结点 u 使得 h(u) > ceil(c*log[2] (|u|)),然后对以 u 为根的子树进行局部重平衡操作,最后增加全局计数 |T|; 发生结点删除时,增加全局计数 d(T),若条件 d(T) >= (2^(b/c)-1)*|T| 满足,则对整个树进行全局重平衡,并重置 d(T) 为 0。 这里 c、b 为常数且满足条件 c > 1、b > 0,使用这种重平衡策略后树的最大高度为 max(h(T)) = ceil(c*log[2](|T|)+b)
  • gb_treesErlang 的 gb_trees 并没有完全按照论文的方式实现,而是进行了若干折中处理: 因为结点删除不会增加树的高度,故删除操作不进行自动重平衡,以降低删除开销。在大规模删除结点后,为了让查找效率最优化,开发人员可以显式调用 balance/1 方法对整 棵树进行重平衡; 由于不进行自动重平衡,gb_trees 不需要记录删除次数 d(T),只有一个全局数据 |T| 需要记录; 局部重平衡子树的根结点查找策略从原来的 h(u) > ceil(c*log[2](|u|)) 改为 2^h(u) > |u|^c,以提高计算效率。虽然变换前后的条件不完全相同,但实测差异不大;
  • gb_trees gb_trees 内部结构如下图所示: 其中字段含义为: Size - 树中结点总数 |T|; Tree - 保存整棵树结构; Key - 结点键; Val - 结点值; Smaller - 左子树结构,其中所有结点键都小于当前结点键; Bigger - 右子树结构,其中所有结点键都大于当前结点键;
  • gb_trees smallest/1、largest/1、is_defined/2、lookup/2、get/2 都是尾递归操作,附加空间开销很低; 所有对树进行变更的操作都是普通递归操作,数据集较大时附加空间开销很大; iterator/1 创建的迭代器是一个列表,记录了树中序遍历调用栈,next/2 只是基于迭代器列表进行简单的弹栈、压栈操作而已;
  • gb_sets gb_sets 同 gb_trees 结构基本一致,只是结点上只存储键,不再存储值。 gb_sets 内部结构如下图所示:
  • gb_sets gb_sets 进行集合间操作时,总是会将元素数量较少的集合 X 转换为有序列表,并根据另一个集合 Y 的元素数量使用不同的方法进行遍历操作: 若 |Y| < 10,则集合 Y 也被转换为有序列表,X 和 Y 之间的集合操作通过列表归并进行,结果列表再转换为 gb_sets 结构; 若 |Y|/|X| < c*log[2](|Y|),则仍然将 Y 转换为有序列表处理,这里 c 设为 1,且因 math 模块中没有以 2 为底的对数函数,条件被改为 |Y| < |X|*c1*ln(|Y|),其中 c1=c/ln(2)=1.46; 以上条件都不满足时,保持 Y 为树形结构不变进行遍历;
  • 性能评测 数据结构 写1w次时间(us) 读1w次时间(us) array 5398 2808 dict 19081 3393 sets 15267 3009gb_trees 42864 5061 gb_sets 46527 4902 orddict 2812805 1248887 ordsets 1699518 964396
  • 性能评测queue头部入队1w次时间(us) queue头部查看1w次时间(us) queue头部出队1w次时间(us) 940 906 1395queue尾部入队1w次时间(us) queue尾部查看1w次时间(us) queue尾部出队1w次时间(us) 1036 809 1169
  • 总结 要求遍历的顺序性时,尽量避免使用效率低下的 orddict 和 ordsets,用 gb_trees 和 gb_sets 代替它们; 不要求遍历的顺序性时,尽量使用 dict 和 sets,因它们比 gb_trees 和 gb_sets 的效率高; 平均来看 queue 的入队出队操作基本上同列表头部操作开销一样,效率很高,只需要双向队列访问模式的地方推荐使用;
  • Q&A