try ai
科普
编辑
分享
反馈
  • 有效访问时间

有效访问时间

SciencePedia玻尔百科
核心要点
  • 有效访问时间 (EAT) 是内存访问延迟的加权平均值,由缓存命中与未命中的概率和成本决定。
  • 性能严重依赖于转译后备缓冲器 (TLB) 的命中率;一次未命中需要进行缓慢的页表遍历,而一次缺页的速度可能要慢上数千倍。
  • EAT 公式是工程师分析硬件设计(如 TLB 大小)和操作系统策略(如页面大小)之间权衡的关键工具。
  • 理解 EAT 有助于软件开发人员通过改善数据局部性并避免像颠簸这样的性能悬崖来编写更高效的代码。

引言

你的计算机内存有多快?答案并非一个单一的数字,而是一个称为有效访问时间 (EAT) 的统计平均值。这个关键指标决定着系统性能,它在虚拟内存的无缝幻象与物理硬件的复杂分层现实之间架起了一座桥梁。现代计算机每秒执行数十亿次内存查找,如果没有机制使这些查找在平均情况下变得快速,系统将慢到无法使用。本文将揭开决定这一平均速度的各种因素的神秘面纱,从硬件缓存到操作系统的决策。

在接下来的章节中,我们将首先解构 EAT 背后的核心原理。“原理与机制”一节将介绍其基本公式,探讨转译后备缓冲器 (TLB)、页表以及诸如缺页之类的灾难性事件所扮演的角色。随后,“应用与跨学科联系”一节将展示这个单一概念如何为分析计算机体系结构、操作系统设计、虚拟化乃至高级应用调优中的复杂权衡提供一个强大的量化视角。读完本文,你将不仅理解 E.A.T 是什么,还将明白为何它是现代计算中最基本的概念之一。

原理与机制

想象你有一个藏有数百万册图书的图书馆,但你的私人图书管理员只有一个神奇但非常小的记事本。在取任何书之前,你都会问图书管理员:“《虚拟地址历险记》这本书在哪里?”大多数时候,图书管理员已将位置记在了记事本上,你可以直接走向书架。但有时,记事本上对于这个书名是空白的。这时,图书管理员必须匆忙跑到图书馆后面的一个巨大的、多卷本的卡片目录处,查找书的位置,为下次使用把它写在记事本上,然后告诉你去哪里。

平均来说,你拿到一本书需要多长时间?这既不是使用记事本的快速时间,也不是搜索卡片目录的慢速时间。它介于两者之间,并且完全取决于图书管理员的记事本上有答案的频率。这个简单的想法就是我们所说的​​有效访问时间 (EAT)​​ 的核心。它是我们在现代计算机中为每一次内存访问所付出的时间上的平均代价。

完美内存的代价:地址翻译

现代计算机施展了一个绝妙的戏法。它们为每个程序提供各自私有的、广阔而纯净的内存空间,称为​​虚拟内存​​。这可以防止程序相互干扰,也让程序员的工作轻松许多。但这只是一种幻象。在其之下,是有限且共享的物理内存芯片池,即​​物理内存​​。

每当你的程序试图访问一个内存位置——比如其虚拟世界中的 address 1000——处理器都必须将该虚拟地址翻译成一个物理地址,比如某个特定内存芯片上的 address 54321。这个翻译过程就相当于计算机查找一本书的位置。

如果处理器对每一次内存访问(现代处理器每秒执行数十亿次)都必须执行一个复杂的多步查找,系统就会陷入停顿。为了避免这种情况,硬件设计者创造了那个神奇的记事本:一个小型、极速的缓存,称为​​转译后备缓冲器 (TLB)​​。TLB 存储了最近使用过的虚拟到物理地址的翻译。

一场概率游戏:有效访问时间的核心

