MoreRSS

site iconJiaJe | 杰哥修改

清华大学计算机系博士生。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

JiaJe | 杰哥的 RSS 预览

AI 时代的本科 CS 教育随想

2026-04-12 08:00:00

AI 时代的本科 CS 教育随想

背景

前几天参加了系里的关于 AI 时代的 CS 教育的研究生论坛,在论坛上我分享了一些小的思考,也在论坛上得到了许多不同的想法,于是把一些想法记录下来,过一段时间再回来看看,到底 CS 教育应该怎么办。

叠甲

本文仅代表本人观点,不代表本系或本校的观点,请勿扩大解读!请不要让我上 AI 三大顶会(机、量、新),谢谢!但欢迎大家参与到这个讨论当中,因为目前谁也不知道未来应该怎么做。

现状

为了让读者了解背景,首先要知道前 AI 时代的 CS 教育大概是怎样的:本科的时候先上编程课,教大家各种编程语言,然后逐渐深入到各个领域,课上讲授知识点,课下通过工程训练来夯实,由于计算机是工科,这里面通过不断的工程实践来获取经验,是很重要的一个部分。这一部分学习过程很辛苦,但是确实很有效果,可以说几乎每一位系友都是这么锻炼过来的。

下面这一段,如果你还在读本科,请不要点开,点开了也请忘掉,按照老师的要求去做:

但是,现在 AI 时代来临,很多事情都发生了变化。首先,AI 编程能力很强,大一同学辛辛苦苦学完一年,然后发现自己写的代码还不如 AI 写得好写得快,内心的挫败感和对这种古法编程的学习方法的质疑是无与伦比的。这对课程的教学产生了很大的冲击,因为人很难克制自己的懒惰,面对巨大的诱惑,其实很难静下心去学习这些已经由 AI 掌握的基础课程。论坛上有同学做了个比喻,计算器被发明了以后,人类没有失去心算的能力,因为你为了去用好计算器,还是要知道这些基础知识,从小学起,然后到某一个年级告诉你可以用计算器,然后各种考试还可以出计算器没法解决的题目。但是,AI 的能力边界太大了,它能解决从简单到困难的各种问题,只是有一定的概率解决出来是错的。其次,即使是前几年我们还会觉得,专业核心课的大作业还很难由 AI 完成,似乎还能通过大作业的难度来倒逼大家学习,但在今年也纷纷沦陷,对于学生来说,只要愿意,完全可以自己不写一行代码,纯让 AI 写一个能通过所有测试的作业,自己完全不了解内部是怎么实现的,用很短的时间完成作业。而且还不好去举证,说这一定是 AI 写的。这一点在这次论坛上,不同课程的助教都做了类似的实验,证明了这一点。虽然发这篇博客可能会让一些本科同学看到,然后不好好写大作业,但还是希望更多教育工作者可以看到并参与讨论。如果你是正在上课的同学,就自觉忘记吧。

怎么办

那么,应该怎么办呢?在这里阐述一些我的观点。一个大的前提是,肯定不能完全禁止 AI,也不能完全依赖 AI,需要辩证地把 AI 引入到 CS 教育当中。

首先是关于 CS 教育要培养出什么样的人才。之前,我们要培养的一方面是工程师,在长时间的工程实践当中积累经验,通过自己的经验,可以打造出一个很完备的系统,功能完善,可靠安全。但其实细分看来,在系统的搭建当中,其实有偏向于顶层设计的架构师,也有偏向于具体实现的工程师。目前 AI 已经可以很快地针对一个给定的 Plan 去做实现,并且实现得还不错,但是从需求到 Plan 的这一步,其实还需要人类的专家知识,因为实际的需求往往很复杂,会有许多大模型没有学过的假设与背景,这需要架构师脑子里把架构想清楚,知道哪里应该怎么做,然后把一部分的工程实现外包给 AI,自己再保证它的实现质量,确保它忠实地实现了所设计的架构,并且实现的系统是可靠安全的。用 AI 写代码很容易,但是写出来复杂可靠的软硬件系统,依然不是容易的事情。另一方面是科学家,在科研方面,科研的品味(Taste)变得更加重要,因为许多科研,其工程量本来就很小,完全可以由 AI 代劳,那么谁能够找到正确的路径,谁才能更好地与 AI 协作,完成科研。换句话说,以后的每个科研工作者,可能自己都是通讯作者,手下是一堆 AI 博士生在做实验,自己提出研究的思路,由 AI 实现和写作,然后自己来保证整个过程的正确性和学术伦理。无论是哪个方向,重点都从以前的知道某个东西“是什么”,变成了“为什么”,进而能够判断“对不对”。论坛上有同学总结得好,人类会更多地变成一个鉴别器(Discriminator)。

那么,具体到课程上,应该怎么做呢?其实我也没有想太明白,需要在未来几年里通过实践来不断修正。目前的一些初步的想法主要有下面这几点:

首先,作业已经不再能区分同学,不能代表同学对知识的掌握情况,只能代表 AI 对知识的掌握情况。所以作业已经完全沦为 AI 的课后送分小练习,在目前这个卷绩点的氛围下,让大家都开开心心地拿作业满分,也是越来越普遍了。如果真的想要通过作业来督促同学进行学习,那就必须回归作为人类的基本功,就是通过更多的线下的口语、展示和对话,以最“人味”的方式对抗 AI 的“机味”。事实上,在目前这个时代,其实如何扩大自己的影响力,也是很重要的技能,真的是酒香也怕巷子深,如何能够让大家看到你,抓住大家的注意力(Attention),很多时候会比你做出来的东西有多好更重要。这些能力,其实是值得通过作业的设计来培养的。我在本科的时候,尝试选了一次演讲的课程,当时看到作业要求,人直接麻了:需要每个人在班级所有人面前做演讲,这对于当时还比较社恐的我,由于太过害怕直接退课了。现在想想,其实都是小意思,当你迈出那一步以后,会发现懂得大大方方展示自己,真的是很重要的能力,是 AI 暂时还无法取代的能力。

