语言的数据结构相通性

最近读了Redis的原理实现,感受到程序语言的相通性,只要你掌握了语言的共性,触类旁通其他语言的开发就变得非常简单了。

总体来说,各种程序语言底层的设计思想是非常相通的,首先针对需要解决的问题和场景选择不同的数据结构和算法,根据运行环境设计不同的架构和特性,根据作者的喜好选择开发的风格,根据应用场景开发对外的接口,根据程序员的实践维护社区和bug反馈区。

不要将某种数据结构固化成你理解的某种语言的一种实现方式,它们都只是一种方便理解的概念,有许多种实现它的方式,甚至完全不同。

我们下面看下数组这种数据结构的设计思路。

数据类型:数组

当我们想要设计一种数组的数据结构时,最容易想到的就是排成一队的学生,每个学生就是一个元素,我们可以对他们进行增删查改。他们紧紧相连,就像一块连续的存储空间。

当我们可以从头到尾的看完所有学生信息(遍历),也可以从头开始查找第4个学生(索引)。我们可以加入一个学生到任意位置(插入),也可以将任意一位同学移出队列(删除),但为了保持紧密连续的队列,我们需要做一些额外的调整。

这就是最常用的数据结构:数组。

优势: 1. 数据存储连续紧密,占用空间少。 1. 遍历数据时可以充分利用磁盘连续空间,减少磁盘臂的移动,提高访问速度。 2. 在每个元素占用空间相同时,能够支持快速索引访问。

缺点: 1. 只有头部指针,无法得知当前数组有多少元素,只能全部遍历后统计。 2. 元素占用空间不同时,缺乏随机读写的能力,必须从数组头部顺序访问和查找。 3. 如果中间元素出现增删,后续元素的位置需要依次更新。

改进版1:支持总数查询

在使用数组时,查询元素的总数是常见的需求,遍历元素获取数组长度的方式非常低效,如mysql普通的查询总行数,select count(*) from table_name,就会扫描全表。

为了支持总数快速查询,我们可以看下javascript的数组实现方式,它通过增加一个字段length,在每次变更时更新这个数字,即可无需遍历,直接读取长度信息。

改进版2:支持下标的快速访问

数组经常会进行遍历,但也会使用下标获取指定的元素,而典型的数组只能通过使用单独的计数器来遍历查找指定的元素,时间复杂度为O(n),在元素很多时耗时很久。

方式一:元素长度固定

这种方式下,我们就可以使用(目标元素地址 = 数组头部地址 + 元素长度 * 元素下标)的方式访问指定元素。

但是缺点也很明显,应用场景比较狭窄,因为所有元素占用空间都相同的情况非常少,在大部分场景下各个元素使用的空间不尽相同,这样就会导致空间的浪费。所以基本不会使用这种方式。

方式二:使用Hash方式

在这种存储方式中,我们先使用一个指定长度l的连续数组作为槽,这个长度就是hash的模值,我们用数组元素的索引i对数组长度l取模,得到槽的索引,然后用链表的方式进行存储,这样就能够进行快速的下标访问。

但是缺点也很明显,就是如果中间的元素增加或删除,后面的所有元素都需要重新hash和排列,因此也比较低效。

改进版3: 无需后置元素依次更新

在原数组更新时,我们可以直接在原位置上进行重写,而如果需要删除元素2,我们可以直接申请一块内存空间,将元素2之前和之后的连续内存空间直接拷贝到新空间中,就完成了数组的缩容。

扩容也是一样的,新增了元素5,我们同样重新申请一块内存空间,然后将元素5之前的拷贝到新空间,写入元素5,再将元素5之后的连续内存空间进行批量拷贝。

JS数组实现