每一次内存访问都变成了一场由概率法则支配的简单机会游戏。可能的结果有两种:

  1. ​​TLB 命中 (TLB Hit):​​ 在 TLB 中找到了翻译。这是快速路径。时间成本是检查 TLB 的极短时间,加上访问物理内存的时间。我们称这个总时间为 ThitT_{\text{hit}}Thit​。

  2. ​​TLB 未命中 (TLB Miss):​​ 翻译不在 TLB 中。这是慢速路径。处理器必须执行一次​​页表遍历 (page table walk)​​,这涉及从慢得多的主存中读取一个名为页表的数据结构来找到翻译。完成这次遍历后,它才能最终访问所需的数据。总时间成本是 TLB 查找时间,加上页表遍历时间,再加上内存访问时间。我们称之为 TmissT_{\text{miss}}Tmiss​。

有效访问时间就是这两个结果的期望值,或者说加权平均值。如果 TLB 命中的概率(​​命中率​​)是 hhh,那么未命中的概率就是 (1−h)(1-h)(1−h)。这个源于期望值第一性原理的公式是:

EAT=h⋅Thit+(1−h)⋅TmissEAT = h \cdot T_{\text{hit}} + (1-h) \cdot T_{\text{miss}}EAT=h⋅Thit​+(1−h)⋅Tmiss​

让我们考虑一个简单的模型,其中 TLB 查找非常快,并且与其他操作并行发生。一次内存访问的成本为 tmt_mtm​。在 TLB 命中的情况下,总时间只是一次内存访问,所以 Thit=tmT_{\text{hit}} = t_mThit​=tm​。在未命中的情况下,我们需要一次访问页表和另一次访问数据,所以 Tmiss=2tmT_{\text{miss}} = 2t_mTmiss​=2tm​。公式变为:

EAT=h⋅tm+(1−h)⋅(2tm)=(2−h)tmEAT = h \cdot t_m + (1-h) \cdot (2t_m) = (2 - h)t_mEAT=h⋅tm​+(1−h)⋅(2tm​)=(2−h)tm​

这个简洁的小表达式 向我们揭示了一个深刻的道理:平均访问时间与命中率直接相关。0.90.90.9 的命中率得出 1.1tm1.1 t_m1.1tm​ 的 EAT,而 0.990.990.99 的命中率则得出 1.01tm1.01 t_m1.01tm​。命中率的微小变化对性能有显著影响。

未命中的剖析:导航页表

“页表遍历”期间究竟发生了什么?为了管理广阔的虚拟地址空间,页表本身通常被分解为多个级别,就像一个分层的文件系统。为了找到一个翻译,处理器可能需要查看第一级表,它指向第二级表,第二级表又指向第三级,依此类推。如果页表有 LLL 个级别,一次 TLB 未命中可能需要 LLL 次独立的内存访问才能找到翻译,然后才能开始最终的数据访问。

在这种情况下,未命中的时间变得大得多:Tmiss=tT+L⋅tm+tmT_{\text{miss}} = t_T + L \cdot t_m + t_mTmiss​=tT​+L⋅tm​+tm​,其中 tTt_TtT​ 是 TLB 查找时间。EAT 公式现在反映了这种增加的惩罚:

EAT=tT+(1+L(1−h))tmEAT = t_T + (1 + L(1-h))t_mEAT=tT​+(1+L(1−h))tm​

这告诉我们,EAT 对两件事极其敏感:命中率 hhh 和未命中惩罚,后者主要由页表深度 LLL 决定。

层层递进的层次结构