既然作业沦陷了,那么,怎么打分呢?难道让每个人都能拿到满绩点?几年前,我在和大一新生聊天,他们就对这个打分的事情感到困惑,因为在目前这个绩点膨胀的时代里,好像很多课程拿满绩点都是天经地义的事情,如果你这个课不给我满绩点,我就要给你打教评低分。但是,又有不少东西和绩点挂钩,奖学金,保研等等。老师当然可以撒手不管,让所有人满绩点,但这只是让竞争延后、转化为其他领域了而已,不比绩点,那就比谁更能在本科的时候做科研,打比赛等等。另一方面,打分也是一个很重要的督促学习的手段,还是一样的前提,人类是很难抵抗自己的懒惰的,如果不是为了毕业以后有更好的发展,可能会有很多人放弃毕业、放弃学习。以前,为了能够顺利毕业,还会咬咬牙做一些比较困难的学习,甚至可能是自己不喜欢的;现在可以用 AI 糊弄了,那就糊弄过去,反正分数不错,能给父母交差,大环境也不好,然后就陷入了虚无主义,一泻千里。所以,似乎考试成为了最后的防线,还能在一定程度上督促学习。

但其实考试也受到了巨大的冲击。第一个问题就是,考试是否允许使用 AI 呢?许多 CS 课程,未来都会或多或少地引入一些 AI,那么学生对 AI 的掌握程度,也是一个需要考核的能力。但目前不同厂商的 AI 的可用性与性能差距过大,“AI 平权”会成为一个新的问题,我们希望比的是谁更会用 AI,而不是谁能用上更好的 AI。就像高考作文要考虑贫富差距一样,本科课程的考试也会面临类似的问题。一种可能性是在考试的时候提供统一的 AI 访问,但目前 AI 生态还是比较混乱,指定一个 AI 让大家用,其实也很容易出现与学生平时使用工具或生态不兼容的问题,而且学校自己部署一个同时几百上千人同时用的 AI 服务,也不是一件容易的事情,希望未来有云厂商可以提供类似的服务,并且能够控制住成本,其实就是一个持续两小时的上百 QPS 的专属推理服务。如果要类比的话,其他一些学科允许使用计算器,出题的时候可以规避,但 AI 能做的事情太杂了,其实很难针对。

另一方面,如果禁止 AI 的话,也有很多问题。首先是没法考察学生的 AI 使用能力,这个在未来会更加重要。其次,学生自己会比较难接受,先给了 AI 这么方便的工具,结果期末考试又要古法做一遍,最后结果可能就是学期中都在用 AI,只有考试前一周突击一下,考完就忘了,当然,好像现在很多人也是这样呢。而且课程很容易被贴上“不与时俱进”的标签,就如那些用十几年前课件的课程一样。现在这个过渡时期,大家都知道会变,但是怎么变并没有达成共识,所以一定会有一个阵痛期。如果你是刚上本科,或者马上要上本科的高中同学,那就要做好成为小白鼠的准备了。此外,随着本地模型的发展,如果让学生带电脑,即使不给联网,有更好的独立显卡的同学,事实上可以通过电脑配置的优势转化为分数,这也会带来新的不公平性。

当然,也不是毫无希望,比如前面说的,加一些有“人味”的考核,唯一的缺点是人力需求较大,难以扩展;或者允许使用 AI,但是必须提交完整的 AI 使用记录,这一点很多地方已经在实践;出题的时候,可能也要想办法去考察学生的思路,一些可以由 AI 完成的作业,不如就直接让学生用 AI 做,变成考察 AI 使用能力的题目。

讨论

以上基本是我在论坛上所展示的内容,下面也分享一些我在论坛上了解到的一些情况,以及所引发的思考。

首先,这次论坛不仅有大量的研究生助教参与,也有许多一线的教学老师参与了讨论。其实老师们感受到的冲击也很直接,因为可能就是从 2025-2026 开始,就有一批学生可以完全不接触古法编程,直接上手写代码,用一种完全不同的学习方法来学习各种课程内容。有的人可以很好地利用 AI 加速自己的学习,比如之前需要花费很多时间做的工程实践,现在可以在相同时间内用 AI 做更多的实践,一样可以获得很多甚至更多的实践经验。有的人就完全依赖 AI,可以糙快猛地完成很多事情,但对内部工作一概不知,能做的事情完全取决于 AI 的能力边界,同时自己又缺乏很多基础知识,可以说上知天文下知地理,但是四体不勤五谷不分。现在大家心里没底的就是,AI 的能力是否可以无限扩展,自己只需要站在 AI 的肩膀上,坐等 AI 发火箭上月球就行;还是需要脚踏实地,踩着地月天梯去月球。

咱也不知道答案,就在实践中前行吧。

AI 时代的本科(非 CS)教育随想

也顺带聊聊 CS 以外的教育吧,其实它们受到的冲击并不比 CS 少。但从某种意义来说,对于很多学科而言,AI 给每个人都带来了特别强大的工具,而且由于本来也不是学 CS 的,用 AI 能写出以前自己写不出来的代码,一下就把能力范围拓宽了。即便受到 AI 能力的限制,但反正自己也不是干这行的,本来也达不到那个上限,自然也就无所谓了。所以其实在 AI 时代,CS 以外的学科,都很值得学会怎么用 AI,给自己的学科赋能。比如论坛上有同学举了个例子,像写网站这种事,几天之内就能由来自不同学科、可能完全没有基础的同学,各自写出不同的校内交友相亲网站,而且还能让大模型帮忙做运维。好的想法、合适的商机、宣传和包装,这些才是更重要的,不用担心自己做不出来。

SDRAM 在不同访存模式下的带宽分析与实验

2026-03-26 08:00:00

SDRAM 在不同访存模式下的带宽分析与实验

背景

最近在和 @CircuitCoder 交流 SDRAM(通常简写为 DRAM,或更进一步简写为 DDR)的各种性能指标,于是想到利用现有的 DRAMSim3Ramulator2 做一些模拟测试,看看各种访存模式下可以实现峰值带宽的多少比例,再结合时序验证理论与模拟结果是否吻合。实验相关代码已开源至 jiegec/dram-bench

SDRAM 背景