// The JSArray describes JavaScript Arrays
//  Such an array can be in one of two modes:
//    - fast, backing storage is a FixedArray and length <= elements.length();
//       Please note: push and pop can be used to grow and shrink the array.
//    - slow, backing storage is a HashTable with numbers as keys.
class JSArray: public JSObject {
public:// [length]: The length property.DECL_ACCESSORS(length, Object)

首先看源码实现,会发现JS中数组是基于对象的,根据数组状态不同,元素属性分为固定长度的快数组,和hashTable存储的慢数组。

快数组和慢数组

快数组和慢数组最大的区别就是存储使用的数据结构不同,快数组采用连续空间的方式存储,慢数组采用hashTable的链表方式存储。

// Constants for heuristics controlling conversion of fast elements// to slow elements.// Maximal gap that can be introduced by adding an element beyond// the current elements length.static const uint32_t kMaxGap = 1024;// JSObjects prefer dictionary elements if the dictionary saves this much// memory compared to a fast elements backing store.static const uint32_t kPreferFastElementsSizeFactor = 3;

查看快慢数组转换源码:

static inline bool ShouldConvertToSlowElements(JSObject object,uint32_t capacity,uint32_t index,uint32_t* new_capacity) {STATIC_ASSERT(JSObject::kMaxUncheckedOldFastElementsLength <=JSObject::kMaxUncheckedFastElementsLength);if (index < capacity) {*new_capacity = capacity;return false;}if (index - capacity >= JSObject::kMaxGap) return true;*new_capacity = JSObject::NewElementsCapacity(index + 1);DCHECK_LT(index, *new_capacity);// TODO(ulan): Check if it works with young large objects.if (*new_capacity <= JSObject::kMaxUncheckedOldFastElementsLength ||(*new_capacity <= JSObject::kMaxUncheckedFastElementsLength &&ObjectInYoungGeneration(object))) {return false;}// If the fast-case backing storage takes up much more memory than a// dictionary backing storage would, the object should have slow elements.int used_elements = object->GetFastElementsUsage();uint32_t size_threshold = NumberDictionary::kPreferFastElementsSizeFactor *NumberDictionary::ComputeCapacity(used_elements) *NumberDictionary::kEntrySize;return size_threshold <= *new_capacity;}

快数组、慢数组两者转化的临界点有两种: 1. if (index - capacity >= JSObject::kMaxGap) return true; 2. return size_threshold <= *new_capacity;

其中kEntrySize根据数组存储的内容不同,会在1|2|3中选择一个作为系数,当为数组索引时一般为2。

根据代码可知,也就是空洞元素大于1024个,或者新容量 > 3*旧容量*2 时,会将快数组转化为慢数组。

所谓的空洞就是未初始化的索引值,如

const a = [1,2];
a[1030] = 1;

此时就会产生1028个空洞产生,会直接使用满数组来存储,这样能够节省大量的存储空间。

总之,在JS V8引擎中,数组使用快慢两种方式设计,快数组提高操作效率,慢数组节省空间。

数组的操作

数组的常用push/pop是通过直接在内存尾部追加或删除,一般申请内存时会留有冗余,空间不够时再次申请。

// Number of element slots to pre-allocate for an empty array.static const int kPreallocatedArrayElements = 4;
};

从上面的代码中可以看到,初次申请就会分配4个元素槽位置。

static const uint32_t kMinAddedElementsCapacity = 16;// Computes the new capacity when expanding the elements of a JSObject.static uint32_t NewElementsCapacity(uint32_t old_capacity) {// (old_capacity + 50%) + kMinAddedElementsCapacityreturn old_capacity + (old_capacity >> 1) + kMinAddedElementsCapacity;}

当空间不够用时,就会申请新的空间,新空间容量=原空间+原空间/2+16

然后根据需要变动的位置分为前后两块,直接按照连续内存空间的长度一次性拷贝到新内存地址上,效率是很高的。

Redis数组实现

Redis(Remote Dictionary Service, 远程字典服务)是使用最为广泛的存储中间件,由于其超高的性能和丰富的客户端支持,常常用于缓存服务,当然它也可以用于持久化存储服务。

Redis数组常用来存储任务队列,使用队列或者栈的方式,进行任务分发和处理。

ziplist压缩列表

Redis在数组元素较少时,使用ziplist(压缩列表)来存储,它是一块连续的内存空间,元素紧密存储,没有空隙。

// 压缩列表结构体
struct ziplist<T> {int32 zlbytes;  // 整个压缩列表占用字节数int32 zltail_offset;    // 最后一个元素的偏移量int16 zllength;     // 元素个数T[] entries;    // 元素内容列表int8 zlend;     // 结束标志位,值为0xFF
}
// 压缩列表元素结构体
struct entry {int<var> prevlen;   // 前一个entry的字节长度int<var> encoding;      // 元素类型编码optional byte[] content;    // 元素内容
}

因此通过zltail_offset我们可以快速定位到最后一个元素,通过prevlen可以支持双向遍历,通过zllength属性我们可以不用遍历就能支持整个数组的元素个数。

由于ziplist采取紧凑存储,因此没有空间冗余,导致每次插入新元素时,我们都需要申请新的内存空间进行扩展,然后将原内存地址空间直接拷贝到新空间中。由于Redis是单线程,因此如果压缩列表的容量过大,就会导致服务卡顿,因此不适合存储过大空间的内容。当更新数据时,如果内容是减少的或者没有超过已占用的指定字节数阈值,就可以原地更新。

quicklist快速列表

由于ziplist不适合大容量存储,因此在数组元素较多时,我们结合linkedlist(链表)的方式设计了quicklist

struct quicklist {quicklistNode* head;    // 头部指针quicklistNode* tail;    // 尾部指针long count;     // 元素总数int nodes;      // ziplist节点个数int compressDepth;      // LZF压缩算法深度
}
struct quicklistNode {quicklistNode* prev;    // 前节点指针quicklistNode* next;    // 后节点指针ziplist* zl;    // ziplist指针int32 size;     // ziplist字节总数int16 count;    // ziplist元素总数int2 encoding;      // 存储形式:原生数组|LZF压缩数组
}

一般每个ziplist的空间上限为8KB,超过就会创建新的节点,这样保证每个节点在更新时不会操作过大的空间进行复制,同时在检索时也大大提高了效率。每个节点的空间限制可以由list-max-ziplist-size参数配置。

在该结构体中,为了进一步压缩空间占用,可以使用LZF算法进行压缩,压缩深度为0|1|2三种,0就是不压缩,1就是首尾的前两个元素不压缩,其余都压缩,2就是首尾的一个元素不压缩,其余都压缩。

首尾元素不压缩是为了保证push/pop的快速操作时不用再解压缩改指针内容,而其他元素的压缩预计可以节省一半的空间。

总结

在语言的数组设计中,我们会发现几个通性: 1. 优先采用连续存储的内存空间,提升操作的效率。 2. 在新增元素时,采用连续内存空间复制的方式提升操作效率。 3. 使用专用的变量来存储数组长度,而不是通过遍历。 4. 在元素很多时,采用链表的方式存储,减少大块内存的申请和占用。同时提升查询效率。

参考资料

