2026-03-26 08:00:00
最近在和 @CircuitCoder 交流 SDRAM(通常简写为 DRAM,或更进一步简写为 DDR)的各种性能指标,于是想到利用现有的 DRAMSim3 和 Ramulator2 做一些模拟测试,看看各种访存模式下可以实现峰值带宽的多少比例,再结合时序验证理论与模拟结果是否吻合。实验相关代码已开源至 jiegec/dram-bench。
首先简单回顾 SDRAM 的背景,我的知识库中有更详细的介绍,这里仅提炼几个便于理解后续内容的要点,完整的 SDRAM 介绍请移步知识库:
[33:18] 位,共 65536 个 Row[17:17] 位,共 2 个 Rank[16:15] 位,每个 Bank Group 内有 4 个 Bank[14:13] 位,共 4 个 Bank Group[12:6] 位,共 1024 个 Column,每 8 个 Column 为一个 Burst首先考虑最经典的顺序访存,从地址 0 开始,以 64 字节为跨步访问。直觉上顺序访存似乎能实现最大带宽,但实际未必如此。例如以下测试结果中,DDR3 确实接近峰值,而 DDR4 则相差甚远:
先分析 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% 左右,必定是其他命令引入了空泡:
尝试理论计算:每 \(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 相比 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 地址越向高位移动,带宽越低,说明 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-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-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 交错可以在一定程度上掩盖 Activate-Precharge 的开销,但如果连这种掩盖也失效了,会发生什么?下面进行一组模拟,固定在某 Bank Group 内的一个 Bank 中,对其内部随机 Row 进行访问。
仍以 DDR3-1866 时序参数为例进行理论分析:每 \(\mathrm{tRAS}+\mathrm{tRP}\) 时间只能完成一次 Read 操作,因此带宽仅为峰值的 \(4/(\mathrm{tRAS}+\mathrm{tRP})\) 倍。代入实际时序参数得 \(4/(32+13)=0.089\) 倍,模拟结果为 8.5%,与理论分析吻合。
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。
简单总结上述分析,根据访存模式的不同:
如果读者感兴趣,也可以在代码基础上添加其他访存模式,进一步探索性能表现。
2026-03-05 08:00:00
最近有同学遇到这么一个问题:在 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,观察到以下现象:
这说明确实是 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. 根据描述,可以想到一些可能的解决方法:
proxy_buffering off;:工作X-Accel-Buffering: no(self.send_header("X-Accel-Buffering", "no")):工作在一开头的场景里,由于中间的 Nginx 配置改起来比较麻烦,最后就用了第二种方法。回想起来,一开始思路走偏了,一直在往 cache 方向想,实际上是 buffering 的问题:Nginx 会先从 server 读取一大片数据,攒够了再发给 client,避免来回转发小段数据的开销,但 SSE 又希望有较低的延迟,这就冲突了。
小结一下:排查这类问题要理解 Nginx 的工作机制,找错方向可能很难定位;同时,利用 LLM 快速构建可复现的测试环境,有助于验证假设。
2026-01-21 08:00:00
最近遇到一个运维场景,两个 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 分区缩容,这就需要:
fsck -f /dev/md0 && resize2fs /dev/md0 newsize 对根分区进行缩容mdadm --grow --size=newsize /dev/md0 对 RAID 进行缩容mdadm --stop /dev/md0
cfdisk /dev/sda
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。
这个过程中,踩了一些小坑,比如:
does not have a valid v1.2 superblock 报错,实际上就是它记录了旧的分区大小,和新的分区大小不匹配,此时要强制修改它2026-01-17 08:00:00
继 IBM POWER8 之后,也来评测一下后续的 IBM POWER9 微架构。IBM POWER9 有 SMT4 和 SMT8 两种版本,我只有 SMT4 版本的测试环境,下列所有评测都是针对 SMT4 版本进行测试。
IBM 关于 POWER9 微架构有如下公开信息:
下面分各个模块分别记录官方提供的信息,以及实测的结果。官方信息与实测结果一致的数据会加粗。
IBM POWER9 的性能测试结果见 SPEC。
官方信息: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 的容量,构造 b 序列,每个 b 在一个单独的页(64KB 的页大小)中,观察 b 的性能:

可以看到明显的 256 pages 的拐点,对应了 256 entry 的 L1 ITLB。CPI 从 3 升高到了 28。相比 POWER8 的 64-entry L1 ITLB 容量有所提升。
官方信息:1 cycle latency
构造不同深度的调用链,测试每次调用花费的时间,得到如下测试结果:

可以看到 64 的拐点,对应的就是 RAS 的大小。
官方信息:BHT(3 cycle redirect) + TAGE(4 components, 5 cycle redirect), 256-bit LGHB(long global history vector)
官方信息:6 instructions per SMT4, 12 instructions per SMT8
官方信息:256 entries per SMT4 core
把两个独立的 long latency pointer chasing load 放在循环的头和尾,中间用 NOP 填充,当 NOP 填满了 ROB,第二个 pointer chasing load 无法提前执行,导致性能下降。测试结果如下:

拐点在 256 附近。相比 POWER8 的 28*6=168 有所提升
官方信息:54 instructions per SMT4 core, 108 instructions per SMT8 core
官方信息:32KB(SMT4)/64KB(SMT8, split into two regions)
用类似测 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 容量相同。
官方信息:8-way 512KB L2 cache
官方信息:20-way 10MB eDRAM L3 cache per core
参考 Battling the Prefetcher: Exploring Coffee Lake (Part 1) 的方式,研究预取器的行为:分配一片内存,把数据从缓存中 flush 掉,再按照特定的访存模式访问,触发预取器,最后测量访问每个缓存行的时间,从而得到预取器预取了哪些缓存行的信息。
首先是连续访问若干个 128B cacheline,观察哪些被预取了进来:

预取的行为相比 POWER8 更加激进:有更多的缓存行被预取到了更近的 L1(或者是 L2?)。
如果是访问了几个分立的缓存行,有时会表现出 Next 3 Line 的行为,但都是到 L3:

2026-01-15 08:00:00
之前评测了很多 AMD64 和 ARM64 指令集的处理器,这次也来评测一下 PPC64LE 指令集的 IBM POWER8 微架构。
IBM 关于 POWER8 微架构有如下公开信息:
下面分各个模块分别记录官方提供的信息,以及实测的结果。官方信息与实测结果一致的数据会加粗。
IBM POWER8 的性能测试结果见 SPEC。
官方信息: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。
官方信息:64-entry, fully associative
为了测试 L1 ITLB 的容量,构造 b 序列,每个 b 在一个单独的页(64KB 的页大小)中,观察 b 的性能:

可以看到明显的 64 pages 的拐点,对应了 64 entry 的 L1 ITLB。
官方信息:无 BTB,总是通过 3 周期延迟的 Fetch + Decode(Branch Scan) 来得到分支指令的目的地址,靠 SMT 来填补流水线的气泡。
实测也是如此,对于连续执行多个 b 指令的情况,每条 b 指令都需要 3 周期。
官方信息: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:

官方信息: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
官方信息: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)
官方信息:按 Group 来 Dispatch,ST 模式下每周期一个 Group,每个 Group 最多 8 条指令(最多 2 条分支,最多 6 条非分支,且第二条分支必须是最后一条指令);SMT 模式下,每周期从两个线程各 Dispatch 一个 Group,每个 Group 最多 4 条指令(最多 1 条分支,3 条非分支)
官方信息: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 条指令。
官方信息:一共可以有 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。
官方信息: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 流水线还能执行简单的定点计算
官方信息:共有四个 Pipeline,L0/L1 仅 Load,LS0/LS1 可 Load/Store, 3 cycle load-to-use latency
官方信息:40-entry(128 Virtual)Store Reorder queue,44-entry(128 Virtual)Load Reorder Queue
官方信息: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。
官方信息: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。
官方信息:L1 DCache 由 16 个 macro 组成,每个 macro 是 16 个 bank,一共是 256 个 bank;sram 用的是 2R 或 1W,所以每个 bank 可以支持每周期 2R 或 1W
官方信息: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 的平台。
官方信息:256-entry(ST 模式下全可见,SMT 模式下每个线程只有一半可见), fully associative
继续扩大 DTLB 测试规模,可以看到在 256 处出现了新的拐点,其中 256 的地方出现周期数的骤降,是触发了 Linux 的大页合并功能:

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