首先简单回顾 SDRAM 的背景,我的知识库中有更详细的介绍,这里仅提炼几个便于理解后续内容的要点,完整的 SDRAM 介绍请移步知识库

  • SDRAM 由多级层次组成:
    • Channel:对应内存控制器的通道数量,通常每个 Channel 对应 64 位的数据总线
    • Rank:每个 Channel 内可能有多个 Rank,这些 Rank 共享总线
    • Bank Group:在 DDR4 引入,每个 Rank 有多个 Bank Group
    • Bank:每个 Bank Group 有多个 Bank
    • Row:每个 Bank 内部同时只有一个 Row 被激活
    • Column:激活的 Row 内,每个 Column 对应保存数据的 Cell
  • 如何读写 SDRAM 中的数据:
    • 首先根据数据的地址找到对应的 Channel/Rank/Bank Group/Bank/Row/Column,如:
      • Row 地址等于地址的 [33:18] 位,共 65536 个 Row
      • Rank 地址等于地址的 [17:17] 位,共 2 个 Rank
      • Bank 地址等于地址的 [16:15] 位,每个 Bank Group 内有 4 个 Bank
      • Bank Group 地址等于地址的 [14:13] 位,共 4 个 Bank Group
      • Column 地址等于地址的 [12:6] 位,共 1024 个 Column,每 8 个 Column 为一个 Burst
    • 通过 Activate 命令激活对应的 Row,如已激活可跳过,如当前激活了其他 Row,则需要先执行 Precharge 命令
    • 读写 Row 中保存的数据
  • SDRAM 中可能的性能瓶颈:
    • 在 Row 内连续访问数据很快,但如果要访问的数据位于不同 Row,就需要频繁执行 Activate 和 Precharge
    • SDRAM 有周期性的 Refresh,会导致部分时间无法访问数据
    • 额外的时序参数,对各类命令的顺序和间隔提出了约束:
      • tCCD:两次 Read 之间的最小间隔
      • tREFI:平均 Refresh 间隔
      • tRFC:Refresh 到下一个 Activate/Refresh 的最小间隔
      • tRTP:同一个 Bank 的 Read 到 Precharge 的最小间隔
      • tRP:同一个 Bank 的 Precharge 到下一个命令的最小间隔
      • tRCD:同一个 Bank 的 Activate 到 Read/Write 的最小间隔
      • tRAS:同一个 Bank 的 Activate 到 Precharge 的最小间隔
    • 如何计算峰值带宽:按接口速率(需考虑 DDR)乘以总线位宽可得峰值带宽,但由于上述瓶颈,实际无法达到该值

不同访存模式下的带宽分析与实验结果

顺序访存

首先考虑最经典的顺序访存,从地址 0 开始,以 64 字节为跨步访问。直觉上顺序访存似乎能实现最大带宽,但实际未必如此。例如以下测试结果中,DDR3 确实接近峰值,而 DDR4 则相差甚远:

  • 模拟 DDR3-1866,带宽达到峰值的 95.6%
  • 模拟 DDR4-3200,带宽达到峰值的 66.4%

DDR3

先分析 DDR3-1866 的模拟结果。实验中发出 50000 次 Read,其中 49772 次命中了已激活的 Row,无需额外 Activate 或 Precharge;此外还有 53 次 Refresh,228 次 Activate 和 222 次 Precharge。由于 DDR3-1866 的时序参数中,tCCD(两次 Read 之间的最小间隔)仅为 4 个周期,而一次 Burst 为 8 拍,因为 DDR 在时钟上下边沿都传输数据,所以一次 Read 正好占用数据总线 4 个周期,因此如果所有命令都是 Read,理论上可以完美衔接,不浪费任何带宽。既然实测只有 95% 左右,必定是其他命令引入了空泡:

  • Activate/Precharge:在顺序访存模式下,当一个 Row 的数据全部被访问后,就要进入下一个 Row,此时需要一次 Precharge 和一次 Activate。一个 Row 内有 2048 个 Column,意味着需要执行 \(2048/8=256\) 次 Read 才能遍历完一个 Row,因此 50000 次 Read 对应约 \(50000/256=195.3\) 次 Activate/Precharge。此外,Refresh 之前不能有激活的 Row,所以还需要少量额外的 Activate/Precharge 来配合 Refresh。
  • Refresh:DDR3 SDRAM 要求平均每 tREFI 时间进行一次 Refresh,这里 tREFI 等于 7800 个周期。考虑到有两个 Rank 需要分别 Refresh,因此在 209168 个周期内,需要进行约 \(209168\times2/7800=53.6\) 次 Refresh,与实际基本吻合。

尝试理论计算:每 \(x\) 次 Read,对应 \(x/256\) 次因 Row 结束带来的 Activate/Precharge,每轮 Activate/Precharge 带来 \(\mathrm{tRTP}+\mathrm{tRP}+\mathrm{tRCD}\) 的开销;此外在大约 \(4x\) 个周期内,每个 Rank 还需进行 \(4x/\mathrm{tREFI}\) 次 Refresh,每次 Refresh 带来约 \(\mathrm{tRFC}\) 的开销。将这些开销汇总,代入时序参数计算得到约 \(0.30x\) 的额外周期数。但实际上,Activate/Precharge 的部分开销可以通过 Bank 级交错来隐藏,比如在访问一个 Bank 的同时,提前对下一个 Bank 执行 Activate/Precharge,因此主要开销来自 Refresh。即使只考虑一个 Rank 内的 Refresh 开销,也有约 \(0.17x\) 的额外周期数,此时带宽约为峰值的 \(4x/(4x+0.17x)=0.959\) 倍,与实际测得的 95.6% 高度吻合。

DDR4

但 DDR4 的带宽比例显著下降,显然出现了新瓶颈。DDR4 相比 DDR3 一个重大改动是,原本一个 Rank 内只有 Bank,现在一个 Rank 包含多个 Bank Group,每个 Bank Group 内又有多个 Bank。这种分层是因为 Bank Group 内部的 tCCD 无法像 DDR3 那样保持在 4 个周期,只能退化为 5-8 个周期,这个新时序参数称为 tCCD_L(L 代表 Long);而 Bank Group 之间的 tCCD 仍能保持在 4 个周期。这意味着在 DDR4 下,只有交替对不同 Bank Group 发送 Read 命令,才能逼近峰值带宽;一旦局限在某个 Bank Group 内部,每次 Read 需间隔 tCCD_L 个周期,而每次 Read 仅提供 4 个周期的数据,导致巨大的带宽浪费。特别是在 DDR4-3200 速率下,tCCD_L 长达 8 个周期,数据总线有一半时间处于空闲。

为验证这一点,额外做了一个测试:不再单纯顺序访存,而是固定一个 Bank Group,交错读取不同 Bank,每个 Bank 内顺序访问 Row 和 Column,最终测得的带宽仅为峰值的 47.5%,这大致是考虑 Refresh 后数据带宽减半的结果。按前述 DDR3 的分析方法,计算此时 Refresh 的开销:每 \(x\) 次 Read,对应 \(8x\times\mathrm{tRFC}/\mathrm{tREFI}\) 的周期开销,代入时序参数约为 \(0.36x\),性能可达峰值的 \(4x/(8x+0.36x)=0.478\) 倍,与实际测试的 47.5% 高度吻合。