  1. Redis深度历险-核心原理与应用实践
  2. 探究V8引擎的数组底层实现:https://juejin.im/post/5d80919b51882538036fc87d
  3. 从Chrome源码看JS Array的实现:https://www.yinchengli.com/2017/04/16/chrome-js-array/
  4. V8源码:https://github.com/v8/v8/tree/4b9b23521e6fd42373ebbcb20ebe03bf445494f9

c语言uint8的数组怎么转换为uint32_剖析JS和Redis的数据结构设计:数组相关推荐

  1. js 取得数组下标_剖析JS和Redis的数据结构设计:数组

    语言的数据结构相通性 最近读了Redis的原理实现,感受到程序语言的相通性,只要你掌握了语言的共性,触类旁通其他语言的开发就变得非常简单了. 总体来说,各种程序语言底层的设计思想是非常相通的,首先针对 ...

  2. php mysql读取数组_php实现通用的从数据库表读取数据到数组的函数实例

    本文实例讲述了php实现通用的从数据库表读取数据到数组的函数.分享给大家供大家参考.具体分析如下: 此函数不关心表结构,只需要指定表名.结构和查询条件既可以对表进行通用查询操作,非常实用. funct ...

  3. C++实现从.txt文件中读取数据存入数组,将数组数据写入.txt文件

    声明: 编译器:vs2017 所有用到的.txt文件都是提前放在当前工程目录下的. 完整测试代码:github–Zhaotiedan 方法一:针对可以提前知道存入数组的大小 1.从.txt文件中读取数 ...