当然,现实世界甚至更为复杂。但其美妙之处在于,同样的期望值原理在各个层次上一再适用。

  • ​​为“规则手册”设置缓存:​​ 如果页表条目本身被缓存了会怎样?现代处理器拥有快速的数据缓存,可以存储部分页表。在 TLB 未命中时,处理器首先检查这个缓存中是否有页表条目。一次“PTE 缓存命中”比访问主存要快得多。因此,页表遍历的期望时间就变成了这些新可能性的加权平均值。

  • ​​多个记事本:​​ 为什么只用一个 TLB?系统通常有一个微小、极速的一级 (L1) TLB 和一个更大、稍慢的二级 (L2) TLB。内存访问首先检查 L1。如果未命中,它会检查 L2。只有当两者都未命中时,才会执行完整的页表遍历。EAT 的计算只需扩展以包含三种结果:L1 命中、L1 未命中/L2 命中,以及 L1/L2 未命中,每种都有其自身的概率和时间成本。

  • ​​不同类型的访问:​​ 并非所有内存访问都是生而平等的。从内存中取指令的访问模式——因而其 TLB 命中率——可能与为计算加载数据的访问模式不同。处理器通常有独立的用于指令的 TLB (I-TLB) 和用于数据的 TLB (D-TLB)。系统的整体 EAT 就是指令 EAT 和数据 EAT 的加权平均值,权重基于程序中每种访问类型的比例。

  • ​​不同岛屿上的内存:​​ 在拥有多个处理器插槽的大型服务器中,CPU 可以非常快速地访问其直接连接的本地内存 (tlocalt_{\text{local}}tlocal​)。但访问连接到另一个插槽的内存则较慢,因为请求必须跨越一个互连 (tremotet_{\text{remote}}tremote​)。这被称为​​非一致性内存访问 (NUMA)​​ 架构。现在的 EAT 依赖于访问本地内存的概率 ppp:EAT=p⋅tlocal+(1−p)⋅tremoteEAT = p \cdot t_{\text{local}} + (1-p) \cdot t_{\text{remote}}EAT=p⋅tlocal​+(1−p)⋅tremote​。对于在这些机器上编程的程序员来说,确保数据被放置在使用它的线程的本地(增加 ppp)是一项关键的性能调优任务。

在每一种情况下,结构都是相同的:一系列 (概率) x (时间成本) 项的总和,其层次深度与硬件本身一样。

当游戏被操控时:颠簸与缺页

EAT 模型不仅是一个描述符;它还是一个强大的性能悬崖预测器。该模型假设有相当高的命中率,但当 hhh 暴跌至接近零时会发生什么?

这种情况可能在一种称为​​颠簸 (thrashing)​​ 的情境中发生。TLB 只能同时容纳一定数量内存的翻译,这个量被称为 ​​TLB 覆盖范围 (TLB Reach)​​(TLB 条目数 × 页面大小)。如果一个程序的​​工作集​​——它正在活跃使用的内存页面集合——远大于 TLB 覆盖范围,那么这个程序就注定要失败。当它遍历完其数据时,所有旧的 TLB 条目都已被驱逐,这保证了下一次访问将是未命中。

考虑一个程序以步长等于一个内存页面的方式遍历一个巨大的数组。每次访问都是针对一个全新的页面。页面 1 的 TLB 条目被加载,然后是页面 2,以此类推。如果程序访问的独立页面数量超过了 TLB 中的条目数,那么当它再次需要页面 1 时,那个翻译早已消失。命中率 hhh 趋近于零,几乎每次访问都要付出缓慢的、完整的未命中惩罚。性能不仅仅是下降;它是断崖式下跌。

一个更具灾难性的事件是​​缺页 (page fault)​​。这是一种 TLB 未命中,其中页表显示所需数据根本不在物理内存中。它位于一个速度慢得多的存储设备上,如固态硬盘 (SSD) 或硬盘。此时,操作系统必须介入,在内存中找到一个空闲位置,从磁盘加载数据,更新页表,然后让程序恢复执行。

为此付出的时间成本是天文数字。一次主存访问可能需要 50 纳秒(50×10−950 \times 10^{-9}50×10−9 秒)。一次来自 SSD 的缺页服务可能需要 100 微秒(100×10−6100 \times 10^{-6}100×10−6 秒),而来自硬盘的可能需要 10 毫秒(10×10−310 \times 10^{-3}10×10−3 秒)。这是一个 2,000 到 200,000 倍的减速因子!即使是极小的缺页概率 (pfp_fpf​) 也可能完全主导 EAT。一个常见的经验法则是,当花在缺页上的时间(pf⋅tdiskp_f \cdot t_{\text{disk}}pf​⋅tdisk​)与花在正常内存访问上的时间相当时,性能就变得受限于磁盘。