官方信息:2048-entry, 4-way set associative, 4 concurrent page table walk
继续扩大 DTLB 测试规模,在 2048 处出现了拐点,注意要关闭 THP,否则拐点会消失,因为实际上没有用到 2048 个页:

官方信息: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:

2025-12-25 08:00:00
经常看我博客的读者应该能看出来,我研究的主要是计算机系统结构方向,特别是处理器的微架构,几乎没有涉及到 AI 的内容,我也确实不喜欢 AI 研究,仅关注但不参与。但今年,因为各种 AI 技术尤其是 LLM 的发展,我确实成为了很多 AI 技术的用户,可以说 2025 年是我正经大规模用 AI 的元年,所以在年末做一个简单的总结。
我不想在这里给大模型厂商打广告,所以相关的名字我都会按照某 PDF 的方法进行打码,有需要的朋友可以自行查看实际的内容。
首先的一个冲击来自于 Vibe Coding。我写代码也有大概十五年了,一直都是坚持自己写代码,但今年从一些朋友那里了解到一些 Vibe Coding 的效果以后,也自己尝试了一下,确实能够感受到 Vibe Coding 对写代码的巨大冲击,我的心态也出现了一定的变化。Vibe Coding 并不复杂,其实就是用一些 Coding 客户端,配上 LLM 加一些 Tool Call,使得 LLM 可以自己编写、测试和运行代码。目前随着 LLM 能力的变强,Vibe Coding 逐渐成为了一个可以负担得起且效果不错的东西。结合实际的使用,以及受朋友们的一些启发,我目前已经用它进行了一些 Vibe Coding 尝试,例如:
目前给我的感觉是,LLM 借助各种 MCP Tooling,在很多事情上可以做的很好,但也有一些前提条件。第一是 LLM 需要有针对这个事情的知识,但如果它的知识停留在几年前,又做一些比较新的东西(例如 Typst 语法很多 LLM 就不会写),它就比较难写对;第二是,一定要给 LLM 反馈的路径,能够让它自产自纠自查,不然幻觉是很难避免的,一次写对的情况很少,有反馈和无反馈完全是两个表现;第三是,目前 LLM 做复杂事情需要大量的 Token,这就意味着 API 调用时间和开销都是不可忽略的因素,即使我用了比较便宜的 DeepSeek 模型,让 LLM 在后台跑几个小时,价格一样受不了。
举一个数据,我这个月在 DeepSeek 上已经花费了 200 多元,而这个月之前的所有时间加起来,也就不过 10 元。如果相同的 Token 数用在 Claude 上,这个价格不可想象。所以我也终于能理解,那些几百美刀一个月的订阅服务为啥有市场了。也是因为这个原因,我才会降本增效,通过订阅 GLM Coding Plan 去解决一些低频的 Vibe Coding 需求,但它的用量限制和并发限制都比较容易触发,所以才去 Vibe Coding 了一个 API 路由器,对于 GLM Coding Plan 用量以外的需求,再 Fallback 到 DeepSeek 上。
在使用 Vibe Coding 的过程中,我也有一些感受,就感觉我并不是在 Vibe Coding,而是在指挥一个水平飘忽不定的人在写代码。它有时候能精准地找到问题并写出正确的代码,有时候又注意力涣散,必须要我及时地打断它,让它按照我指定的方法去做。对于一些简单的代码,可能可以让它在后台跑,我去做一些别的事情,然后隔一段时间再看看它做得怎么样,有问题了,再提供及时的纠正。然后我就在想,这其实就是当领导吧,给钱让手下的人干活,不一定干的对,所以还得时不时地去纠正一下。某种意义来说,LLM 让每个人都有了成为领导,领导一群 LLM 干活的能力。我目前的工作流就是在 tmux 里挂几个 Qwen Code,连上几个配好的 MCP 服务器以及 API 路由器,然后时不时地看看它做的咋样,做得好就验收,让它 Git Commit,做得不好就让它改,时不时还得翻翻代码看看怎么帮它修。某种意义上,这和课后布置作业,给学生答疑也没啥本质上的区别,甚至 LLM 还更爱说话一点。
既然提到了答疑,也来谈谈教学。这种 Vibe Coding 的能力对于计算机教育的冲击无疑是巨大的,本来很多上课教的内容,AI 可以比较容易地完成,那学生可能就更倾向于让 AI 去完成了,换位思考一下,如果让我在 2025 年成为大一不会编程的新生,我也很难抵御这个诱惑。但是,锻炼代码和工程能力就欠缺了。这就对应一个很重要的问题,就是 AI 它到底是不是一种类似编译器、调试器或者编程语言的工具?我们说学生可以从编程语言而不是汇编学起,是因为它是一个很成熟很可靠的工具,你学会了高层次的工具就是会了,就可以用它做很多事情。AI 就很奇怪,它确实可以做很多事情,但又不总是可以完成,它好像是概率性的图灵完全,全看是否出现幻觉,所以它不是一个可靠的工具,但又是一个好用的工具。那么紧接的问题是,计算机教育,是要教出来真的会写代码的人,还是会用 AI 写代码就行?我目前没有答案,也不知道未来会怎么发展,只能慢慢走一步看一步了。但抛开计算机专业的教育,如果是对于计算机的通识教育,我觉得用 AI 写代码完全没有问题,毕竟对于更多人来说,能解决问题就可以,可不可靠,其实很多时候并不在考虑范围内。
我知道上面这段话可能会让读者有一些焦虑,但我觉得,它都这样了,就共存吧,反正焦虑也没有用,不如拥抱它。至于是否担心自己会被替代,我确实是不担心,目前它还不够专业,就算它再专业,它也没有身份证是吧。希望早日实现生产力极大富足,实现共同富裕,那就不用思考人是不是会被 AI 替代了。另外,高级编程语言出现了,那些写汇编的人去写高级语言,现在 Vibe Coding 来了,只是同一拨人又跑去做 Vibe Coding 罢了。持续学习才是最重要的。今年开始尝试 Vibe Coding 也是让我意识到,随着年龄增大,确实是没有当年对新事物接受得那么快了,这也让我有了一些反思,以后还是要多多接触新技术,一些过去的思维可能也要重新审视。
目前我对 Vibe Coding 的态度是,它不能替代我的思考,相反,我可以更多地思考一些更高层次的东西,而可以适当地把一些细节交给 AI。我也持续在自己写代码,特别是一些关键的部分,我还是无法信任完全由 AI 编写,毕竟它比人还懂得偷懒,经常写出来一些没有测试效果的测例,一看测例都过,一测全是 BUG。
我还会继续尝试和 LLM 协作,尽量保持高质量的代码产出,我认为这是用 Vibe Coding 的底线:用 AI 并不是写出烂代码的理由。以前我们有所谓的中文羞耻,觉得写了很多中文的项目的代码可能不靠谱,现在是所谓的 AI 羞耻,看到 README 里一堆 AI 生成的辞藻就觉得不靠谱一样。我们作为业内人士,还是要把事情做得漂亮,而不是让 AI 生成一个勉强能用的组装拖拉机就完事。
另一方面 AI 影响比较大的,其实是写作,包括日常的各种文字,比较正式的文档、论文甚至教材,不得不承认,AI 在写作方面确实是比我这种语文是考试弱项的偏科生要做得更好。我通常会自己编写一遍,然后交给 DeepSeek 来润色一遍,再在润色的基础上修改,保证我要表达的意思能够完全地被保留下来。一些小的人情世故,比如微信上和各种人打交道的措辞,网络上发送的邮件或者是 GitHub Issue 等等场合的客套话,AI 确实也是做得比自己好。但是,更完整的内容,或者整体架构上的把握,还是不会让 AI 完全去完成,因为能感觉到 AI 训练所使用的语料和自己的思维方式或者写作的习惯还是不一样的,我还是希望我写的东西能更加得有我的思考和劳动在里面,AI 只是一个让文字看起来更加通顺的工具,帮我纠正一些语法错误之类的。例如,我平时可能更习惯一些口语化的表达,能够让我很快地通过打字或者语音输入把我的脑子里的想法变成文字,然后再让 AI 改写成更加严肃的文字,像教材或者论文,这时 AI 就沦为了纯粹的文字风格改写或者语言翻译器。
既然提到了语音输入法,就不得不提,今年我用语音输入的比例大大提升了。其实语音输入法历史已经很久了,但是以前每次体验,都觉得效果不行,每次输入的有错误还得改,自己改正的时间,还不如自己打字来得快。所以一直以来我都是坚持在所有设备上都是 26 键打字用拼音输入的,当然包括手机,经过多年的练习,确实速度还不错,包括我也不喜欢麻烦别人在微信上听语音,所以我尽量都是用文字的。但今年感受下来,确实是不一样,感觉语音输入的准确率有了质的飞跃,能看到它先识别出一个音对字不对的状态,再纠正成正确的表达,还会提示你,这里可能是另一个词,如果你要修改的话,就直接点一下就行。有这个功能以后,我在手机上真的很多时候就直接用语音输入了,尤其是在一些不太正式的场合,对方也能够对那些少数的识别错误脑补的时候,语音输入确确实实替代了手机上打字。在电脑上,还是打字通常更快一些,但最近也尝试了一下 智谱 AutoGLM 的输入法,感觉这种语音输入和 LLM 结合还挺有意思的,就是它们家的语音输入准确率还比不上 鸿蒙 6 上的小艺输入法,要是二者的优点能够结合在一起就更好了,相信这一天并不遥远。
目前想到的就这么多,其实 AI 还有很多场景可以用到,比如生成图片、视频和音乐等等,目前还没有太多的尝试,相信明年开始会逐渐接触,到时候再在年底写一个 AI 使用总结。总的下来,就是感叹自己也到了感慨科技进步的年纪了,十几年前学技术,虽然也能感觉到科技进步,但因为自己是从零开始,学的就是最新的科技,所以没有啥感觉。但这几年,不断地把新的输入和已有的积累进行对比,就能感觉明显到技术潮流和技术栈的移动,也能感觉到自己对新技术的接受度开始有了略微的下降,这值得让我警醒。以前,我们总是嘲笑大人不追求潮流,不去学习手机等新技术,我们在这个时代长大的人,可也不能犯这样的错误呀。