4、多级TLB设计:L1 TLB与L2 TLB的层次划分
TLB这东西,说白了就是个地址翻译的缓存。单级TLB在简单核里够用,但到了高性能处理器里,单级TLB就成了瓶颈。我最早做的一款RISC-V核,用的就是单级TLB,结果跑Linux时页表遍历频繁,性能惨不忍睹。后来才明白,多级TLB才是正解。
4.1 为什么需要多级TLB?
单级TLB的问题在于:容量做大了,访问延迟就上去了;容量做小了,命中率又不够。你想想看,一个64条目的L1 TLB,命中率可能只有90%出头,剩下的10%都要去查页表,那性能损失太大了。
多级TLB的思路很简单:用L1 TLB保速度,用L2 TLB保容量。L1做小、做快,L2做大、稍慢。这样大部分翻译请求在L1就搞定了,L1 miss了再去L2查,L2再miss才去查页表。
核心权衡:L1 TLB追求单周期命中,L2 TLB追求高命中率。两者配合,整体性能远优于单级TLB。
4.2 L1 TLB与L2 TLB的层次划分
我在项目中习惯这样划分:
- L1 TLB:通常4-64个条目,全相联或组相联,单周期访问。只缓存最近最常用的翻译。
- L2 TLB:通常256-1024个条目,组相联,2-4周期访问。缓存L1 TLB淘汰的条目。
举个例子,我参与的一个RISC-V乱序执行核,L1 TLB是32条目全相联,L2 TLB是512条目4路组相联。L1命中率约95%,L2命中率约98%,整体TLB miss率降到0.1%以下。
| 参数 | L1 TLB | L2 TLB |
|---|---|---|
| 条目数 | 4-64 | 256-1024 |
| 相联度 | 全相联/组相联 | 组相联 |
| 访问延迟 | 1周期 | 2-4周期 |
| 命中率目标 | >90% | >95% |
| 功耗 | 低 | 中 |
4.3 容量与延迟的权衡
容量和延迟,永远是死对头。L1 TLB做大了,延迟就上去了,反而拖累关键路径。我见过有人把L1 TLB做到128条目,结果频率降了10%,得不偿失。
我个人建议:L1 TLB的容量,以不增加关键路径延迟为上限。通常4-32条目是安全区,64条目需要仔细优化。L2 TLB则可以大胆做大,因为它的访问不在关键路径上。
避坑指南:我曾经在一个项目里把L1 TLB做到64条目,结果综合后时序不收敛。后来改成32条目+预取机制,命中率没降多少,时序问题解决了。所以,别贪心,L1 TLB够用就行。
4.4 多级TLB的包含性与排他性策略
这是多级TLB设计里最容易踩坑的地方。包含性和排他性,决定了L1和L2之间怎么配合。
4.4.1 包含性策略
包含性:L1 TLB里的条目,一定也在L2 TLB里。换句话说,L2是L1的超集。
- 优点:L2 TLB可以独立处理一致性操作,不需要查L1。L1 miss时,L2一定知道这个条目是否存在。
- 缺点:L2 TLB里有很多冗余条目(L1里已经有的),浪费了容量。而且L1做替换时,L2里的对应条目不能动。
4.4.2 排他性策略
排他性:L1 TLB里的条目,一定不在L2 TLB里。L1和L2互斥。
- 优点:容量利用率高,没有冗余。L1和L2加起来,能覆盖更多的不同翻译。
- 缺点:L1 miss时,如果L2 hit,需要把L2的条目搬到L1,L2里就没了。下次再查这个条目,L2 miss,又得去页表。
4.4.3 我的选择
我做过几个项目,总结下来:
- 简单核、顺序核:用包含性。实现简单,L2 TLB容量够大,冗余不是问题。
- 高性能乱序核:用排他性。容量利用率高,L1和L2能覆盖更多翻译,减少页表访问。
但要注意,排他性实现起来更复杂。L1替换时,要把条目写回L2;L2 hit时,要把条目搬到L1。这些操作都需要额外的控制逻辑。
警告:排他性策略下,L1和L2之间需要握手信号,处理不好容易死锁。我曾经在一个项目里,L1替换写回L2时,L2正好也在做替换,结果两个模块互相等待,系统卡死。后来加了优先级仲裁才解决。
4.5 多级TLB的完整结构
下面这张图,是我常用的多级TLB结构。L1 TLB和L2 TLB之间通过请求-响应接口交互,L2 TLB再连到页表遍历器(Page Table Walker)。
4.6 多级TLB的调试要点
调试多级TLB,比单级麻烦得多。我总结几个常见问题:
- 一致性检查:包含性策略下,L1里的条目,L2里必须有。用随机测试验证。
- 死锁检测:排他性策略下,L1和L2之间的请求-响应不能形成循环依赖。加超时机制。
- 延迟测量:L1 hit、L2 hit、L2 miss三种情况,延迟是否符合预期。用性能计数器监控。
调试技巧:我曾经在仿真时,用断言检查L1和L2的包含性。一旦发现L1有而L2没有的条目,立刻报错。这个断言帮我抓到了好几个设计bug。
4.7 小结
多级TLB设计,说白了就是平衡速度、容量和复杂度。L1做小做快,L2做大做全。包含性简单但浪费容量,排他性高效但实现复杂。选哪个,看你的应用场景。
我个人更倾向排他性,尤其是高性能场景。但如果你刚开始做,从包含性入手更稳妥。嗯,先跑起来,再优化,这是硬件设计的铁律。