工程师的视角:我们能做什么?

EAT 公式的美妙之处在于,它不仅描述了问题,还指明了解决方案。要提高性能,我们必须降低 EAT。公式告诉我们,有两种基本方法可以做到这一点:

  1. ​​提高命中率 (hhh):​​ 更高的命中率意味着我们更频繁地走快速路径。硬件设计者通过构建更大或更智能的 TLB(例如,使用更好的替换策略)来实现这一点。程序员则可以通过改善​​数据局部性​​来做到这一点——编写以紧凑、可预测的方式访问内存的代码,从而保持工作集小且对 TLB 友好。

  2. ​​降低未命中惩罚 (TmissT_{\text{miss}}Tmiss​):​​ 如果我们无法避免未命中,至少可以使其更快。这可以通过在层次结构中增加层次(L2 TLB、PTE 缓存)或使用更浅的页表(这可能涉及使用更大的页面大小)来实现。降低缺页的最终惩罚是我们拥有越来越快的 SSD 的原因。

敏感性分析甚至可以告诉我们应该转动哪个旋钮。通过对 EAT 求关于命中率的偏导数 ∂EAT∂h\frac{\partial EAT}{\partial h}∂h∂EAT​,我们发现性能的改善与我们所避免的未命中惩罚成正比。如果未命中惩罚巨大,那么即使命中率有微小的提升,也能带来巨大的性能增益。

从一场简单的机会游戏出发,我们构建了一个框架,它解释了硬件架构、操作系统和应用软件之间错综复杂的相互作用。有效访问时间不仅仅是一个公式;它是一个统一的原则,使我们能够对支撑着所有现代计算的复杂内存系统的性能进行推理、预测并最终加以控制。

应用与跨学科联系

在上一章中,我们剖析了内存访问的机制,得出了一个既简洁优美又功能强大的有效访问时间公式。但一个孤立的公式只是一个奇物;它的真正价值,它的美,在于我们用它作为透镜来观察世界时才会显现。结果的加权平均这一概念是大自然最钟爱的技巧之一,而在计算领域,有效访问时间(EAT)是我们理解硬件与软件之间复杂舞蹈的最锐利的工具。它让我们能够超越单纯的描述,开始进行预测、设计和工程。它是从建筑师的蓝图通往用户速度体验的桥梁。

现在,让我们踏上一段旅程,看看这个思想将我们引向何方。我们将看到它如何指导处理器核心的设计,如何揭示操作系统决策的隐藏成本,以及它甚至如何影响像机器学习这样遍布全球的现代应用的结构。

机器之心:架构与设计权衡

想象一下,你是一名工程师,正在绘图桌前勾画一款新处理器。你做的每一个决定都是一种权衡。在内存系统中,这一点尤为明显。考虑一下转译后备缓冲器,我们那个小小的地址翻译缓存。我们应该把它做大,以容纳更多的翻译并实现高命中率吗?还是应该把它做小,以便它能极快地运行并消耗更少的电力?

这不是一个哲学问题;这是一个 EAT 可以回答的量化问题。一个更大的 TLB 可能每次查找都稍慢一些,消耗更多能量,但其更高的命中率意味着我们能更频繁地避免代价高昂的完整页表遍历。相反,一个更小、更快的 TLB 在命中时更敏捷,但会遭受更频繁的未命中。通过为每种设计计算 EAT,我们可以看到净效应。但在一个由电池供电设备和电费至关重要的大型数据中心组成的世界里,性能并非一切。我们还可以计算每次内存访问的平均能耗。通过将这些结合起来,我们可以评估一个更全面的指标:能量延迟积 (EDPEDPEDP),它就是 EAT 乘以平均能量。通常,纯粹追求速度的最佳设计,并非在性能和效率之间取得最佳平衡的设计。掌握了这些计算的工程师可以做出合理的选择,找到那个最佳点,即昂贵的页表遍历的大幅减少,足以弥补每次访问查找成本的微小增加。