再回到顺序访存,为何能实现 66.4% 的峰值带宽?注意刚才假设访存总是映射到同一个 Bank Group,而 66.4% 突破了 47.5% 的极限,意味着必然访问了多个 Bank Group。此时需要深入分析地址映射方式,它采用的 RoChRaBaBgCo 映射方法,意味着从地址高位到低位依次是 Row、Channel、Rank、Bank、Bank Group 和 Column。因此随着地址每次增加 64,当 Column 溢出时就会访问下一个 Bank Group,两个 Bank Group 的 Read 命令可以交错执行,填补流水线空档。如果改变映射顺序,会得到不同结果:

  • 将 Bank Group(Bg)从地址低位挪到高位:
    • RoChRaBaCoBg:95.2%
    • RoChRaBaBgCo:66.4%
    • RoChRaBgBaCo:51.0%
    • RoChBgRaBaCo:49.4%
    • RoBgChRaBaCo:49.4%
    • BgRoChRaBaCo:49.4%
  • 进一步调整 Rank(Ra)的位置:
    • BgRoChBaCoRa:76.6%
    • BgRoChBaRaCo:57.5%
    • BgRoChRaBaCo:49.4%
    • BgRaRoChBaCo:47.5%

可见,Bank Group 地址越向高位移动,带宽越低,说明 Bank Group 交错的频率降低,性能随之下降;除了 Bank Group,Rank 之间也可以交错来掩盖部分延迟,但效果不如 Bank Group 交错显著;若两者都置于最高位,则退化为前述 47.5% 的带宽,即数据总线一半时间为空泡,再加上 Refresh 开销。

再回头看 DDR3 的分析:若只考虑 Refresh 带来的性能损耗,理论上限为 95.9% 带宽,实际达到 95.6%;若将 Activate/Precharge 的损耗也计入,理论上限仅为 \(4x/(4x+0.30x)=0.930\) 倍峰值,低于 95.6%,这说明在顺序访存模式下,通过地址映射在 Bank 或 Rank 层面实现了交错,从而隐藏了一部分延迟。为此再进行一组实验:仅访问一个 Bank 内的连续 Row 和 Column,测得带宽为峰值的 92.7%,与分析基本吻合。

小结

即使是简单的顺序访存,由于地址映射的存在,地址的连续变化会映射到不同的 SDRAM 层次,从而产生不同的性能表现。例如,在 DDR3 上,通过 Bank 和 Rank 的交错,可以隐藏一部分 Activate/Precharge 开销,仅剩 Refresh 开销无法避免;在 DDR4 上,根据地址映射的不同,若能在 Bank Group 层面实现细粒度的交错,就能充分利用更短的 tCCD_S 填满数据总线;否则会产生大量空泡,最坏情况下带宽降至 \(4/\mathrm{tCCD_L}\) 的比例。

随机访存

与顺序访存相对的另一个极端是随机访存:访问地址随机分布在各种 Bank 和 Row 上,此时 Row 命中率很低,几乎每次 Read 之前都需要 Precharge 和 Activate。在这种场景下,只能依靠 Bank 等层次上的交错来尽量掩盖开销。

DDR3

从 DDR3-1866 实验数据可以明显看出随机访存与顺序访存的差异:同样是 50000 次 Read,顺序访存仅有 228 次 Activate 和 222 次 Precharge,而随机访存则达到了 50086 次 Activate 和 50078 次 Precharge。接下来尝试理论分析该场景下的性能。首先,在每个 Bank 内,循环执行 Activate-Read-Precharge,这一组操作至少耗时 \(\mathrm{tRAS}+\mathrm{tRP}\);其次,若共有 8 个 Bank(为简化,固定只用一个 Rank),则这 8 个 Bank 可以交错执行 Activate-Read-Precharge 循环,理想情况下在 \(\mathrm{tRAS}+\mathrm{tRP}\) 时间内,8 个 Bank 各可完成一次 Read。代入时序参数,推测带宽为峰值的 \(4\times8/45=0.71\) 倍,但实际仅测到 46.0%,说明还存在其他瓶颈。事实上,这里需要考虑另一个时序参数 tFAW,其含义是在连续的 tFAW 时间内,最多只能有 4 次 Activate,且该限制跨 Bank 生效。因此即使有 8 个 Bank,实际也只能达到 \(4\times4/\mathrm{tFAW}=0.485\) 倍的峰值性能,与模拟值已较为接近,还需考虑 Refresh 开销。在另一组 DDR3-1866 时序参数下,tFAW 为 26 个周期,理论值为 \(4\times4/26=0.615\) 倍峰值,模拟结果为 57.7%,同样比较接近。

DDR4

DDR4-3200 的情况类似。当 tFAW 为 34 个周期时,理论值为 \(4\times4/34=0.471\) 倍峰值,模拟结果为 44.5%。尽管 DDR4-3200 有 4 个 Bank Group,每个 Bank Group 内含 4 个 Bank,总共 16 个 Bank,但在频繁 Activate 的场景下,依然受限于 tFAW。

小结

因此,即使是随机访存,只要能将请求分散到不同 Bank 上,性能依然可以接受。当然,随机访存的困境还体现在其他方面:缓存命中率低,且每个缓存行可能只用到少量数据就被丢弃。

对同一个 Bank 的随机访存

前面分析提到,Bank 交错可以在一定程度上掩盖 Activate-Precharge 的开销,但如果连这种掩盖也失效了,会发生什么?下面进行一组模拟,固定在某 Bank Group 内的一个 Bank 中,对其内部随机 Row 进行访问。

DDR3

仍以 DDR3-1866 时序参数为例进行理论分析:每 \(\mathrm{tRAS}+\mathrm{tRP}\) 时间只能完成一次 Read 操作,因此带宽仅为峰值的 \(4/(\mathrm{tRAS}+\mathrm{tRP})\) 倍。代入实际时序参数得 \(4/(32+13)=0.089\) 倍,模拟结果为 8.5%,与理论分析吻合。

DDR4

DDR4-3200 同样如此,代入时序参数得 \(4/(52+22)=0.054\) 倍,实际模拟结果为 5.2%,基本吻合。

小结

