这份终点把分支目标缓冲、返回地址栈和推测状态恢复连成同一个事件协议。你要证明的不是“预测准确率很高”,而是每次预测引用了什么状态、错误发现后哪些记录仍有资格存在、最终哪些结果真的训练了表。
python3 foundation-speculative-predictor-check.py > predictor.json
python3 -O foundation-speculative-predictor-check.py > predictor-optimized.json
cmp predictor.json predictor-optimized.json
两次完整输出应相同,status为PASS。程序只有标准输出,不写输入文件。它是已解码分支事件模型,不执行ISA;actual方向、target以及正确kind由调用者提供。表中的事件行不是CPU周期。地址在正文用十六进制,JSON保存整数,例如0x104对应260。
任务一:让目标命中和预测正确分别接受检验
取4槽BTB,逐项交出PC0x100与0x110的字地址、槽号与标记。它们分别为64、68,槽都为0,标记为16、17。冷查询、训练100→200、训练110→300的结果必须与 target_buffer.collisions 相同。
写出每个查询的hit与target,不要只看槽里有没有值。第二次训练以后,查0x100应MISS;查0x110应HIT0x300。说明索引与完整标记如何唯一恢复PC,以及为何这只证明表项身份,不能证明下次目标。
读取 target_buffer.alternating:同一PC0x100的四个真实目标依次为0x200、0x300、0x200、0x300。预测依次为0x104、0x200、0x300、0x200;首次目标不可用,后三次BTB命中,四次nextPC均错。方向却始终taken。
交付: 三步表内容、四次目标对照、HIT与nextPC正确性的分别统计。再解释目标0为什么必须与MISS分开,以及b=2时紧凑逻辑表为何为236位,而Python对象并不只占236位。
任务二:用槽内容证明返回状态恢复
读取 return_stack.events。CALL0x100与CALL0x120先后压入0x104、0x124;两次RET都来自PC0x400,却分别应去0x124和0x104。第二次返回时,BTB还记着第一次的0x124,RAS则正确给0x104。交出每次预测前后完整逻辑栈,并说明调用的实际跳转目标与压入的继续地址是两件事。
然后复算容量3的覆写例子。初始物理槽 [0x104,0x204,0x304],next=0、count=3。保存快照,push0x404、push0x504,得到 [0x404,0x504,0x304],next=2、count=3。
- 只恢复next=0、count=3,pop得到304、504、404
- 再恢复旧栈顶304,结果仍然相同
- 恢复全部槽、指针、计数,pop得到304、204、104,第四次为None
这些地址均为十六进制。用 return_stack 的三个实际返回数组核验,而不是仅比较恢复后的栈高度。再检查容量2连续压三个地址会正式丢弃最早项,最后不能凭空返回104。
交付: 保存前、覆写后、三种恢复后的物理与逻辑状态;嵌套返回正确性所需的深度、配对及上下文条件。解释为什么同容量快照可跨ReturnStack实例导入,而一个复制字段的新Ticket仍不被预测器接受。
任务三:按真实顺序取消已解析的年轻记录
读取 out_of_order。初始提交的两个CALL序号为0、1,建立已提交RAS [104,204]。随后A、B、C的ticket序号为2、3、4:
| 记录 | PC与种类 | 预测 | 最终解析 |
|---|---|---|---|
| A | 0x300,C | N,next0x304,j=0 | T,next0x600 |
| B | 0x304,CALL | 目标MISS,next0x308 | T,next0x500 |
| C | 0x400,RET | RAS给0x308 | T,next0x308 |
严格按C、B、A解析,不能换成程序顺序后声称测试过乱序。C先解析正确,commit仍等待队首A。B目标错,取消序号4;A随后方向与目标都错,取消序号3。每次取消后,给出完整H、RAS和队列。A最终提交时counter[0]才从1变2,Hc才从0变1。
交出为何“C已经正确解析”不足以保留它:C仍在一个更老错误分支之后。B的恢复也不能阻止后来A覆盖它。最后尝试用A、B、C旧凭据再次resolve;A已提交,B/C已取消,三者均应拒绝且状态、事件日志不变。
交付: 全部predict/resolve/commit事件,取消集合 [4]、[3],每次恢复前快照,最终BTB槽0的tag48与目标字地址384。把这两个数还原为PC0x300、目标0x600。不要把其他槽或仅解析的结果偷偷训练进去。
任务四:分别击穿三种看似合理的快捷写法
先看 timing.equal_next_pc。A在PC0x100预测N、next0x104,真实却是taken到0x104。地址没变,方向位变了;验收为direction_wrong真、next_pc_wrong假,仍取消年轻ticket并把H修复为1。只比较地址的版本会漏修。
再看 timing.target_only。通过真实提交先把BTB训练成100→300,方向表初值2。C分支预测T到300,实际T到400。direction_wrong假、next_pc_wrong真,仍须恢复。不能拿方向正确替代控制流地址核对。
最后看 timing.current_counter。m=h=0、单槽计数器初值1。预测A并解析T但不提交,再预测B并解析N;两张ticket都保存旧计数器1。提交A后为2,提交B后为1。若从各ticket旧值写回,结果会错误地为0;若提交时重算gshare索引,则在h>0时可能更新错误槽。
可以用下面的短驱动重新得到最后一例,不必手工改参考器:
PYTHONDONTWRITEBYTECODE=1 python3 - <<'PY'
import importlib.util, sys
spec = importlib.util.spec_from_file_location('predictor', 'foundation-speculative-predictor-check.py')
m = importlib.util.module_from_spec(spec); sys.modules[spec.name] = m; spec.loader.exec_module(m)
p = m.Predictor(m=0, h=0)
a = p.begin(0x100, 'C'); p.resolve(a, 1, 0x200)
b = p.begin(0x104, 'C'); p.resolve(b, 0)
print('captured', a.counter, b.counter)
p.commit(); print('after A', p.counters)
p.commit(); print('after B', p.counters)
PY
应输出captured1、1,after A [2],after B [1]。交付: 三份独立反例与各自破坏的不变量,不把“方向与目标的区别”“保存索引”“当前计数器累积”混成一句笼统的更新错误。
任务五:整段flush、容量与无效凭据
读取 timing.flush。已经提交的历史Hc=1,已提交RAS [104];年轻RET和条件分支改变推测状态后整体flush。恢复H=1、RAS [104],两张表保持已提交值。所有被清空ticket此后失效,但递增序号不回退。
取Q=2,连续begin两项,再begin第三项。第三次返回None,所有状态与日志不变。非法PC、kind、实际方向或目标即使碰到满队列也不能被当作正常不可用;接口先验证参数。分别检查重复解析、跨预测器凭据、复制字段伪造、已取消、已提交的凭据,不能仅凭相同序号接收。
读取 invalid:共26个明确拒绝,其中包括无效配置、布尔值、未对齐地址、种类方向不一致、无效RAS快照与凭据生命周期。环形快照必须检查活跃槽非空,不能先改指针再发现数据非法。0xfffffffc处CALL的继续地址为0,参考器还交出这个真实回绕事件。
交付: 一份拒绝前后状态/日志相等的证据、一份满队列无变更证据,以及flush保留已提交状态的解释。说明这些输入检查不负责验证调用者提供的actual值是否真的由某段ISA程序算出。
任务六:把有限回归与一般证明接起来
公开输出的 exhaustive 应报告655个目标表状态、5765次查询、17295次训练,以及516个可达环形栈物理状态、1548次操作和1548次破坏后完整恢复。环形栈对照器使用普通列表保存底到顶顺序,不用相同的环形索引公式计算期望。
random使用900组配置、90000个事件,实际包含26067次成功预测、14919次解析、8656次提交、8744次随机flush和10047次旧凭据拒绝;每组结束的清空另外执行,不计入随机事件数。EventOracle从已提交记录与当前队列重新折叠方向位列表和普通地址列表,不通过恢复代码计算期望状态。方向索引还逐bit重建,目标表则用完整PC字典核验。
external_mutants中的四个错误版本应全部被击穿:重算训练索引、覆盖旧计数器、只恢复RAS指针、解析时提前训练。错误版本只在测试层派生,不改正常接口。若你增加一种故障,应保存真正触发的输入与不变量错误,不把未运行代码称作反例。
最后给出可重放归纳:初始、begin、正确resolve、错误resolve、commit和flush各怎样保持“已提交前缀+在途后缀”的状态。最老记录移入已提交前缀时,串接的作用序列未变,所以推测状态不能再次push或移位。剩余队首不被解析时,协议也没有自动前进的保证。
成本须包括完整快照,而不只数表查询:begin复制D项,追加列表的摊还总成本为O(D),单次扩容最坏为O(D+Q);身份查找扫描Q项;列表队首移除为O(Q),commit返回的ticket在调用方最终释放时还可能花O(D);取消k项若计入快照回收,另有O(kD);整体flush最坏O(QD+D)。关闭日志时的核心状态为O(P+N+D+QD+Q)个字,调用方长期持有的旧ticket、启用后的事件日志、全表快照、重放oracle和JSON分别计费。不要把这份正确性优先实现的事件成本转换成硬件周期。
最终提交三张独立接口的合同、六项真实输出、四个错误版本证据、一般不变量证明与完整资源账单。回到预测状态路线。