但为什么要仅仅对未命中做出反应?我们能更主动一些吗?如果硬件能够检测到一种模式——比如说,一个程序正以可预测的方式步进访问内存——并开始在翻译被请求之前就将它们预加载到 TLB 中呢?这样的“预取器”听起来是个好主意,但究竟有多好?它不会是完美的。它会有一定的覆盖率(它尝试预取多少比例的未命中)和一定的准确率(它的预测有多大频率是正确的)。这些参数中的每一个都会影响 TLB 未命中率,通过将新的、更低的未命中率代入我们的 EAT 公式,我们可以精确地量化加速效果。我们可以决定预取硬件增加的复杂性是否值得由此带来的性能增益。

最后,将内存系统的性能与整个处理器的性能联系起来至关重要。EAT 提高 5% 并不一定意味着程序快 5%。为什么?因为并非每条指令都会触及内存。处理器花费大量时间进行算术或其他内部操作。整体性能通常以每指令周期数 (CPI) 来衡量。一个典型的程序可能有一个用于其非内存工作的基准 CPI0CPI_0CPI0​。总 CPI 是这个基准加上内存访问的平均惩罚。这个惩罚就是访问内存的指令所占的比例 fmf_mfm​,乘以每次访问的平均时间(EAT),再转换为时钟周期。完整的方程变为 CPI=CPI0+fm⋅EAT⋅FCPI = CPI_0 + f_m \cdot EAT \cdot FCPI=CPI0​+fm​⋅EAT⋅F,其中 FFF 是时钟频率。这向我们展示了内存子系统的改进是如何传播或被稀释,从而影响整个系统的最终性能的。

乐团指挥:操作系统的角色

如果说硬件是乐团,那么操作系统就是指挥,它的决策对性能有着深远的影响。EAT 的概念让我们能够听到并衡量指挥棒的效果。

现代计算的奇迹之一是多任务:能够同时运行多个程序。操作系统通过在进程之间快速切换,给每个进程一小片时间来实现这一点。但这种“上下文切换”有其隐藏成本。由于每个进程都有自己独特的虚拟到物理地址映射,操作系统必须在切换时刷新 TLB,以防止新进程意外使用旧进程的翻译。结果呢?新进程以一个“冷”的 TLB 开始。它的最初几次内存访问几乎肯定是未命中,每一次都要付出页表遍历的全部代价,直到 TLB 用其工作集翻译“预热”起来。

这是个大问题吗?我们可以找出来!通过知道上下文切换的速率和每次预热期间的强制性未命中次数,我们可以计算出每秒的总时间惩罚。通过将这个总惩罚摊销到处理器每秒进行的数十亿次内存引用上,我们就能得到每次内存访问增加的平均时间。这个摊销惩罚,直接加到我们的 EAT 上,是多任务征收的一种性能税。这种效应在某些操作系统设计中尤其明显,比如微内核,它们依赖于进程间频繁、快速的通信 (IPC)。每次 IPC 都可能触发一次地址空间切换,导致大量的 TLB 刷新和潜在的显著性能下降,而这可以使用 EAT 精确量化。即使是一个常见的事件,比如启动一个新程序(类 Unix 系统中的 fork-exec 模式),也会产生一连串的 TLB 未命中,其平均成本可以在程序的初始时间片内计算出来。

操作系统也可以在其他方面表现得更聪明。一个标准的内存“页面”通常很小,也许是 4 千字节。但如果一个程序正在使用一个巨大的内存块,比如一个 1 GB 的数组呢?用 4 KB 的页面来映射它将需要二十五万个页表条目,并且会污染 TLB。为了解决这个问题,现代系统支持“透明大页”,允许操作系统用一个单一的、巨大的页面(例如,2 兆字节)来映射大的内存区域。这极大地减少了所需的 TLB 条目数量,从而提高了命中率。但同样,没有免费的午餐。使用大页可能导致内存浪费(内部碎片),这反过来又可能使底层内存系统效率稍低,增加原始内存访问时间。EAT 提供了一个完美的框架来模拟这种权衡,平衡了更高 TLB 命中率的好处与增加碎片的惩罚,使我们能够推导出性能净变化的表达式。

