尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Hello 算法:内存与缓存视角下的数组与链表——存储层次、缓存机制与数据结构选型

Hello 算法:内存与缓存视角下的数组与链表——存储层次、缓存机制与数据结构选型 Hello 算法内存与缓存视角下的数组与链表——存储层次、缓存机制与数据结构选型【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo数组与链表是算法学习中最基础的两类数据结构它们分别对应连续存储与分散存储两种物理布局。本篇围绕《Hello 算法》第 4.4 节内存与缓存关联文档从计算机存储设备的层次结构出发讲解硬盘、内存、缓存三者的性能分工剖析数据结构的内存利用效率与缓存利用效率差异并最终落到何时选数组、何时选链表的工程判断准则。阅读完本篇你将掌握用缓存行、预取机制、空间/时间局部性等概念定量解释为什么数组通常比链表更快并能结合本仓库多语言源码在算法题与实战中做出正确的结构选型。计算机存储设备硬盘、内存与缓存的性能分工计算机中承担数据存储职责的设备主要有三类硬盘Hard Disk、内存Random-Access Memory, RAM与缓存Cache。它们在计算机系统中的定位与性能特征差异显著下表是对原文表格的完整保留与整理项目硬盘内存缓存用途长期存储操作系统、程序与文件等数据暂时存储当前运行中的程序及其正在处理的数据存储高频访问的数据与指令减少 CPU 对内存的访问断电易失性断电后数据不丢失断电后数据丢失断电后数据丢失容量大通常在 TB 量级小通常在 GB 量级极小通常在 MB 量级速度慢约数百至数千 MB/s快数十 GB/s 量级极快数十至数百 GB/s 量级成本元/GB便宜约几毛到几元每 GB昂贵约几十到几百元每 GB极昂贵实际随 CPU 一起封装出售存储金字塔速度、容量与成本的三方权衡存储金字塔示意CPU 之下依次是缓存L1/L2/L3、内存与硬盘越靠近 CPU 速度越快、容量越小、单位成本越高。将整个存储体系想象为一座金字塔越靠近顶端越接近 CPU的设备速度越快、容量越小、单价越昂贵。这种多层结构并非偶然而是计算机科学家与工程师反复权衡后的刻意设计。硬盘无法轻易被内存取代。其一内存中的数据在断电后会丢失不适合作为长期存储介质其二内存单价是硬盘的数十倍若全部用内存替代硬盘成本难以被消费市场接受。缓存无法同时做到大容量与高速度。随着 L1、L2、L3 缓存容量增大其物理尺寸变大与 CPU 核心的物理距离随之增加数据传输时间与元素访问延迟都会上升。就当前工艺水平而言多层缓存结构正是在容量、速度、成本之间取得的最优平衡点。存储层次的设计本质上是速度、容量、成本的平衡艺术。这种取舍其实遍布各个工业领域——我们往往需要在不同优势与约束之间寻找最优解而非追求单一指标的最大化。结论硬盘负责长期存放海量数据内存负责临时存放程序运行过程中正在处理的数据缓存负责存放被频繁访问的数据与指令以提升执行效率。三者协同工作维持计算机系统高效运转。运行期数据流从硬盘到 CPU 的通路数据流示意程序执行时数据自硬盘读入内存供 CPU 计算缓存可视为 CPU 的一部分通过智能地从内存装载数据为 CPU 提供高速访问。如图中箭头所示程序执行期间数据从硬盘读入内存供 CPU 计算使用。缓存本质上可被视作 CPU 的一部分——它充当 CPU 与内存之间的高速中转层缓存智能地从内存加载数据为 CPU 提供高速数据访问从而显著提升程序执行效率并降低 CPU 对较慢内存的直接依赖。数据结构的内存效率数组紧凑链表灵活站在内存空间利用的角度数组与链表各有优劣。一方面内存总量有限同一块内存无法被多个程序共享因此我们总希望数据结构尽量节省空间数组元素紧挨着排列不需要像链表那样为节点之间的引用指针额外预留空间因而空间效率更高但数组需要一次性申请足够的连续内存容易造成内存浪费且扩容需要额外的时间与空间开销详见后文列表扩容小节链表以节点为单位进行动态的内存分配与释放使用上更灵活能够按需伸缩。另一方面程序运行过程中内存被反复分配与释放空闲内存的碎片化程度会不断加剧导致内存利用率下降数组采用连续存储天然不太容易引发内存碎片链表元素散落在存储空间中频繁的插入与删除更容易造成内存碎片。从源码实现上看这两种特征非常直观。例如在 array.py 中扩容需要新建一块更大的连续内存再把旧元素逐一拷贝过去def extend(nums: list[int], enlarge: int) - list[int]: 扩展数组长度 # 初始化一个扩展长度后的数组 res [0] * (len(nums) enlarge) # 将原数组中的所有元素复制到新数组 for i in range(len(nums)): res[i] nums[i] # 返回扩展后的新数组 return res而链表由于每个节点是独立new出来的如 linkedlist_stack.cpp 中每次push都创建一个新ListNode天然支持按需动态分配不需要像数组那样预留或搬运整块内存void push(int num) { ListNode *node new ListNode(num); node-next stackTop; stackTop node; stkSize; }数据结构的缓存效率命中率决定性能缓存容量远小于内存却比内存快得多对程序执行速度举足轻重。由于缓存容量有限只能存放一小部分高频访问的数据当 CPU 访问的数据不在缓存中时便发生缓存未命中cache missCPU 必须从较慢的内存中重新加载数据。缓存未命中的次数越少CPU 读写数据的效率越高程序性能越好。我们把 CPU 从缓存中成功获取数据的比例称为缓存命中率cache hit rate它通常被用作衡量缓存效率的指标。缓存的四大加载机制为了尽可能提升命中率缓存采用了以下数据加载机制缓存行cache lines缓存并不是按字节逐个存取数据而是以缓存行为单位。相比逐字节传输缓存行传输效率更高。预取机制prefetching处理器会尝试预测数据的访问规律如顺序访问、固定步长跳跃访问等并按特定规律把数据预载入缓存以提高命中率。空间局部性spatial locality如果某个数据被访问其邻近的数据很可能在不久后也会被访问因此缓存加载某份数据时会顺带加载其邻近数据。时间局部性temporal locality如果某个数据被访问过它很可能在不久后再次被访问缓存据此保留近期访问过的数据。数组与链表在缓存利用上的四项差距实际上数组与链表在缓存利用效率上的差异主要源于以下四点占用空间链表元素比数组元素占用更多空间需存指针因此缓存中能容纳的有用数据更少缓存行链表数据在内存中分散排布而缓存按行加载加载到的无效数据比例更高预取机制数组的访问模式比链表更可预测系统更容易猜中下一步要加载哪些数据空间局部性数组集中存放在一段连续内存中已加载数据附近的数据更可能在不久后被访问。性能结论数组通常更快但并非万能总体而言数组的缓存命中率更高因此在操作效率上通常优于链表。这解释了为何在解决算法问题时基于数组实现的数据结构往往更受青睐。需要注意缓存效率高并不代表数组在所有场景下都优于链表。实际工程中应根据具体需求选择在算法题场景中我们更倾向基于数组的栈实现因为其操作效率更高且支持随机访问代价是需要预先为数组分配一定的内存空间当数据量非常大、动态性很强、栈的预期规模难以估计时基于链表的栈更合适——链表可以把大量数据分散到内存不同区域同时避免数组扩容带来的额外开销。本仓库 chapter_stack_and_queue 目录恰好在同一章内提供了这两种实现的对照源码可作为现场验证的样例array_stack.cpp基于vector动态数组的栈元素连续存储push/pop 操作作用于容器尾部缓存友好度高linkedlist_stack.cpp基于单链表的栈把头节点作为栈顶每次 push 都动态新建节点随规模增长天然避免扩容搬运但节点地址分散、缓存友好度低。在 summary.md 的问答环节中作者还以 C 的std::list与std::vector的对比进一步印证了这一判断链表的每个元素需要额外两个指针指向前驱与后继空间开销更大数据非连续存储导致缓存利用率低因此一般场景下std::vector性能更好——只有在二叉树、图等确实需要链式结构的场合链表才是必要之选。内存效率与缓存效率同一枚硬币的两面将上面两节合起来看数组与链表的核心差异可以凝练为一张对照表维度数组链表存储布局连续内存元素紧邻节点分散靠指针串联空间开销无指针开销容量可能有预留浪费每个节点额外携带指针开销更大内存碎片相对不易产生频繁插入删除更易产生碎片扩容成本需另辟大块内存并整体搬运$O(n)$按节点动态分配无整体扩容随机访问支持$O(1)$需遍历$O(n)$缓存行利用有效数据占比高无效数据占比高预取/局部性访问模式可预测、邻近数据易命中分散访问难以预测综合缓存命中率高低因此内存效率与缓存效率实则是同一枚硬币的两面数组以紧凑 连续换来了更高的空间利用率与缓存命中率代价是扩容与插入删除的高成本链表以分散 指针换来了灵活的动态伸缩与 $O(1)$ 的插入删除代价是更高的空间占用与更差的缓存表现。工程判断准则如何根据场景选择结构综合原文结论与仓库源码可将选型准则归纳为三步判断看访问模式需要随机访问、顺序遍历、按索引定位——优先数组/基于数组的动态数组看数据规模与动态性数据量巨大、规模难以预估、插入删除极为频繁——考虑链表避免反复扩容带来的搬运成本看场景约束算法竞赛与刷题场景默认以数组实现为优先cache 友好、可随机访问、预分配成本可接受内存极度受限或对延迟敏感的生产环境则应实测后再决定不宜一概而论。下一章将系统讲解栈与队列届时这两类结构均可用数组或链表实现届时即可套用上述准则进行实战选型。若想继续深入本章其他内容可参考 array.md、linked_list.md 与 summary.md。【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表