因此,若对同一 Bank 频繁进行随机访存,性能将显著下降。不过,由于地址映射机制的存在,Row 通常位于地址的高位,在实际应用中,绕过 Bank 与 Bank Group 对应的地址位、直接在 Row 地址位上进行随机访问的概率相对较低;然而一旦发生,对性能的影响将是毁灭性的。为此,研究人员提出了一些更为复杂的地址映射模式,例如在选取地址特定位的基础上引入异或运算,或进一步采用 Row Indirection Table,实现从逻辑 Row 到物理 Row 的映射,甚至动态交换特定 Row 中的数据。

讨论

上述测试均针对 DDR3 和 DDR4 的读请求展开,那么这些结论对写请求,或者对新一代的 DDR5 会产生怎样的影响呢?

首先,如果将读操作替换为写操作,上述分析基本依然成立:无论是读还是写,占用数据总线的时间相同,虽然时序上略有差异,但瓶颈主要在于 Activate、Precharge、Refresh 等操作,这些方面读和写并无本质区别。模拟结果也证实了这一点,读与写的带宽相差不大。

另一方面,DDR5 相较于 DDR4,主要有两点不同。其一,为支持更高频率,DDR5 将预取(Prefetch)位数从 8n 提升至 16n,即一次突发传输(Burst)包含 16 次传输,对应 8 个时钟周期。同时,为保证每次传输仍为 64 字节,原有的 64 位宽的 Channel 被拆分为两个 32 位宽的 SubChannel。因此,本质上 DDR5 将 Channel 数量翻倍,每个 Channel 内部的数据位宽减为 32 位,突发长度翻倍。这使得一次读写操作将占用数据总线 8 个周期,而不再是先前的 4 个周期,因此上述分析中的相关数值需作相应调整。其二,DDR5 进一步增加了 Bank Group 数量,由 4 组提升至 8 组,从而更容易触发 tCCD_S 而非 tCCD_L。

总结

简单总结上述分析,根据访存模式的不同:

  • 顺序访存:DDR3 基本可以打满带宽,DDR4 则取决于地址映射能否在 Bank Group 层面实现细粒度的交错
  • 随机访存:借助 Bank 交错,随机访存也能达到约一半的峰值带宽,主要受 tFAW 限制
  • 对同一个 Bank 的随机访存:无法隐藏 Activate-Read-Precharge 延迟,性能最低,受限于 tRAS+tRP

如果读者感兴趣,也可以在代码基础上添加其他访存模式,进一步探索性能表现。

Nginx 反代导致 SSE 延迟变高的问题与解决方法

2026-03-05 08:00:00

Nginx 反代导致 SSE 延迟变高的问题与解决方法

背景

最近有同学遇到这么一个问题:在 Nginx 反代后面搭了一个使用 SSE(Server Sent Events)机制的服务端,但客户端观察到请求延迟比较高,数据批量到达,而不是一行一行地出现。经过排查,发现是 Nginx 的 buffering 机制导致的。本文通过实验复现该问题,并探索了几种解决方法。

问题复现

为了复现这个问题,我 Vibe Coding 了一个测试服务端 server.py,监听 8080 端口,在 /events 路径下每秒发送一条 SSE 消息,共发送 5 次:

#!/usr/bin/env python3 """SSE server that sends 5 messages, one every second."""  import time from http.server import HTTPServer, BaseHTTPRequestHandler   class SSEHandler(BaseHTTPRequestHandler):  def do_GET(self):  if self.path == "/events":  self.send_response(200)  self.send_header("Content-Type", "text/event-stream")  self.end_headers()   for i in range(5):  message = f"data: Message {i + 1} at {time.time()}\n\n"  self.wfile.write(message.encode("utf-8"))  self.wfile.flush()  time.sleep(1)   self.wfile.close()  else:  self.send_response(404)  self.end_headers()   def log_message(self, format, *args):  print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {format % args}")   if __name__ == "__main__":  server = HTTPServer(("0.0.0.0", 8080), SSEHandler)  print("SSE server starting on http://0.0.0.0:8080")  server.serve_forever() 

启动服务端,使用 curl 访问 localhost:8080/events,可以看到每秒输出一条消息,没有延迟。接下来在 docker compose 里启动 Nginx,配置如下:

services:  nginx:  image: nginx:alpine  ports:  - "80:80"  volumes:  - ./nginx.conf:/etc/nginx/nginx.conf:ro  depends_on:  - sse-server  networks:  - sse-network   sse-server:  image: python:3.11-slim  command: python /app/server.py  volumes:  - ./server.py:/app/server.py:ro  ports:  - "8080:8080"  networks:  - sse-network  networks:  sse-network:  driver: bridge 

接着是 nginx 的配置:

events {  worker_connections 1024; }  http {  server {  listen 80;   location /events {  proxy_pass http://sse-server:8080;   # Add additional config later here  }  } } 

启动 docker compose,用 curl 分别访问 80 和 8080 端口的 /events,观察到以下现象:

  • 通过 80 端口访问 nginx:5 秒后一次性输出所有 data
  • 通过 8080 端口直接访问 server:每秒输出一条 data

这说明确实是 nginx 导致的。接下来测试几种解决方法。

解决方法

首先,查阅 nginx 的文档,可以看到它的描述:

Syntax: proxy_buffering on | off; Default: proxy_buffering on; Context: http, server, location  Enables or disables buffering of responses from the proxied server.  When buffering is enabled, nginx receives a response from the proxied server as soon as possible, saving it into the buffers set by the proxy_buffer_size and proxy_buffers directives. If the whole response does not fit into memory, a part of it can be saved to a temporary file on the disk. Writing to temporary files is controlled by the proxy_max_temp_file_size and proxy_temp_file_write_size directives.  When buffering is disabled, the response is passed to a client synchronously, immediately as it is received. nginx will not try to read the whole response from the proxied server. The maximum size of the data that nginx can receive from the server at a time is set by the proxy_buffer_size directive.  Buffering can also be enabled or disabled by passing “yes” or “no” in the “X-Accel-Buffering” response header field. This capability can be disabled using the proxy_ignore_headers directive. 

根据描述,可以想到一些可能的解决方法:

  1. Nginx 配置添加 proxy_buffering off;:工作
  2. 服务端在响应的 header 里添加 X-Accel-Buffering: noself.send_header("X-Accel-Buffering", "no")):工作

在一开头的场景里,由于中间的 Nginx 配置改起来比较麻烦,最后就用了第二种方法。回想起来,一开始思路走偏了,一直在往 cache 方向想,实际上是 buffering 的问题:Nginx 会先从 server 读取一大片数据,攒够了再发给 client,避免来回转发小段数据的开销,但 SSE 又希望有较低的延迟,这就冲突了。

小结一下:排查这类问题要理解 Nginx 的工作机制,找错方向可能很难定位;同时,利用 LLM 快速构建可复现的测试环境,有助于验证假设。

记一次软 RAID1 坏盘的恢复过程

2026-01-21 08:00:00

记一次软 RAID1 坏盘的恢复过程

背景

最近遇到一个运维场景,两个 SATA 盘组了一个 RAID1,Linux 的根系统也在上面,启动时能进内核,但是内核一直在报错 link is too slow to respond, please be patient 以及 COMRESET failed (errno=-16)。下面记录一下故障排查以及恢复的过程。

恢复过程

考虑到 Linux 系统也在 RAID1 上面,所以找了另一台机器,接上两个 SATA 盘,然后观察到,其中一个盘直接无法识别,另一个盘可以正常访问,但它分区表里只有一个分区,参与到了 md 组的 RAID1 当中。遇到盘坏了又是 RAID,第一反应是买一个新盘,然后重建 RAID。但是一通询价,发现最近硬盘价格涨的比较多,所以先尝试如何单盘启动。由于是 UEFI 启动,推测 ESP 在已经坏的那个盘上面,好的盘上并没有 ESP,但它唯一的分区已经占满了整个空间,所以第一步是对 RAID 分区缩容,这就需要:

  1. 首先用 fsck -f /dev/md0 && resize2fs /dev/md0 newsize 对根分区进行缩容
  2. mdadm --grow --size=newsize /dev/md0 对 RAID 进行缩容
  3. 停止 RAID:mdadm --stop /dev/md0
  4. 重新分区,缩小 RAID 分区大小:cfdisk /dev/sda
  5. 重新启动 RAID,更新 device size:mdadm --assemble --update=devicesize /dev/md0 /dev/sda1

这些步骤完成以后,就可以在空余的空间里建 ESP 分区了:建分区,mkfs.vfat,挂载到 /mnt/boot/efi(假设 /dev/sda1 已经挂载到了 /mnt),接着 arch-chroot /mnt(或者手抄 Archlinux Wiki),进去 grub-install,修改 /etc/fstab,重新 update-grub

这个过程中,踩了一些小坑,比如:

  1. 重启以后直接进 grub shell,没有菜单显示出来,后来发现是 UEFI 启动项里有之前的旧残留,导致 grub 没有能够正确加载 ESP 里面的 grub.cfg,如果在 grub shell 里手动 source 一下是正常的
  2. 如果不更新 device size,那么 assemble 的时候会说 does not have a valid v1.2 superblock 报错,实际上就是它记录了旧的分区大小,和新的分区大小不匹配,此时要强制修改它
  3. 最后买了个新盘,但是不够大:960GB vs 1TB,导致如果要重组 RAID1 还得再缩小一次已有的 RAID1 分区,之前缩小的时候只给 ESP 预留了足够的空间,但分区还不够小到能够在新盘里建一个相同大小的分区

IBM POWER9 微架构评测

2026-01-17 08:00:00

IBM POWER9 微架构评测

背景

IBM POWER8 之后,也来评测一下后续的 IBM POWER9 微架构。IBM POWER9 有 SMT4 和 SMT8 两种版本,我只有 SMT4 版本的测试环境,下列所有评测都是针对 SMT4 版本进行测试。

官方信息

IBM 关于 POWER9 微架构有如下公开信息:

下面分各个模块分别记录官方提供的信息,以及实测的结果。官方信息与实测结果一致的数据会加粗。

Benchmark

IBM POWER9 的性能测试结果见 SPEC

前端

L1 ICache

官方信息:32KB(SMT4)/64KB(split into 2 regions, SMT8)

为了测试 L1 ICache 容量,构造一个具有巨大指令 footprint 的循环,由大量的 nop 和最后的分支指令组成。观察在不同 footprint 大小下的 IPC:

测试环境是 SMT4 Core,所以只有 32KB 的容量。超出 L1 ICache 容量后,IPC 从 6 降低到了 4.7。相比 POWER8,容量不变,超出 ICache 容量后的 IPC 提高了。

测试过程详见测试代码

取指带宽

官方信息:32 bytes/cycle

为了测试实际的 Fetch 宽度,参考 如何测量真正的取指带宽(I-fetch width) - JamesAslan 构造了测试。

其原理是当 Fetch 要跨页的时候,由于两个相邻页可能映射到不同的物理地址,如果要支持单周期跨页取指,需要查询两次 ITLB,或者 ITLB 需要把相邻两个页的映射存在一起。这个场景一般比较少,处理器很少会针对这种特殊情况做优化,但也不是没有。经过测试,把循环放在两个页的边界上,发现 IBM POWER9 微架构遇到跨页的取指时确实会拆成两个周期来进行。

在此基础上,构造一个循环,循环的第一条指令放在第一个页的最后四个字节,其余指令放第二个页上,那么每次循环的取指时间,就是一个周期(读取第一个页内的指令)加上第二个页内指令需要 Fetch 的周期数,多的这一个周期就足以把 Fetch 宽度从后端限制中区分开,实验结果如下:

图中蓝线(cross-page)表示的就是上面所述的第一条指令放一个页,其余指令放第二个页的情况,横坐标是第二个页内的指令数,那么一次循环的指令数等于横坐标 +1。纵坐标是运行很多次循环的总 cycle 数除以循环次数,也就是平均每次循环耗费的周期数。可以看到每 8 条指令会多一个周期,因此 IBM POWER9 的前端取指宽度确实是 8 条指令即 32 字节。

为了确认这个瓶颈是由取指造成的,又构造了一组实验,把循环的所有指令都放到一个页中,这个时候 Fetch 不再成为瓶颈(图中 aligned),两个曲线的对比可以明确地得出上述结论。

随着指令数进一步增加,最终瓶颈在每周期执行的 NOP 指令数,因此两条线重合。

测试过程详见测试代码

L1 ITLB

为了测试 L1 ITLB 的容量,构造 b 序列,每个 b 在一个单独的页(64KB 的页大小)中,观察 b 的性能:

可以看到明显的 256 pages 的拐点,对应了 256 entry 的 L1 ITLB。CPI 从 3 升高到了 28。相比 POWER8 的 64-entry L1 ITLB 容量有所提升。

测试过程详见测试代码

BTB (aka Branch Target Address Calculator, BTAC)

官方信息:1 cycle latency

Return Address Stack

构造不同深度的调用链,测试每次调用花费的时间,得到如下测试结果:

可以看到 64 的拐点,对应的就是 RAS 的大小。

测试过程详见测试代码

CBP (Conditional Branch Predictor)

官方信息:BHT(3 cycle redirect) + TAGE(4 components, 5 cycle redirect), 256-bit LGHB(long global history vector)

Dispatch

官方信息:6 instructions per SMT4, 12 instructions per SMT8

后端

ROB (aka ICT)

官方信息:256 entries per SMT4 core

把两个独立的 long latency pointer chasing load 放在循环的头和尾,中间用 NOP 填充,当 NOP 填满了 ROB,第二个 pointer chasing load 无法提前执行,导致性能下降。测试结果如下:

拐点在 256 附近。相比 POWER8 的 28*6=168 有所提升

测试过程详见测试代码

Issue Queue

官方信息:54 instructions per SMT4 core, 108 instructions per SMT8 core

L1 DCache

官方信息:32KB(SMT4)/64KB(SMT8, split into two regions)

L1 DTLB

用类似测 L1 DCache 的方法测试 L1 DTLB 容量,只不过这次 pointer chasing 链的指针分布在不同的 64KB page 上,使得 DTLB 成为瓶颈:

可以看到 256 Page 出现了明显的拐点,对应的就是 256 的 L1 DTLB 容量。没有超出 L1 DTLB 容量前,Load to use latency 是 4 cycle。L1 DTLB 容量相比 POWER8 的 48(ST)/96(SMT) 有所提升,和 POWER8 的 256-entry L2 DTLB 容量相同。

测试过程详见测试代码

L2 Cache

官方信息:8-way 512KB L2 cache

L3 Cache

官方信息:20-way 10MB eDRAM L3 cache per core

Prefetcher

参考 Battling the Prefetcher: Exploring Coffee Lake (Part 1) 的方式,研究预取器的行为:分配一片内存,把数据从缓存中 flush 掉,再按照特定的访存模式访问,触发预取器,最后测量访问每个缓存行的时间,从而得到预取器预取了哪些缓存行的信息。

首先是连续访问若干个 128B cacheline,观察哪些被预取了进来:

预取的行为相比 POWER8 更加激进:有更多的缓存行被预取到了更近的 L1(或者是 L2?)。

如果是访问了几个分立的缓存行,有时会表现出 Next 3 Line 的行为,但都是到 L3:

测试过程详见测试代码

IBM POWER8 微架构评测

2026-01-15 08:00:00

IBM POWER8 微架构评测

背景

之前评测了很多 AMD64 和 ARM64 指令集的处理器,这次也来评测一下 PPC64LE 指令集的 IBM POWER8 微架构。

官方信息

IBM 关于 POWER8 微架构有如下公开信息:

下面分各个模块分别记录官方提供的信息,以及实测的结果。官方信息与实测结果一致的数据会加粗。

Benchmark

IBM POWER8 的性能测试结果见 SPEC

前端

L1 ICache

官方信息:32 KB, 8-way set associative

为了测试 L1 ICache 容量,构造一个具有巨大指令 footprint 的循环,由大量的 nop 和最后的分支指令组成。观察在不同 footprint 大小下的 IPC:

超出 L1 ICache 容量后,IPC 从 6 降低到了 2.4。其中 6 IPC 来自于,IBM POWER8 在 ST 模式下每周期可以发射 8 条指令,但其中分支指令最多两条,非分支指令最多六条,所以执行 NOP 指令的 IPC 只能达到 6。

测试过程详见测试代码

L1 ITLB (aka Instruction Effective to Real Address translation Table, IERAT)

官方信息:64-entry, fully associative

为了测试 L1 ITLB 的容量,构造 b 序列,每个 b 在一个单独的页(64KB 的页大小)中,观察 b 的性能:

可以看到明显的 64 pages 的拐点,对应了 64 entry 的 L1 ITLB。

测试过程详见测试代码

BTB (Branch Target Buffer)

官方信息:无 BTB,总是通过 3 周期延迟的 Fetch + Decode(Branch Scan) 来得到分支指令的目的地址,靠 SMT 来填补流水线的气泡。

实测也是如此,对于连续执行多个 b 指令的情况,每条 b 指令都需要 3 周期。

Return Address Stack (aka Link Stack)

官方信息:32-entry(ST/SMT2)/16-entry(SMT4)/8-entry(SMT8) Link Stack per thread,也就是说总容量是 64,但每个线程只能用一部分

构造不同深度的调用链,测试每次调用花费的时间,得到如下测试结果:

可以看到 32 的拐点,对应的就是 ST 模式下 RAS 的大小。在同一个物理核上的其他三个逻辑核分别运行 stress,就测得 SMT4 模式下的 RAS 大小 16:

类似地,在其余七个逻辑核上分别运行 stress 负载,得到 SMT8 模式下的 RAS 大小为 8:

测试过程详见测试代码

CBP (Conditional Branch Predictor)

官方信息:16K-entry LBHT, 16K-entry GBHT, 16K-entry GSEL,使用 21-bit GHV 记录全局分支历史,GSEL 用来选择由 LBHT 还是 GBHT 提供预测(通过 2-bit 饱和计数器),LBHT 采用 PC 索引,GBHT 和 GSEL 采用 PC+GHV 的哈希索引;此外,还支持把 conditional branch to +8 也就是只跳过一条指令的分支指令改写为 predication

IBP (Indirect Branch Predictor)

官方信息:256-entry local count cache, 512-entry global count cache,前者采用 PC 索引,后者采用 PC+GHV 的哈希索引,entry 内容是 30-bit 预测的目的地址加 2-bit 的 confidence(local count cache 的 entry 还有额外的 2-bit 饱和计数器用于选择 local 还是 global)

Dispatch

官方信息:按 Group 来 Dispatch,ST 模式下每周期一个 Group,每个 Group 最多 8 条指令(最多 2 条分支,最多 6 条非分支,且第二条分支必须是最后一条指令);SMT 模式下,每周期从两个线程各 Dispatch 一个 Group,每个 Group 最多 4 条指令(最多 1 条分支,3 条非分支)

后端

ROB (aka Global Completion Table, GCT)

官方信息:28-entry,ST 模式下每个 entry 对应一个 Group;SMT 模式下每个 entry 对应两个来自同一个线程的 Group;所以最多容纳 28*8=224 条指令;Commit 的粒度是 Group,ST 模式下每周期 Commit 一个 Group,SMT 模式下每周期 Commit 两个 Group

把两个独立的 long latency pointer chasing load 放在循环的头和尾,中间用 NOP 填充,当 NOP 填满了 ROB,第二个 pointer chasing load 无法提前执行,导致性能下降。测试结果如下:

拐点大致在 168 附近,因为每 6 条 NOP 指令对应一个 Group,所以只能容纳 28*6=168 条指令。

测试过程详见测试代码

Register File

官方信息:一共可以有 106 个 Inflight 的 Rename,由 GPR(General Purpose Register)和 VSR(Vector and Scalar Register)共享;GPR 分为两组,每组 124-entry;VSR 分为两组,每组 144-entry;还有额外的两组 SAR(Software Architected Registers),一组用于 GPR,一组用于 VSR;CR(Condition Register)单独 Rename(32-entry mapper)到 64-entry Architected Register File;XER(fiXed-point Exception Register)Rename(30-entry mapper)到 32-entry Architected Register File;LR,CTR 和 TAR 单独 Rename(20-entry mapper)到 24-entry Architected Register File;FPSCR(Floating Point Status and Control Register)单独 Rename 到 28-entry buffer。

Issue Queue

官方信息:15-entry Branch Issue Queue,8-entry Condition Register Queue,64-entry UniQueue 用于其他指令;每周期最多 Issue 10 条指令:1x Branch, 1x Condition Register Logical, 2x Fixed Point, 2x Load/Store/Fixed Point to LSU, 2x Load/Fixed Point to LU, 2x Vector-Scalar to VSU/DFU(Decimal Floating point Unit)/Crypto

执行单元

官方信息:2 个定点计算流水线(FX),2 个 Load/Store 流水线(LS/FX),2 个 Load 流水线(L/FX),4 个双精度浮点流水线(或 8 个单精度浮点流水线),2 个向量流水线(VMX),1 个密码学流水线(Crypto),1 个分支流水线(Branch),1 个条件寄存器流水线(CR),1 个十进制浮点数流水线,共 16 个;其中 2 个 Load/Store 流水线和 2 个 Load 流水线还能执行简单的定点计算

Load Store Unit

官方信息:共有四个 Pipeline,L0/L1 仅 Load,LS0/LS1 可 Load/Store, 3 cycle load-to-use latency

Load/Store (Reorder) Queue

官方信息:40-entry(128 Virtual)Store Reorder queue,44-entry(128 Virtual)Load Reorder Queue

Load to use latency

官方信息:3-cycle latency

实测在下列的场景下可以达到 3 cycle:

  • ldr 4, 0(4): load 结果转发到基地址,无偏移
  • ldr 4, 8(4):load 结果转发到基地址,有立即数偏移
  • ldx 4, 4, 6:load 结果转发到基地址,有寄存器偏移
  • ldx 4, 6, 4:load 结果转发到寄存器偏移

如果访存跨越了 128B 边界,则退化到 16 cycle。

L1 DCache

官方信息:64KB, 8-way set associative, 128B cache line, 4 read port, 1 write port,3 cycle load to use latency, store-through(写入会同时写 L1 DCache 和 L2),所以 store miss 不分配 cache line, 16 MSHR(aka Load Miss Queue)

构造不同大小 footprint 的 pointer chasing 链,测试不同 footprint 下每条 load 指令耗费的时间:

可以看到 64KB 出现了明显的拐点,对应的就是 64KB 的 L1 DCache 容量。第二个拐点在 512KB,对应的是 L2 Cache 的容量。第三个拐点是 3MB,对应的是 L1 DTLB 的容量:48*64KB=3MB

测试过程详见测试代码

Banking

官方信息:L1 DCache 由 16 个 macro 组成,每个 macro 是 16 个 bank,一共是 256 个 bank;sram 用的是 2R 或 1W,所以每个 bank 可以支持每周期 2R 或 1W

L1 DTLB (aka primary Data Effective-to-Real Address Translation, DERAT)

官方信息:48-entry(ST)/96-entry(SMT), fully associative

用类似测 L1 DCache 的方法测试 L1 DTLB 容量,只不过这次 pointer chasing 链的指针分布在不同的 64KB page 上,使得 DTLB 成为瓶颈:

可以看到 48 Page 出现了明显的拐点,对应的就是 48 的 L1 DTLB 容量。没有超出 L1 DTLB 容量前,Load to use latency 是 3 cycle。最终出现一个 18.8 cycle 的平台。

测试过程详见测试代码

L2 DTLB (aka secondary Data Effective-to-Real Address Translation, DERAT)

官方信息:256-entry(ST 模式下全可见,SMT 模式下每个线程只有一半可见), fully associative

继续扩大 DTLB 测试规模,可以看到在 256 处出现了新的拐点,其中 256 的地方出现周期数的骤降,是触发了 Linux 的大页合并功能:

关掉 THP(Transparent Huge Page) 后,周期数的骤降消失,256 的拐点之后周期数增加而不是减少:

测试过程详见测试代码

L3 TLB

官方信息:2048-entry, 4-way set associative, 4 concurrent page table walk

继续扩大 DTLB 测试规模,在 2048 处出现了拐点,注意要关闭 THP,否则拐点会消失,因为实际上没有用到 2048 个页:

测试过程详见测试代码

Prefetcher

官方信息:16-entry Stream Prefetcher,可以跨 4KB/64KB 页边界,用虚拟地址预取,可以预取到 L1/L2/L3

参考 Battling the Prefetcher: Exploring Coffee Lake (Part 1) 的方式,研究预取器的行为:分配一片内存,把数据从缓存中 flush 掉,再按照特定的访存模式访问,触发预取器,最后测量访问每个缓存行的时间,从而得到预取器预取了哪些缓存行的信息。

首先是连续访问若干个 128B cacheline,观察哪些被预取了进来:

可以看到后面有 12 个 cacheline 都被预取了,但是预取到了不同的 cache 层次,猜测距离越近的 4 个 cacheline 预取到 L1,更远的 2 个到 L2,其余的 6 个到 L3。

如果是访问了几个分立的缓存行,行为变成了 Next 3 Line:

测试过程详见测试代码