扩展舞台:现代系统与新前沿

我们所讨论的原则可以很好地扩展到即使是最复杂的现代系统和应用中。

考虑一下虚拟化的世界,它是云计算的基础,我们在其中将整个操作系统作为“客户机”在“主机”系统内运行。客户机的虚拟地址如何转换为主机硬件上的真实物理地址?在旧系统中,这是通过一种名为“影子页表”的复杂软件技巧完成的。虚拟机监控程序(主机操作系统)会创建一个影子页表,直接从客户机虚拟地址映射到主机物理地址。在 TLB 未命中时,硬件会遍历这个影子表。如今,硬件通过 Intel 的扩展页表 (EPT) 或 AMD 的嵌套页表 (NPT) 等功能提供直接支持。在这里,一次 TLB 未命中会触发一个惊人的“二维”页表遍历:对于客户机页表遍历的每一步,硬件都必须首先对主机的嵌套页表进行一次完整的遍历,仅仅是为了找到客户机页表所在的物理位置!这听起来慢得可怕,而且确实如此。单次 TLB 未命中的内存访问次数可以从几次飙升到二十几次。通过为影子页表和硬件辅助的嵌套页表建立 EAT 模型,我们可以量化虚拟化带来的巨大性能成本,并理解为什么在虚拟化环境中拥有非常高的 TLB 命中率是绝对至关重要的。

“内存”的定义本身也在改变。它不再是单一的 DRAM 池。现在的系统采用分层内存:一种混合了超快(且昂贵)的 DRAM 和更慢、更便宜、高容量的非易失性内存 (NVM) 的结构。一个智能的操作系统会试图将频繁使用的数据保留在快速层中。这对整体性能有何影响?我们只需扩展我们的 EAT 模型。数据访问的时间不再是一个常数 tmemt_{\text{mem}}tmem​;它变成了一个加权平均值,θ⋅tDRAM+(1−θ)⋅tNVM\theta \cdot t_{\text{DRAM}} + (1-\theta) \cdot t_{\text{NVM}}θ⋅tDRAM​+(1−θ)⋅tNVM​,其中 θ\thetaθ 是命中快速 DRAM 层的访问比例。这个新项可以优雅地嵌入到我们现有的 EAT 公式中,使我们能够分析这些复杂的、异构的内存层次结构的性能。

也许最令人兴奋的是,这些概念一直延伸到技术栈的顶层,影响着应用程序的设计。想象一个庞大的机器学习模型在一个大到无法装入内存的数据集上进行训练。应用程序以“小批量 (mini-batches)”的方式处理数据。如果一个小批量太大,其所需的数据页将超过分配给该进程的物理内存帧。结果是一场灾难,称为颠簸 (thrashing),此时系统几乎所有时间都花在从磁盘换入换出页面上。缺页是最终的内存访问惩罚——比 DRAM 访问慢数百万倍。我们可以通过扩展我们的 EAT 公式来模拟这种情况,将缺页的概率和毁灭性的时间成本包含进去:EAT=tmem+pfault⋅tfaultEAT = t_{\text{mem}} + p_{\text{fault}} \cdot t_{\text{fault}}EAT=tmem​+pfault​⋅tfault​。“颠簸”正是我们对缺页概率 pfaultp_{\text{fault}}pfault​ 变得如此之高以至于 EAT 爆炸的情况的称呼。对于一名机器学习工程师来说,这不仅仅是一个学术上的好奇。通过对小批量大小、应用程序的内存“足迹”以及由此产生的缺页率之间的关系进行建模,他们可以计算出避免颠簸的最大批量大小。这使他们能够调整算法以最大化硬件利用率,而不会跌下性能悬崖,这是一个由内存层次结构基本原理指导的应用级优化的优美例子。

从 CPU 中最小的设计选择到云软件中最大的架构模式,有效访问时间的简单思想都充当着我们的向导。它证明了一个事实,即在科学和工程领域,最强大的工具往往是那些为描述支配我们世界的权衡提供了清晰、量化语言的工具。