当前位置: 首页 > news >正文

(ZipList入门笔记二)为何ZipList可以实现内存压缩,可以详细介绍一下吗

文章目录

      • 1. 根除指针:最大的开销来源
      • 2. 变长编码:按需分配,杜绝浪费
      • 3. 紧凑的整体布局
      • 综合示例:一个直观的对比
      • Ziplist的代价:连锁更新

我们来深入探讨一下 Ziplist(压缩列表)实现内存压缩的精髓。

理解 Ziplist 为何能极致地节省内存,关键在于理解它解决了传统数据结构(尤其是双向链表)的哪些“内存浪费”问题

它的压缩魔法主要来源于以下三个核心设计:

1. 根除指针:最大的开销来源

这是 Ziplist 最重要、最根本的内存优化点。

传统双向链表的问题:
一个标准的双向链表,每个节点(Node)除了存储数据外,还必须包含两个指针:一个 next 指向后继节点,一个 prev 指向前驱节点。

在 64 位操作系统中,一个指针就占用 8 个字节。这意味着,每个节点都有 8 + 8 = 16 字节的固定指针开销

想象一下,如果你要存储的数据只是一个很小的整数(例如数字 5,实际只需要 1 字节),那么为了存储这 1 字节的数据,你就要付出 16 字节的指针开销。这就像用一个巨大的集装箱去运送一粒米,内存利用率极低。

Ziplist 的解决方案:
Ziplist 将所有元素存储在一块连续的内存中,从根本上就不需要 nextprev 指针。

  • 如何向后遍历? 因为内存是连续的,知道当前节点的起始地址和它的总长度,就可以计算出下一个节点的起始地址。
  • 如何向前遍历?(这是精髓) Ziplist 的每个节点(entry)内部都包含一个 previous_entry_length 字段,它记录了前一个节点的长度。当需要向前遍历时,用当前节点的起始地址减去这个长度,就能精确地定位到前一个节点的起始位置。这个 previous_entry_length 字段本身也是变长的(1或5字节),远比一个8字节的指针要小。

效果对比:

  • 双向链表N 个节点就有 N * 16 字节的指针开销。
  • Ziplist0 字节的指针开销。

2. 变长编码:按需分配,杜绝浪费

传统结构的问题:
在 C 语言等静态语言中,数据类型通常有固定的大小。例如,int 类型可能固定为 4 字节。如果你要存储数字 10,它实际上只需要 1 个字节,但系统依然会为其分配 4 字节,剩下的 3 字节就被浪费了。

Ziplist 的解决方案:
Ziplist 节点的 encoding 字段可以非常灵活地定义数据的存储方式,真正做到“用多少,给多少”。

  • 对于整数:
    • 如果整数值可以用 1 个字节表示(比如 0-255),encoding 就会指明这是一个单字节整数,content 部分就只占用 1 字节。
    • 如果需要 2 个字节,content 就占用 2 字节。
    • …以此类推,支持多种长度的整数。
  • 对于字符串:
    • encoding 字段会记录字符串的长度,然后 content 部分就紧跟相应长度的字节数组。

这种设计避免了为了容纳“可能的最大值”而预先分配固定大小的空间,而是根据“实际值”来动态决定使用多大的空间。

3. 紧凑的整体布局

除了上述两点,Ziplist 还有一个整体上的优势:

  • 单一的头部信息: 整个 Ziplist 只有一个 zlbytes, zltail, zllen 的头部(总共 10 字节)。而双向链表的“头部”(即指针)是分散在每一个节点里的。
  • 连续内存: 连续的内存布局减少了内存碎片,并且有利于 CPU 的缓存(Cache),在遍历时能获得更好的性能。

综合示例:一个直观的对比

假设我们要在 64 位系统上存储一个列表:["hello", 99]
在这里插入图片描述

  • 双向链表实现:

    • 节点1 (“hello”): 16字节(指针) + 6字节(数据"hello\0") ≈ 22字节
    • 节点2 (99): 16字节(指针) + 8字节(假设用long存储) = 24字节
    • 总计 ≈ 46 字节
  • Ziplist 实现:

  • 在这里插入图片描述

    • 头部: 10字节 (zlbytes+zltail+zllen)
    • 节点1 (“hello”): 1字节(prev_len) + 1字节(encoding) + 5字节(数据) = 7字节
    • 节点2 (99): 1字节(prev_len) + 1字节(encoding) + 1字节(数据) = 3字节
    • 结尾: 1字节 (zlend)
    • 总计 = 10 + 7 + 3 + 1 = 21 字节

可以看到,对于小数据量的场景,Ziplist 的内存压缩效果非常显著。

Ziplist的代价:连锁更新

这种极致的压缩是有代价的。最主要的问题就是连锁更新(Cascading Update)。由于每个节点记录的是前一个节点的长度,当在列表的某个位置插入一个新节点,或者删除了一个节点,可能会导致其后续所有节点previous_entry_length 字段需要更新,这种更新可能会像多米诺骨牌一样传播下去,导致一次插入/删除操作的时间复杂度在最坏情况下达到 O(N²),性能较差。

正因为如此,Ziplist 只适用于存储元素数量较少、内容较小的场景(List, Hash, Zset),并且在后来的 Redis 版本中,逐渐被性能更稳定的 Listpack 所取代。

http://www.xdnf.cn/news/1248913.html

相关文章:

  • 在AI时代,如何制定有效的职业规划?AI时代职业规划+AI产品经理角色
  • 探索设计模式的宝库:Java-Design-Patterns
  • FastDeploy2.0:报qwen2.embed_tokens.weight
  • 3. 为什么 0.1 + 0.2 != 0.3
  • 多传感器融合
  • Redis之Set和SortedSet类型常用命令
  • leetcode-python-删除链表的倒数第 N 个结点
  • VUE+SPRINGBOOT从0-1打造前后端-前后台系统-邮箱重置密码
  • 使用ProxySql实现MySQL的读写分离
  • ubuntu24安装vulkan-sdk
  • 一文搞定JavaServerPages基础,从0开始写一个登录与人数统计页面
  • Rust进阶-part4-智能指针2
  • 力扣106:从中序与后序遍历序列构造二叉树
  • VUE+SPRINGBOOT从0-1打造前后端-前后台系统-登录实现
  • Redis里面什么是sdshdr,可以详细介绍一下吗?
  • Linux lvm逻辑卷管理
  • 跑yolov5的train.py时,ImportError: Failed to initialize: Bad git executable.
  • 【Linux】特效爆满的Vim的配置方法 and make/Makefile原理
  • 一种红外遥控RGB灯带控制器-最低价MCU
  • MySQL间隙锁在查询时锁定的范围
  • 前端遇到页面卡顿问题,如何排查和解决?
  • 【运维部署篇】OpenShift:企业级容器应用平台全面解析
  • Spring 的优势
  • Springboot集成Log4j2+MDC串联单次请求的日志
  • HBM Basic(VCU128)
  • 《Python基础》第3期:使用PyCharm编写Hello World
  • Leetcode-2080区间内查询数字的频率
  • 查看部署在K8S服务的资源使用情况
  • LOOP Finance:一场 Web3 共和国中的金融制度实验
  • 创维智能融合终端DT741_移动版_S905L3芯片_安卓9_线刷固件包