  4. c语言程序24转换12时间,C语言将24小时制转换为12小时制的方法

    C语言将24小时制转换为12小时制的方法 本文实例讲述了C语言将24小时制转换为12小时制的方法.分享给大家供大家参考.具体实现方法如下: /* * 24小时制转换为12小时制 */ #include ...

  5. labview 转标准c语言,LabVIEW编程之字符串转换为双精度数

    下面我们就Labview编程中字符串转换为双精度数问题进行详细分析: 大家都知道两点之间最近的路径是直线,遗憾的是程序员往往走的并非直线. 这是用户的程序框图: 分析用户的程序框图: 用户的目的是字符 ...

  6. 浙大版《C语言程序设计》第四版(何钦铭颜晖) 第7章 数组 课后习题答案

    你也可以上程序咖(https://meta.chengxuka.com),打开大学幕题板块,不但有答案,讲解,还可以在线答题. 一.选择题 1.假定 int 类型变量占用两个字节,则以下定义的数组 a ...

  7. R语言Box-Cox变换实战(Box-Cox Transformation):将非正态分布数据转换为正态分布数据、计算最佳λ、变换后构建模型

    R语言Box-Cox变换实战(Box-Cox Transformation):将非正态分布数据转换为正态分布数据.计算最佳λ.变换后构建模型 目录

  8. 【C 语言】一级指针 易犯错误 模型 ( 判定指针合法性 | 数组越界 | 不断修改指针变量值 | 函数中将栈内存数组返回 | 函数间接赋值形参操作 | 指针取值与自增操作 )

    文章目录 一.判定指针合法性 二.数组越界 三.不断修改指针变量值 四.函数中将栈内存数组返回 五.函数间接赋值形参操作 六.指针取值与自增操作 一.判定指针合法性 判定指针合法性时 , 必须进行如下 ...

  9. C语言学习笔记 (005) - 二维数组作为函数参数传递剖析

    前言 很多文章不外乎告诉你下面这几种标准的形式,你如果按照它们来用,准没错: //对于一个2行13列int元素的二维数组 //函数f的形参形式 f(int daytab[2][13]) {...}// ...

最新文章

  1. 广数系统980tdb系统说明书_汽车离合操纵系统随车检测工具使用说明书
  2. 万字长文|线性代数的本质课程笔记完整合集!
  3. Linux时间子系统之二:表示时间的单位和结构【转】
  4. c语言getchar_二级C语言试题刷题录
  5. Linux 系统添加静态路由的方法
  6. STM32-串口通信
  7. 如何利用第三方数据进行大数据分析
  8. Spring Boot集成微信扫码登录(实测通过)
  9. C#之CAD二次开发笔记(1) 开发环境测试
  10. 华为mate7 刷机出现android,华为Mate7怎么刷机 华为Mate7刷机教程【步骤详解】
  11. 您的组织策略阻止我们为您完成此操作,有关详细信息,请联系技术支持
  12. BubbleGum96 开箱杂谈与软件资源
  13. Android 自定义锁屏图案 View
  14. wxpython之StaticText最全介绍(持续更新)
  15. centos 安装python36 pip19.1 python虚拟环境
  16. 基于百度飞浆平台(EasyDL)设计的人脸识别考勤系统
  17. iphone手机safari浏览器访问网站滚动条不显示问题解决办法
  18. [pyecharts]如何使用Python将多个图表生成到一个HTML中?
  19. 数据库原理—关系数据库
  20. 【Vue】Vue3脚手架

热门文章

  1. 原创精华:剖析亿级请求下的多级缓存
  2. Facebook陷入史上最大危机:近7000页机密文件泄露!
  3. sap的ides和ecc分别是什么意思
  4. OLAP引擎:基于Druid组件进行数据统计分析
  5. cmake打包ICONV库
  6. 数据中台应该具备的能力
  7. mini2440-Openwrt启动信息
  8. ASCII中关于大小写字母间隔为32的思考
  9. framebuffer[转之]
  10. 保存到redis的字符串类型出现斜杆_深入浅出Redis:这次从Redis底层数据结构开始...