MoreRSS

site iconJiaJe | 杰哥修改

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

Inoreader Feedly Follow Feedbin Local Reader

JiaJe | 杰哥的 RSS 预览

条件分支预测器逆向工程(以 Apple M1 Firestorm 为例)

2025-10-28 08:00:00

条件分支预测器逆向工程(以 Apple M1 Firestorm 为例)

背景

去年我完成了针对 Apple 和 Qualcomm 条件分支预测器(Conditional Branch Predictor)的逆向工程研究,相关论文已发表在 arXiv 上,并公开了源代码。考虑到许多读者对处理器逆向工程感兴趣,但可能因其复杂性而望而却步,本文将以 Apple M1 Firestorm 为例,详细介绍条件分支预测器的逆向工程方法,作为对原论文的补充说明。

背景知识

首先介绍一些背景知识。要逆向工程条件分支预测器,需要先了解其工作原理。条件分支预测器的基本思路是:

  • 条件分支的跳转行为(跳转或不跳转)通常是高度可预测的
  • 预测器的输入包括条件分支的地址,以及近期执行的若干条分支的历史记录;输出则是预测该条件分支是否跳转

为了在硬件上实现这一算法,处理器会维护一个预测表,表中每一项包含一个 2 位饱和计数器,用于预测跳转方向。查表时,系统会对条件分支地址以及近期执行的分支历史进行哈希运算,使用哈希结果作为索引读取表项,然后根据计数器的值来预测分支的跳转方向。

(图源:CMU ECE740 Computer Architecture: Branch Prediction)

目前主流处理器普遍采用 TAGE 预测器,它在上述基础查表方法的基础上进行了重要改进:

  1. 观察到不同分支的预测所需的历史长度各不相同:有些分支无需历史信息即可准确预测,有些依赖近期分支的跳转结果,而有些则需要更久远的历史信息;
  2. 分支历史越长,可能的路径组合就越多,导致预测器训练过程变慢,训练期间的预测错误率较高,因此希望尽快收敛;
  3. 为满足不同分支对历史长度的需求,TAGE 设计了多个预测表,每个表使用不同长度的分支历史。多个表同时进行预测,当多个表都提供预测结果时(仅在 tag 匹配时提供预测),选择使用最长历史长度的预测结果。

(图源:Half&Half: Demystifying Intel's Directional Branch Predictors for Fast, Secure Partitioned Execution

因此,要逆向工程处理器的条件分支预测器,需要完成以下工作:

  1. 确定分支历史的记录方式:通常涉及分支地址和目的地址,通过一系列移位和异或操作,将结果存储在寄存器中;
  2. 确定 TAGE 算法的具体实现:包括表的数量、每个表的大小、索引方式以及使用的分支历史长度。

分支历史的逆向

第一步是逆向工程处理器记录分支历史的方式。传统教科书方法使用一个寄存器,每当遇到条件分支时记录其跳转方向(跳转记为 1,不跳转记为 0),每个分支占用 1 bit。然而,现代处理器(包括 Intel、Apple、Qualcomm、ARM 和部分 AMD)普遍采用 Path History Register 方法。这种方法设计一个长度为 \(n\) 的寄存器 \(\mathrm{PHR}\),每当遇到跳转分支(包括条件分支和无条件分支)时,将寄存器左移,然后将当前跳转分支的地址和目的地址通过哈希函数映射,将哈希结果异或到移位寄存器中。用数学公式表示为:

\(\mathrm{PHR}_{\mathrm{new}} = (\mathrm{PHR}_{\mathrm{old}} \ll \mathrm{shamt}) \oplus \mathrm{footprint}\)

其中 \(\mathrm{footprint}\) 是通过分支地址和目的地址计算得到的哈希值。接下来的任务是确定 \(\mathrm{PHR}\) 的位宽、每次左移的位数,以及 \(\mathrm{footprint}\) 的计算方法。

历史长度

首先分析这个更新公式:它将最近的 \(\lceil n / \mathrm{shamt} \rceil\) 条跳转分支的信息压缩存储在 \(n\) 位的 \(\mathrm{PHR}\) 寄存器中。随着移位操作的累积,更早的分支历史信息对 \(\mathrm{PHR}\) 的贡献最终会变为零。

第一个实验的目标是确定 \(\mathrm{PHR}\) 能够记录多少条最近分支的历史。具体方法是构建一个分支历史序列:

  1. 第一个条件分支:以 50% 的概率随机跳转或不跳转;
  2. 中间插入若干条无条件分支;
  3. 最后一个条件分支:跳转方向与第一个条件分支相同。

接下来分析两种情况:

  1. 如果在预测最后一个条件分支时,分支历史 \(\mathrm{PHR}\) 仍然包含第一个条件分支的信息,预测器应该能够准确预测最后一个条件分支的方向;
  2. 如果中间的无条件分支数量足够多,使得第一个条件分支的跳转信息对预测最后一个条件分支时的 \(\mathrm{PHR}\) 没有影响,预测器只能以 50% 的概率进行正确预测。

通过构造上述程序,调整中间无条件分支的数量,并使用性能计数器统计分支预测错误率,可以找到一个临界点。当无条件分支数量超过这个阈值时,第二个条件分支的错误预测率会从 0% 上升到 50%。这个临界点对应 \(\mathrm{PHR}\) 能够记录的分支历史数量,即 \(\lceil n / \mathrm{shamt} \rceil\)

经过测试

# 第一列:第二步插入的无条件分支数量加一# 第二列到第四列:分支预测错误概率的 min/avg/max# 第五列:每次循环的周期数size,min,avg,max,cycles97,0.00,0.00,0.01,216.8798,0.00,0.00,0.01,221.0299,0.00,0.00,0.01,225.18100,0.00,0.00,0.01,229.17101,0.45,0.50,0.53,331.97102,0.47,0.50,0.54,336.27103,0.46,0.50,0.54,339.85

测试结果表明阈值为 100:在 Apple M1 Firestorm 上,最多可以记录最近 100 条分支的历史信息。

分支预测错误率是怎么测量的?

处理器内置了性能计数器,会记录分支预测错误次数。在 Linux 上,可以用 perf 子系统来读取;在 macOS 上,可以用 kpep 私有 API 来获取。我开源的代码中对这些 API 进行了封装,可以实现跨平台的性能计数器读取。更进一步,我们还逆向了 Qualcomm Oryon 的针对条件分支的预测错误次数的隐藏性能计数器,用于后续的实验。

分支地址 B 的贡献

接下来需要推测 \(\mathrm{footprint}\) 的计算方法,即分支地址和目的地址如何参与 \(\mathrm{PHR}\) 的更新过程。约定分支地址记为 \(B\)(Branch 的首字母),目的地址记为 \(T\)(Target 的首字母),用 \(B[i]\) 表示分支地址从低到高第 \(i\) 位(下标从 0 开始)的值,\(T[i]\) 同理。假设 \(\mathrm{footprint}\) 的每一位都由若干个 \(B[i]\)\(T[i]\) 通过异或运算得到。

分支指令本身占用了多个字节,那么分支地址指的是哪一个字节的地址呢?

经过测试,AMD64 架构下,分支地址用的是分支指令最后一个字节的地址,而 ARM64 架构下,分支地址用的是分支指令第一个字节的地址。这大概是因为 AMD64 架构下分支指令是变长的,并且可以跨越页的边界;ARM64 则是定长的,并且不会跨越页的边界。

设计以下程序来推测某个 \(B[i]\) 如何参与 \(\mathrm{footprint}\) 的计算:

  1. 根据上面的分析,Apple M1 Firestorm 最多可以记录最近 100 条分支的历史信息,为了让 \(\mathrm{PHR}\) 进入一个稳定的初始值,执行 100 个无条件分支;
  2. 设计两条分支指令,第一条是条件分支,按 50% 的概率跳或不跳;第二条是无条件分支;这两条分支的分支地址只在 \(B[i]\) 上不同,其余的位都相同,目的地址相同;
  3. 执行若干条无条件分支,目的是把 \(B[i]\)\(\mathrm{PHR}\) 的贡献向前移动;
  4. 执行一条条件分支指令,其跳转方向与第二步中条件分支的方向一致。

对应的代码如下:

// step 1.// 100 jumps forwardgoto jump_0;jump_0: goto jump_1;// ...jump_98: goto jump_99;jump_99:// step 2.int d = rand();// the follow two branches differ in B[i]// first conditional branch, 50% taken or not takenif (d % 2 == 0) goto target;// second unconditional branchelse goto target;target:// step 3.// variable number of jumps forwardgoto varjump_0;varjump_0: goto varjump_1;// ...varjump_k: goto last;// step 4.// conditional branchlast:if (d % 2 == 0) goto end;end:

第二步中条件分支跳转与否,会影响分支历史中 \(B[i]\) 一个位的变化,它会经过哈希函数,影响 \(\mathrm{footprint}\),进而异或到 \(\mathrm{PHR}\) 中。通过调整第三步执行的无条件分支个数,可以把 \(B[i]\)\(\mathrm{PHR}\) 的影响左移到不同的位置。如果 \(B[i]\)\(\mathrm{PHR}\) 造成了影响,就可以正确预测最后一条条件分支指令的方向。当左移次数足够多时,\(B[i]\)\(\mathrm{PHR}\) 的贡献会变为零,此时对最后一条条件分支指令的方向预测只有 50% 的正确率。在 Apple M1 Firestorm 上测试,得到如下结果:

横坐标 Dummy branches 指的是上面第三步插入的无条件分支的个数,纵坐标 Branch toggle bit 代表修改的是具体哪一个 \(B[i]\),颜色对应分支预测的错误率,浅色部分对应最后一条分支只能正确预测 50%,深色部分对应最后一条分支总是可以正确预测。

从这个图可以得到什么信息呢?首先观察 \(B[2]\) 对应的这一行,可以看到它确实参与到了 \(\mathrm{PHR}\) 的计算中,但是仅仅经过 28 次移位,这个贡献就被移出了 \(\mathrm{PHR}\),为了保留在 \(\mathrm{PHR}\) 内,最多移动 27 次。类似地,在移出 \(\mathrm{PHR}\) 之前,\(B[3]\) 最多移动 26 次,\(B[4]\) 最多移动 25 次,\(B[5]\) 最多移动 24 次。

但实际上,这些 \(B\) 是同时进入 \(\mathrm{PHR}\) 的:这暗示它们对应 \(\mathrm{footprint}\) 的不同位置。如果某个 \(B[i]\) 出现在 \(\mathrm{footprint}\) 更高位的地方,它也会进入 \(\mathrm{PHR}\) 更高位,经过更少的移位次数就会被移出 \(\mathrm{PHR}\);反之,如果 \(B[i]\) 出现在 \(\mathrm{footprint}\) 更低位的地方,它能够在 \(\mathrm{PHR}\) 中停留更长的时间。

根据上面的实验,可见 \(B[5], B[4], B[3], B[2]\) 参与到了 \(\mathrm{footprint}\) 计算中,而 \(B\) 的其他位则没有。但比较奇怪的是,\(\mathrm{PHR}\) 理应可以记录最近 100 条分支的信息,但实际上只观察到了 28。所以一定还有其他的信息。

目的地址 T 的贡献

刚刚测试了 \(B\),接下来测试 \(T\) 各位对 \(\mathrm{PHR}\) 的贡献,方法类似:

  1. 为了让 \(\mathrm{PHR}\) 进入一个稳定的初始值,执行 100 个无条件分支;
  2. 设计一个间接分支,根据随机数,随机跳转到两个不同的目的地址,这两个目的地址只在 \(T[i]\) 上不同,其余的位都相同,分支地址相同;
  3. 执行若干条无条件分支,目的是把 \(T[i]\)\(\mathrm{PHR}\) 的贡献向前移动;
  4. 执行一条条件分支指令,其跳转方向取决于第二步中间接分支所使用的随机数。

对应的代码如下:

// step 1.// 100 jumps forwardgoto jump_0;jump_0: goto jump_1;// ...jump_98: goto jump_99;jump_99:// step 2.int d = rand();// indirect branch// the follow two targets differ in T[i]auto targets[2] = {target0, target1};goto targets[d % 2];target0:// add many nopstarget1:// step 3.// variable number of jumps forwardgoto varjump_0;varjump_0: goto varjump_1;// ...varjump_k: goto last;// step 4.// conditional branchlast:if (d % 2 == 0) goto end;end:

在 Apple M1 Firestorm 上测试,得到如下结果:

为了测试 T[31],岂不是要插入很多个 NOP,一方面二进制很大,其次还要执行很长时间?

是的,所以这里在测试的时候,采用的是类似 JIT 的方法,通过 mmap MAP_FIXED 在内存中特定位置分配并写入代码,避免了用汇编器生成一个巨大的 ELF。同时,为了避免执行大量的 NOP,考虑到前面已经发现 \(B[6]\) 或更高的位没有参与到 \(\mathrm{PHR}\) 计算中,所以可以添加额外的一组无条件分支来跳过大量的 NOP,它们的目的地址相同,分支地址低位相同,因此对 PHR 不会产生影响。对应的代码如下:

// step 1.// 100 jumps forwardgoto jump_0;jump_0: goto jump_1;// ...jump_98: goto jump_99;jump_99:// step 2.int d = rand();// indirect branch// the follow two targets differ in T[i]auto targets[2] = {target0, target1};goto targets[d % 2];target0:// skip over nops, while keeping B[5:2]=0goto target2;// add many nopstarget1:goto target2;target2:// step 3.// variable number of jumps forwardgoto varjump_0;varjump_0: goto varjump_1;// ...varjump_k: goto last;// step 4.// conditional branchlast:if (d % 2 == 0) goto end;end:

由此我们终于找到了分支历史最长记录 100 条分支的来源:\(T[2]\) 会经过 \(\mathrm{footprint}\) 被异或到 \(\mathrm{PHR}\) 的最低位,然后每次执行一个跳转分支左移一次,直到移动 100 次才被移出 \(\mathrm{PHR}\)。类似地,\(T[3]\) 只需要 99 次就能移出 \(\mathrm{PHR}\),说明 \(T[3]\) 被异或到了 \(\mathrm{PHR}[1]\)。依此类推,可以知道涉及 \(T\)\(\mathrm{footprint} = T[31:2]\),其中 \(T[31:2]\) 代表一个 30 位的数,每一位从高到低分别对应 \(T[31], T[30], \cdots, T[2]\)

小结

那么问题来了,前面测试 \(B\) 的时候,移位次数那么少,明显少于 \(T\) 的移位次数。这有两种可能:

  1. 硬件上只有一个 \(\mathrm{PHR}\) 寄存器,\(T[31:2]\) 被异或到 \(\mathrm{PHR}\) 的低位,而 \(B[5:2]\) 被异或到 \(\mathrm{PHR}\) 的中间位置;
  2. 硬件上有两个 \(\mathrm{PHR}\) 寄存器,其中一个是 100 位,它的 \(\mathrm{footprint} = T[31:2]\),记为 \(\mathrm{PHRT}\);另一个是 28 位,它的 \(\mathrm{footprint} = B[5:2]\),记为 \(\mathrm{PHRB}\)

经过后续的测试,基本确认硬件实现的是第二种。用数学公式表达:

\(\mathrm{PHRT}_{\mathrm{new}} = (\mathrm{PHRT}_{\mathrm{old}} \ll 1) \oplus \mathrm{T}[31:2]\)

\(\mathrm{PHRB}_{\mathrm{new}} = (\mathrm{PHRB}_{\mathrm{old}} \ll 1) \oplus \mathrm{B}[5:2]\)

有意思的是,在我的论文发表后不久,Apple 公开的专利 Managing table accesses for tagged geometric length (TAGE) load value prediction 中就出现了相关表述,证明了逆向结果的正确性。

按照这个方法,我还逆向工程了 Apple、Qualcomm、ARM 和 Intel 的多代处理器的分支历史记录方法,并进行了公开,供感兴趣的读者阅读,也欢迎读者将测试代码移植到更多处理器上,并贡献逆向工程的结果。

TAGE 表的逆向

接下来,我们将目光转向 TAGE 表的逆向工程。TAGE 表与缓存结构类似,也是一个多路组相连的结构,通过 index 访问若干路,然后对每一路进行 tag 匹配,匹配正确的那一路提供预测。TAGE 在预测时,输入是历史寄存器,即上面逆向得到的 \(\mathrm{PHRT}\)\(\mathrm{PHRB}\),以及分支地址,目前这两个输入都是可控的。为了避免多个表同时提供预测,首先逆向工程使用分支历史最长的表的参数:它的容量是多少,index 如何计算,tag 如何计算,以及几路组相连。

如何确保使用分支历史最长的表提供预测呢?其实还是利用分支历史的特性,将随机数注入到 \(PHRT\) 中,例如前面的间接分支,让两个目的地址只在 \(T[2]\) 上不同:

// add some unconditional jumps to reset phr to some constant value// 100 jumps forwardgoto jump_0;jump_0: goto jump_1;// ...jump_98: goto jump_99;jump_99:// injectint d = rand();// indirect branch// the follow two targets differ in T[2]auto targets[2] = {target0, target1};goto targets[d % 2];target0:// add nop heretarget1:// add some unconditional jumps to shift the injected bit leftgoto varjump_0;varjump_0: goto varjump_1;// ...varjump_k: goto last;last:

根据前面的分析,\(T[2]\) 会被异或到 \(\mathrm{PHRT}\) 的最低位上,每执行一次无条件分支,就左移一位。因此,通过若干个无条件分支,可以把 d % 2 这个随机数注入到 \(\mathrm{PHRT}\) 的任意一位上。之后我们还会很多次地进行这种随机数的注入。

把随机数注入到 \(\mathrm{PHRT}\) 高位以后,再预测一个根据随机数跳转或不跳转的分支,就可以保证它只能由使用分支历史最长的表来进行预测。

逆向工程 PC 输入

首先,我们希望推断 PC 如何参与到 index 或 tag 计算中。通常,TAGE 只会采用一部分 PC 位参与 index 或 tag 计算。换句话说,如果两个分支在 PC 上不同的部分没有参与 index 或 tag 计算,那么 TAGE 无法区分这两条分支。如果这两个分支跳转方向相反,并且用相同的 PHR 进行预测,那么一定会出现错误的预测。思路如下:

  1. 用 100 个无条件分支,保证 PHR 变成一个确定的值;
  2. 注入随机数 d % 2 到 PHRT,并移动到高位(例如 \(PHRT[99]\)),使用前面所述的方法;
  3. 执行两个条件分支,它们在分支地址上只有一位 \(PC[i]\) 不同,它们的跳转条件相反,当第一个条件分支不跳转的时候,会执行第二个条件分支,它总是会跳转。

对应代码类似于:

// step 1. inject phrtint d = rand();inject_phrt(d % 2, 99);// step 2. a pair of conditional branches with different direction// their PC differs in one bitif (d % 2 == 0) goto end;if (d % 2 == 1) goto end;end:

经过测试,PC 的输入是 \(PC[18:2]\),其余的没有。

逆向工程相连度和 index 函数的 PC 输入

接下来是比较复杂的一步,同时逆向工程表的相连度和 index 函数的 PC 输入。这是因为这两部分是紧密耦合的:只有知道相连度,才能知道预测出来的分支数对应几个 set;但不知道 index 函数,又无法控制分支被分配到几个 set 中。首先,为了避免 PHR 的干扰,还是只注入一个随机数到 \(PHRT[99]\) 上(事实上,\(PHRT[99]\) 不是随便选择的,而是需要在 index 函数中,但通过测试可以找到满足要求的位)。其次,构造一系列分支,它们的地址满足:第 i 条分支(i 从 0 开始)的分支地址是 \(i2^k\),其中 \(k\) 是接下来要遍历的参数。当 \(k=3\) 时,分支会被放到 0x0, 0x8, 0x10, 0x18, 0x20 等地址,涉及的 PC 位数随着分支数的增加而增加。接下来,我们分类讨论:

  • 假如涉及的 PC 位都在 tag 中,没有出现在 index 中:那么这些分支都会被映射到同一个 set 内,一旦分支数量超出相连度,就会出现预测错误。
  • 假如涉及的 PC 位有一部分出现在 index 中:那么每有一个 PC 位出现在 index 中,这些分支可以被分配到的 set 数量就翻倍,直到这些 set 都满了以后,才会出现预测错误。
  • 假如涉及的 PC 位有一部分超出 PC 输入的范围(如前面逆向工程得到的 \(PC[18:2]\)):那么超出输入的部分地址会被忽略,使得 set 内出现冲突。

实验结果如下图:

纵坐标就是上面的 \(k\),横坐标是测试的条件分支数,颜色表示预测的错误率。当颜色从深色变浅,就说明出现了预测错误。观察:

  • \(PC[3]\) 的情况下,只能预测 4 个分支,而 \(PC[4]\)\(PC[5]\) 可以预测 8 个分支,暗示了四路组相连,然后 \(PC[4]\)\(PC[5]\) 对应到了两个 set,所以能够正确预测 8 个分支。
  • \(PC[6]\) 的情况下,可以预测 16 个分支,对应 4 个 set;后续 \(PC[7]\)\(PC[8]\) 又可以预测 8 个分支,对应 2 个 set;意味着 \(PC[6]\) 在 index 中,给 \(PC[4]\)\(PC[5]\) 提供了两倍的 set;\(PC[9]\) 在 index 中,给 \(PC[6]\)\(PC[7]\)\(PC[8]\) 提供了两倍的 set。
  • 后续更高的 PC 位,没有受到 index 函数的影响,因此都是 4,直到最后超出 PC 输入范围。

这就说明它是四路组相连,PC[6] 和 PC[9] 参与到了 index 函数中。

下面给读者一个小练习,下面是在 Qualcomm Oryon 上测得的结果,可以看到噪声比较大,你能推断出它是几路组相连,有哪些 PC 参与到了 index 计算吗?

揭晓答案

四路组相连,\(PC[6]\)\(PC[7]\) 参与到了 index 函数。

那么,这种测试是怎么构造的呢?即需要用相同的 PHR 去预测 \(PC=i2^k\) 的多条分支。思路比较复杂:

  1. 首先执行一条间接分支,目的地址是 \(i2^{k-1}\),那么它对 PHRT 的贡献是 \(\mathrm{PHRT}_1 = (\mathrm{PHRT}_0 \ll 1) \oplus (i2^{k-3})\)
  2. 接下来,在 \(i2^{k-1}\) 的位置,再执行一条直接分支,目的地址是 \(i2^k\),那么它对 PHRT 的贡献是 \(\mathrm{PHRT}_2 = (\mathrm{PHRT}_1 \ll 1) \oplus (i2^{k-2}) = (((\mathrm{PHRT}_0 \ll 1) \oplus (i2^{k-3})) \ll 1) \oplus (i2^{k-2}) = \mathrm{PHRT}_0 \ll 2\)

可见经过两步以后,PHRT 是保持不变的。针对 PHRB,只要 \(i2^{k-1}\) 没有涉及 \(PC[5:2]\),就能保证相同。那么如果 \(k\) 足够小,也有办法:

  1. 首先执行一条间接分支,目的地址是 \(i2^{k-1}\)
  2. 接下来执行大量的 NOP,使得 \(B\) 的低位等于 0,然后再执行一条间接分支,目的地址是 \(i2^k\)

因此我们总是可以通过两次分支,实现用相同的 PHR 预测不同 PC 上的多条分支。

逆向工程 tag 函数

接下来,进行 tag 函数的逆向工程。为了逆向工程 tag 函数,我们希望找到两个位在 tag 函数中有异或关系,那么如果这两个位同时设为 0,或者同时设为 1,其异或结果都等于 0,使得计算出来的 tag 函数相同,如果此时 index 还相同,那么预测器就无法区分这两种情况。

为了利用这一点,生成两个 0 到 1 的随机数 \(k\)\(l\),分别把它们注入到 PC、PHRB 或者 PHRT 中,去预测一个条件分支,其跳转与否取决于 \(k\) 的值(论文中有个小 typo)。如果 \(k\)\(l\) 在 tag 函数中有异或关系,那么预测器总会预测错误。

实验结果大致如下,横纵坐标表示注入哪一个位,颜色代表预测错误率,深色意味着预测错误,也就是找到了一组异或关系:

其中有一些异或关系,因为对应的位在 index 中出现的缘故,导致没有显现出来。根据已知的异或关系外推,可以得到如下的 tag 计算公式:

  • PC[7] xor PHRT[0,12,...,96] xor PHRB[8,21]
  • PC[8] xor PHRT[1,13,...,97] xor PHRB[9,22]
  • PC[9] xor PHRT[2,14,...,98] xor PHRB[10,23,24]
  • PC[10] xor PHRT[3,15,...,87,99] xor PHRB[11,12,25]
  • PC[11] xor PHRT[4,16,...,88] xor PHRB[0,13,26]
  • PC[12] xor PHRT[5,17,...,89] xor PHRB[1,14,27]
  • PC[13] xor PHRT[6,18,...,90] xor PHRB[2,15]
  • PC[14] xor PHRT[7,19,...,91] xor PHRB[3,16]
  • PC[15] xor PHRT[8,20,...,92] xor PHRB[4,17]
  • PC[16] xor PHRT[9,21,...,93] xor PHRB[5,18]
  • PC[17] xor PHRT[10,22,...,94] xor PHRB[6,19]
  • PC[18] xor PHRT[11,23,...,95] xor PHRB[7,20]
  • PC[2:5]: 单独出现,不和其他位异或

那么,这里是怎么实现针对 PC、PHRT 和 PHRB 的注入的呢?针对 PHRT 的注入前面已经提到过,只需要一个间接分支:

// target differs in T[2]auto targets[2] = {target0, target1};goto targets[d % 2];target0:// add noptarget1:

PHRB 的注入就比较复杂了,例如要注入 \(B[2]=k\),我们需要进行三次分支的跳转:

  1. 第一次跳转,\(B\) 相同,\(T[2]=k\)
  2. 第二次跳转,\(B[2]=k\)\(T[3]=k\)
  3. 第三次跳转,\(B[2]=B[3]=k\), \(T\) 相同。

经过计算,可以发现前两次跳转对 PHRT 的抵消,第二次跳转的 \(B[2]\) 与第三次跳转的 \(B[3]\) 抵消,最后相当于只有最后一次跳转的 \(B[2]=k\) 对 PHRB 产生了贡献。

最复杂的是 PC 的注入,这次需要分情况讨论:

第一种情况是,要注入的 PC 位比较高,具体来说,是 \(PC[7]\) 或者更高的位数,此时我们可以很容易地避免引入对 PHRB 的贡献,因为它只考虑 \(B[5:2]\)

  1. 第一次跳转,\(B\) 相同,\(T[6]=k\)
  2. 第二次跳转,\(B[6]=k\) 但对 PHRB 没有贡献,\(T[7]=k\)

那么两次跳转完以后,PHRB 不变,PHRT 的贡献被抵消掉,同时实现了 \(PC[7]=k\) 的注入。

第二种情况是,要注入的正好是 \(PC[6]\),继续用上面的方法会发现 PHRB 无法抵消,这时候,需要引入第三次跳转:

  1. 第一次跳转,\(B\) 相同,\(T[3]=k\)
  2. 第二次跳转,\(B[3]=k\)\(T[5]=T[4]=k\)
  3. 第三次跳转,\(B[4]=k\)\(T[6]=k\)

验算可以发现,PHRB 和 PHRT 的贡献全都被抵消,成功注入 \(PC[6]=k\)

最后来看要注入的 PC 更低的情况,例如要注入 \(PC[3]=k\),还是用三次跳转:

  1. 第一次跳转,\(B\) 相同,\(T[2]=k\)
  2. 第二次跳转,\(B[2]=k\)\(T[2]=T[3]=k\)
  3. 第三次跳转,\(B[3]=k\)\(T[3]=k\)

这样就成功注入 \(PC[3]=k\)

那么 PC 的注入就完成了,只有 \(PC[2]\) 没有找到合适的方法来注入。

逆向工程 index 函数

相连度和 tag 函数已知,接下来,让我们逆向工程最后的 index 函数。逆向工程的思路如下:

  1. 通过前面的逆向工程,发现 \(PC[5:2]\) 是独立出现在 tag 函数中,并且没有出现在 index 中,所以我们可以构造出多个分支,它们的 tag 不同;
  2. 进一步,构造两组条件分支,每组都有四个条件分支,因为是四路组相连,如果这两组分别映射到两个 set 中,就可以正确预测;反之,如果被映射到同一个 set 中,就会预测错误;
  3. 和之前类似,向 PC、PHRB 或 PHRT 注入两个随机数 \(k\)\(l\),然后预测两组共八个条件分支,这些条件分支的跳转方向都是 k xor l
  4. 如果 \(k\)\(l\) 注入的位同时出现在 index 函数中,但是没有异或关系,那么这八个条件分支可以正确地被预测;
  5. 如果 \(k\)\(l\) 注入的位同时出现在 index 函数中,并且有异或关系,那么这八个条件分支会被映射到同一个 set 内,最多只能正确预测其中四个分支;
  6. 如果 \(k\)\(l\) 注入的位至少出现一个在 tag 函数中,那么一个分支会在同一个 set 内占用两项,导致最多只能正确预测其中两个分支。

注入 \(PC[9]\)\(PHRT[i]\)实验结果如下:

结合上面的讨论,可以知道:

  • PC[9] 和 PHRT[38] 和 PHRT[88] 有异或关系
  • PHRT[2]、PHRT[12]、PHRT[17]、PHRT[22]、PHRT[27]、PHRT[33]、PHRT[43]、PHRT[53]、PHRT[58]、PHRT[63]、PHRT[68]、PHRT[73]、PHRT[78]、PHRT[83]、PHRT[93] 在 index 中,但是和 PC[9] 没有异或关系

实际上还有 PHRT[7] 和 PHRT[48] 也在 index 中,但实际上测试的时候为了保证历史最长的表提供预测,还额外注入了 PHRT[99],它与 PHRT[7] 和 PHRT[48] 有异或关系,所以在上图没有显现出来。

用类似的方法,去测试 PHRT 与 PHRT、PHRT 与 PHRB、PHRB 与 PHRB 之间的异或关系,就可以找到最终的 index 函数:

  • PHRT[2] xor PHRT[43] xor PHRT[93]
  • PHRT[7] xor PHRT[48] xor PHRT[99]
  • PHRT[12] xor PHRT[63] xor PHRB[5]
  • PHRT[17] xor PHRT[68] xor PHRB[10]
  • PHRT[22] xor PHRT[73] xor PHRB[15]
  • PHRT[27] xor PHRT[78] xor PHRB[20]
  • PHRT[33] xor PHRT[83] xor PHRB[25]
  • PHRT[38] xor PHRT[88] xor PC[9]
  • PHRT[53] xor PHRT[58] xor PHRB[0]
  • PC[6]

至此使用分支历史最长的表的逆向就完成了。接下来讨论一下,如何逆向工程分支历史更短的表。

逆向工程使用分支历史更短的 TAGE 表

前面提到,TAGE 在预测时,会选取提供预测的多个表中使用历史最长的那个表。那么,如果要测试使用历史第二长的表,应该怎么办呢?我们尝试了以下方法:

  • 在使用历史更长的表里,预先插入一些表项,再去测试历史较短的表的情况,由于 TAGE 会利用它的 useful 计数器来进行新表项的分配,当历史更长的表里的表项的 useful 不为零,可以防止它被覆盖成新的内容,逼迫 TAGE 用历史更短的表来进行预测;
  • 把多个表当成一个整体来考虑,比如在测试能够正常预测的分支数量的时候,得到的是多个表叠加的结果,再减去已知数量,可以得到历史比较短的表的信息;
  • 在超出要测试的表的历史部分,注入大量随机数,例如要测试的一个表,只用到了历史的低 57 位,那就在更高的部分注入大量的随机数,那么历史最长的表能够提供预测的概率会非常小,从而逼迫 TAGE 用当前被测试的表来做预测。

通过这些方法,我们成功地逆向出了 Firestorm 的剩下的 TAGE 表的信息:

有兴趣的读者可以试着自己复现一下,看看能不能得到对应的实验结果,然后从结果中分析出硬件的参数。有意思的是,我们逆向出来 Qualcomm Oryon 的分支预测器的大小(6 个表一共 80KB 的空间),与官方在 Hot Chips 上公开的是一致的。

总结

我们通过一系列方法,实现了对 Apple M1 Firestorm 的条件分支预测器的逆向,并且成功地把它应用到了设计基本一样的 Qualcomm Oryon 处理器上,为后续的研究提供了基础。

引用文献

本博客近三个月来的访问数据观察

2025-10-09 08:00:00

本博客近三个月来的访问数据观察

写在前面

这个博客自 2014 年更新至今,已走过近十一个年头,累计发布了四百多篇文章。出于好奇,我一直想了解哪些内容更受读者欢迎。五年前,我曾配置过 Google Analytics,但使用体验并不理想,于是转而自行部署了 rybbit 实例来收集访问数据。如今三个月过去,是时候与大家分享一些有趣的发现。

P.S. 如果你对数据收集有所顾虑,可以屏蔽对应的 analytics 脚本。

数据总览与趋势

首先来看这三个月内的整体访问情况与趋势:

访问量比我的预期要高一些。虽然这些年写了不少内容,但并没有刻意宣传,主要依赖搜索引擎推荐和读者的订阅与转发。从时间趋势上可以看出两个明显特点:

  1. 工作日访问量显著高于周末,通常为周末的两到三倍;
  2. 开学后工作日的访问量比暑假期间又高出一倍,但周末依然低迷。

由此推测,学生读者占比较高。结合国庆假期访问量的下降来看,国内读者仍是主力。下面是各国家与地区的访问分布:

值得一提的是,也有不少海外读者访问,看来近年来有意识地撰写英文内容确实产生了效果。

以 UTC+8 时区为基准,从访问时间分布中可以看出大家的作息习惯:

尽管大家可能习惯熬夜,但深夜阅读博客的并不多,多数访问集中在工作时间。

接下来是基于 User Agent 的统计。首先是浏览器分布:

不出所料,Chrome 及基于 Chromium 内核的浏览器占据主流,Firefox 和 Safari 占比不高。我本人目前也在使用 Firefox,希望它能持续发展,避免 Chrome 一家独大。

操作系统分布如下:

Windows 占比最高,macOS 次之。考虑到博客内容主要涉及计算机技术,这也大致反映了相关从业人员的偏好。

以下是访问次数较多的几篇文章:

这个排名有些出乎我的意料。这几篇文章在撰写时并未特别考虑入门读者的理解难度或内容的丰富性。或许是因为它们涉及的领域资料较少,因此在相关关键词搜索中容易被找到。这一点在 Google Search Console 中也得到了印证:

相比之下,一些精心准备的文章阅读量并不高,可能是因为所在领域已有较多优质内容,新文章难以脱颖而出。这也说明文章质量与阅读量之间并非简单的正比关系。

我还注意到一些重度用户,他们不仅阅读多篇文章,且停留时间较长:

  • 未知用户一:阅读了分支预测、乱序执行、CPU 微架构分析等文章,推测可能是刚开始研究 CPU 微架构的同行;
  • 未知用户二:浏览了 wishbone、乱序执行相关内容,猜测是学习清华计算机组成原理课程的学生;
  • 未知用户三:从华为相关文章进入,随后浏览了其他内容,可能是搜索华为内容进入博客后,被其他文章吸引的读者;
  • 未知用户四:阅读了 ARM/Samsung/Intel 等处理器微架构分析文章,又一位同行,但有一定基础,更加关注业界。

类似的例子还有不少,在此不一一列举。尽管 CPU 相关文章的阅读量不及软件类内容,但能得到这么多同行的关注,确实令人欣慰——毕竟这本身就是一个小众领域,不能奢求过高的阅读量。

通过这次数据分析,我收获了不少有趣的观察。未来可能会不定期更新类似内容,看看随着时间推移,是否会有新的发现。

最后,如果你对访问数据的收集感到不适,可以直接屏蔽 analytics 脚本(或许你的浏览器插件已经这样做了)。根据 rybbit 的官方说明,其信息收集方法较为尊重用户隐私,我也没有对代码进行任何修改,不放心的读者可以阅读 rybbit 的源码来审计。

P.S. 你能看出这篇文章是,我先写了一遍,然后让大模型润色的结果吗?

ARM 公版核微架构演进

2025-09-10 08:00:00

ARM 公版核微架构演进

背景

ARM 公版核微架构的演进频繁,型号又比较多,相关信息散落在各种地方,为了方便查阅,在这里做一个收集。

2025 年

C1-Ultra

  • Arm の新しい CPU「C1」は 2 桁パーセントの性能アップ。電力効率も大幅改善
  • Inside Arm's New C1‑Ultra CPU: Double‑Digit IPC Gains Again!
    • C1-Ultra: successor to Cortex X925
    • Branch prediction: Additional tracking for local/per-PC history
    • 33% increase in L1 I-Cache available bandwidth
    • Out of order window size growth: Up to 25% growth, Up to ~2K instruction in flight
    • 2x L1 data cache capacity (128KB)
    • Data prefetchers: array-indexing coverage
  • Arm® C1-Ultra Core Technical Reference Manual
    • Implementation of the Scalable Vector Extension (SVE) with a 128-bit vector length and Scalable Vector Extension 2 (SVE2)
    • Implementation of the Scalable Matrix Extension (SME) and Scalable Matrix Extension 2 (SME2), and support for the C1-SME2 unit
    • configure the L2 cache to be 2048KB or 3072KB
    • A 64KB, 4-way set associative L1 instruction cache with 64-byte cache lines
    • A fully associative L1 instruction Translation Lookaside Buffer (TLB) with native support for 4KB, 16KB, 64KB, and 2MB page sizes
    • A 128KB, 4-way set associative cache with 64-byte cache lines
    • A fully associative L1 data TLB with native support for 4KB, 16KB, and 64KB page sizes and 2MB and 512MB block sizes
    • L2 cache is private to the core and can be configured to be 2MB 8-way set associative or 3MB 12-way set associative
    • L1 instruction TLB, Fully associative, 128 entries
    • L1 data TLB, Fully associative, 96 entries
    • L1 Statistical Profiling Extension (SPE) TLB, Located in the SPE block, VA to PA translations of any page and block size, 1 entry
    • L1 TRace Buffer Extension (TRBE) TLB, VA to PA translations of any page and block size, 1 entry
    • L2 TLB, Shared by instructions and data, 8-way set associative, 2048 entries
    • L1 instruction cache, 64KB, 4-way set associative, Virtually Indexed, Physically Tagged (VIPT) behaving as Physically Indexed, Physically Tagged (PIPT), Pseudo-Least Recently Used (LRU) cache replacement policy for L1, 32 bytes per cycle interface with L2
    • L1 data cache, 128KB, 4-way set associative, Virtually Indexed, Physically Tagged (VIPT) behaving as Physically Indexed, Physically Tagged (PIPT), Re-Reference Interval Prediction (RRIP) replacement policy, 4×64-bit read paths and 4×64-bit write paths for the integer execute pipeline, 4×128-bit read paths and 4×128-bit write paths for the vector execute pipeline
    • L2 cache, 2MB 8-way set associative with 4 banks or 3MB 12-way set associative with 4 banks, Physically Indexed, Physically Tagged (PIPT), Dynamic biased cache replacement policy, One CHI Issue E compliant interfaces with 256-bit read and write DAT channel widths

C1-Pro

  • Arm Lumex C1-Pro CPU Core: What You Need to Know
    • C1-Pro: successor to Cortex-A725
    • Larger direction predictor and branch history
    • 2x capacity 0-cycle BTB
    • 16x capacity 1-cycle BTB
    • 50% more L1 Instruction TLB capacity
    • Increase effective L1D cache bandwidth
    • Lower latency L2 TLB hit
    • New indirect prefetcher

2024 年

Cortex X925

  • Arm Unveils 2024 CPU Core Designs, Cortex X925, A725 and A520: Arm v9.2 Redefined For 3nm Archive
    • Decode & Dispatch: 10-wide
    • SIMD/FP execution: 6x 128b
    • Integer ALU pipelines: 1- and 2-cycle operations
    • Integer multiply execution: 4x versus Cortex-X4
    • FP compare execution: 2x versus Cortex-X4
    • >2x increase in SIMD/FP issue queues
    • 2x increase in max instruction-window capacity(注:Cortex X4 是 384x2,推测 Cortex X925 是 768x2)
    • Sign-extension instruction elimination
    • Branch prediction: 2x instruction window size
    • Instruction Fetch: 2x increase in L1 I$ available bandwidth, 2x increase in L1 iTLB size, Fold out unconditional direct branches
    • 3 -> 4 load pipelines
    • 2x increase in L1 D$ available bandwidth
    • 25-40% in back-end OoO growth
  • Arm® Cortex-X925 Core Technical Reference Manual
    • Implementation of the Scalable Vector Extension (SVE) with a 128-bit vector length and Scalable Vector Extension 2 (SVE2)
    • configure the L2 cache to be 2048KB or 3072KB
    • A 64KB, 4-way set associative L1 instruction cache with 64-byte cache lines
    • A fully associative L1 instruction Translation Lookaside Buffer (TLB) with native support for 4KB, 16KB, 64KB, and 2MB page sizes
    • A 64KB, 4-way set associative cache with 64-byte cache lines
    • A fully associative L1 data TLB with native support for 4KB, 16KB and 64KB page sizes and 2MB and 512MB block sizes
    • L2 cache is private to the core and can be configured to be 2MB 8-way set associative or 3MB 12-way set associative
    • L1 instruction TLB, Caches entries at the 4KB, 16KB, 64KB, or 2MB granularity of Virtual Address (VA) to Physical Address (PA) mapping only, Fully associative, 128 entries
    • L1 data TLB, Caches entries at the 4KB, 16KB, 64KB, 2MB, or 512MB granularity of VA to PA mappings only, Fully associative, 96 entries
    • L2 TLB, Shared by instructions and data, VA to PA mappings for 4KB, 16KB, 64KB, 2MB, 32MB, 512MB, and 1GB block sizes, Intermediate Physical Address (IPA) to PA mappings for: 2MB and 1GB block sizes in a 4KB translation granule, 32MB block size in a 16KB translation granule, 512MB block size in a 64KB granule; Intermediate PAs (descriptor PAs) obtained during a translation table walk, 8-way set associative, 2048 entries
    • L1 instruction cache, 64KB, 4-way set associative, Virtually Indexed, Physically Tagged (VIPT) behaving as Physically Indexed, Physically Tagged (PIPT)
    • The Cortex®-X925 core supports the AArch64 prefetch memory instructions, PRFM PLI, into the L1 instruction cache or L2 cache
    • L1 data cache, 64KB, 4-way set associative, Virtually Indexed, Physically Tagged (VIPT) behaving as Physically Indexed, Physically Tagged (PIPT), Re-Reference Interval Prediction (RRIP) replacement policy, 4×64-bit read paths and 4×64-bit write paths for the integer execute pipeline, 4×128-bit read paths and 4×128-bit write paths for the vector execute pipeline
    • L2 cache, 2MB 8-way set associative with 4 banks or 3MB 12-way set associative with 4 banks, Physically Indexed, Physically Tagged (PIPT)
  • Arm® Cortex-X925 Core Software Optimization Guide

2023 年

Cortex X4

Cortex A720

2022 年

Cortex X3

  • Arm Unveils Next-Gen Flagship Core: Cortex-X3
    • 50% larger L1 + L2 (new) BTB capacity
    • 10x larger L0 BTB capacity
    • New predictor dedicated for indirect branches
    • Double return-stack capacity (32 entries)
    • Mop cache 50% capacity (1.5K entries)
    • Removed 1 pipeline stage in Mop Cache fetch, 10->9 cycles for a branch mispredict
    • Increase decode bandwidth: 5->6
    • Integer ALUs increase 4->6: 2->4 single-cycle (SX), 2 single-/multi-cycle (MX)
    • ROB/MCQ: 288x2 -> 320x2
    • Integer load bandwdith: 24B -> 32B
    • Additional data prefetch engines: Spatial, Pointer/Indirect
  • Arm® Cortex‑X3 Core Technical Reference Manual

Neoverse V2

  • Arm Neoverse V2 platform: Leadership Performance and Power Efficiency for Next-Generation Cloud Computing, ML and HPC Workloads
    • 6-wide/8-wide front-end
    • 64KB ICache
    • 320+ OoO window
    • 8-wide dispatch
    • 8-wide retire
    • 2 LS + 1 LD / cycle
    • 64KB DCache
    • 6-ALU + 2-branch
    • Quad 128-bit low latency SIMD datapath
    • L2 10-cycle load-to-use, 128B/cycle, private L2 cache 1 or 2 MB
    • Two predicted branches per cycle
    • Predictor acts as ICache prefetcher
    • 64kB, 4-way set-associative L1 instruction cache
    • Two-level Branch Target Buffer
    • 8 table TAGE direction predictor with staged output
    • 10x larger nanoBTB
    • Split main BTB into two levels with 50% more entries
    • TAGE: 2x larger tables with 2-way associativity, Longer history
    • Indirect branches: Dedicated predictor
    • Fetch bandwidth: Doubled instruction TLB and cache BW
    • Fetch Queue: Doubled from 16 to 32 entries
    • Fill Buffer: Increased size from 12 to 16 entries
    • Decode bandwidth: Increased decoder lanes from 5 to 6, Increased Decode Queue from 16 to 24 entries
    • Rename checkpoints: Increased from 5 to 6 total checkpoints, Increased from 3 to 5 vector checkpoints
    • Late read of physical register file – no data in IQs
    • Result caches with lazy writeback
    • Added two more single-cycle ALUs
    • Larger Issue Queues, SX/MX: Increased from 20 to 22 entries, VX: Increased from 20 to 28 entries
    • Predicate operations: Doubled predicate bandwidth
    • Zero latency MOV; Subset of register-register and immediate move operations execute with zero latency
    • Instruction fusion: More fusion cases, including CMP + CSEL/CSET
    • Two load/store pipes + one load pipe
    • 4 x 8B result busses (integer)
    • 3 x 16B result busses (FP, SVE, Neon)
    • ST to LD forwarding at L1 hit latency
    • RST and MB to reduce tag and data accesses
    • Fully-associative L1 DTLB with multiple page sizes
    • 64kB 4-way set associative Dcache
    • TLB Increased from 40 to 48 entries
    • Replacement policy Changed from PLRU to dynamic RRIP
    • Larger Queues: Store Buffer, ReadAfterRead, ReadAfterWrite
    • Efficiency: VA hash based store to load forwarding
    • Multiple prefetching engines training on L1 and L2 accesses: Spatial Memory Streaming, Best Offset, Stride, Correlated Miss Cache, Page
    • New PF engines: Global SMS – larger offsets than SMS, Sampling Indirect Prefetch – pointer dereference, TableWalk – Page Table Entrie
    • Private unified Level 2 cache, 8-way SA, 4 independent banks
    • 64B read or write per 2 cycles per bank = 128B/cycle total
    • 96-entry Transaction Queue
    • Inclusive with L1 caches for efficient data and instruction coherency
    • AMBA CHI interface with 256b DAT channels
    • Capacity 2MB/8-way with latency of 1MB (10-cycle ld-to-use)
    • Replacement policy 6-state RRIP (up from 4)
  • Hot Chips 2023: Arm’s Neoverse V2
  • Arm® Neoverse™ V2 Core Technical Reference Manual
  • Arm Neoverse V2 Software Optimization Guide

2021 年

Cortex X2

2020 年

Neoverse N2

  • Arm Neoverse N2: Arm’s 2nd generation high performance infrastructure CPUs and system IPs
    • Branch Prediction, 2x 8 instrs (up to 2 taken per cycle), 2x improvement
    • Nano BTB (0 cyc taken-branch bubble), 64 entry, 4x improvement
    • Conditional branch direction state, 1.5x improvement
    • Main BTB, 8K entry, 1.33x improvement
    • Alt-Path Branch Prediction
    • 64KB Instruction cache
    • 1.5K entry Mop Cache
    • 16-entry Fetch Queue, 1.33x improvement
    • Fetch Width: 4 instr from i$, 5 instr from MOP$, Up to 1.5x improvement
    • Early branch redirect: uncond + cond
    • Decode width: 4 (I-cache) or 5 (Mop cache), Up to 1.25x improvement
    • Branch predict up to 16-inst/cycle, 2-taken/cycle
    • New Macro-op (MOP) cache with 1.5k entries
    • 50% larger branch direction predicton
    • 33% larger BTB with shorter average latency
    • Early re-steering for conditional branches that miss the BTB
    • Rename width: 5 instrs, 1.2x improvement
    • Rename Checkpointing: Yes
    • ROB size: 160+, 1.25x improvement
    • ALUs: 4, 1.33x improvement
    • Branch resolution: 2 per cycle, 2x improvement
    • Overall Pipeline Depth: 10 cycles, 1.1x improvement
    • 64KB L1 Data cache
    • Private 512KB/1MB L2 Cache
    • AGU: 2-LD/ST + 1 LD, 1.5x improvement
    • L1 LD Hit bandwidth: 3x 16B/cycle, 1.5x improvement
    • Store data B/W: 32B/cycle, 2x improvement
    • L2 bandwidth: 64B read + 64B write, 2x improvement
    • L2 transactions: 64, 1.3x improvement
    • Data Prefetch Engines: Stride, spatial/region, stream, temporal
    • Correlated Miss Caching (CMC) prefetching

Neoverse V1

  • SW defined cars: HPC, from the cloud to the dashboard for an amazing driver experience
    • Faster run-ahead for prefetching into the I$ (2x32B bandwidth)
    • 33% larger BTBs (8K entry)
    • 6x nano BTB (96 entry), zero-cycle bubble
    • 2x number of concurrent code regions tracked in front-end
    • Introduction of Mop Cache, L0 decoded instruction cache (3K entry)
    • high dispatch bandwidth, 8-instrs per cycle, 2x increase, I$ decode bandwidth increased from 4x to 5x
    • Lower latency decode pipeline by 1 stage
    • OoO window size, 2x+ ROB (256 entry + compression)
    • Increase superscalar integer execution bandwidth, 1->2 Branch Execution, 3->4 ALU
    • 2x vector/fp bandwidth, 2x256b – SVE (new), 4x128b – Neon/FP
    • 3rd LD AGU/pipe (50% incr), LS LS LD
    • LD/ST data bandwidth, LD: 2x16B -> 3x16B, LD (SVE): 2x32B, ST: 16B -> 32B (2x), broken out into separate issue pipes
    • Number of outstanding external memory transactions (48->96)
    • MMU capacity 1.2K->2K entry (67% incr)
    • L2 latency reduced by 1 cycle for 1M (now 10cyc load to use)
    • 11+ stage accordion pipeline
    • 8-wide front-end / 15-wide issue
    • Four 64-bit integer ALUs + two dedicated Branch units
    • 2x 256-bit SVE datapaths
    • 4x 128-bit Neon/FP datapaths
    • 3x load / store addr
    • 3x load data & 2x store data pipeline
    • 8-wide Instruction fetch
    • 5-8 wide decode / rename
    • pipeline: P1 P2 F1 F2 DE1 RR RD I0 I1 I2 ...

Cortex X1

Cortex A78

2019 年

Neoverse N1

  • The Arm Neoverse N1 Platform: Building Blocks for the Next-Gen Cloud-to-Edge Infrastructure SoC
    • 4-wide front-end
    • dispatching/committing up to 8 instructions per cycle
    • three ALUs, a branch execution unit, two Advanced SIMD units, and two load/store execution units
    • minimum misprediction penalty is 11-cycle
    • fetch up to 4 instructions per cycle
    • large 6K-entry main branch target buffer with 3-cycle access latency
    • 64-entry micro-BTB and a 16-entry nano-BTB
    • 12-entry fetch queue
    • fully associative 48-entry instruction TLB
    • 4-way set-associative 64KB I-cache
    • I-cache can deliver up to 16B of instructions per cycle
    • up to 8 outstanding I-cache refill requests
    • 4-wide decoder
    • renaming unit can receive up to 4 macro-ops per cycle
    • up to 8 micro-operations can be dispatched into the out-of-order engine each cycle
    • The commit queue can track up to 128 micro operations
    • up to 8 micro-ops can be committed per cycle
    • a distributed issue queue with more than 100 micro-operations
    • 4 integer execution pipelines, 2 load/store pipelines, and 2 Advanced SIMD pipelines
    • 64kB 4-way set associative L1 data cache, 4-cycle load to use latency and a bandwidth of 32 bytes/cycle
    • The core-private 8-way set associative L2 cache is up to 1MB in size and has a load-to-use latency of 11 cycles
    • can also be configured with smaller L2 cache sizes of 256kB and 512kB with a load-to-use latency of 9 cycles
    • L2 cache connects to the system via an AMBA 5 CHI interface with 16-byte data channels
    • L3 cluster cache can be up to 2MB, with a load-to-use latency ranging between 28 and 33 cycles
    • up to 256MB of shared system-level cache

AMD Zen 3 的 BTB 结构分析

2025-07-08 08:00:01

AMD Zen 3 的 BTB 结构分析

背景

在之前,我们分析了 AMD Zen 1AMD Zen 2 的 BTB,接下来分析它的再下一代微架构:2020 年发布的 AMD Zen 3 的 BTB,看看 AMD 的 Zen 系列的 BTB 是如何演进的。

官方信息

AMD 在 Software Optimization Guide for AMD EPYC™ 7003 Processors (Publication No. 56665) 中有如下的表述:

The branch target buffer (BTB) is a two-level structure accessed using the fetch address of the previous fetch block.

Zen 3 的 BTB 有两级,相比 Zen 1 和 Zen 2 少了一级。BTB 是用之前 fetch block 的地址去查询,而不再是当前 fetch block 的地址。用当前 fetch block 的地址查询 BTB 很好理解,要寻找某个地址开始的第一个分支,就用这个地址去查询 BTB,Zen 1 和 Zen 2 都是如此;用之前 fetch block 的地址,则是用更早的信息,去获取当前 fetch block 的信息,例如:

entrypoint1: jmp entrypoint2entrypoint2: # what's the first branch after entrypoint2?

在查询从 entrypoint2 开始的第一条分支指令的时候,如果使用当前 fetch block,就是用 entrypoint2 的地址去查询,那就必须等到前面 jmp entrypoint2 指令的目的地址被计算得出;如果使用之前 fetch block,就是用 entrypoint1 的地址去查询,不用等到 jmp entrypoint2 指令的目的地址被计算得出。因此,如果用之前 fetch block,可以更早地进行 BTB 的访问,从而减少 BTB 的延迟,或者在相同延迟下获得更大的容量。但是,代价是:

  • 从 entrypoint1 跳转到的 fetch block 可能有多个,例如最后一条是间接分支指令,那就需要找到正确的分支的信息
  • 可能会从不同的地址跳转到 entrypoint2 这个 fetch block,因此它的信息可能会保存多份

Each BTB entry can hold up to two branches if the last bytes of the branches reside in the same 64-byte aligned cache line and the first branch is a conditional branch.

Zen 3 的 BTB entry 有一定的压缩能力,一个 entry 最多保存两条分支,前提是两条分支在同一个 64B 缓存行中,并且第一条分支是条件分支。这样,如果第二条分支是无条件分支,分支预测的时候,可以根据第一条分支的方向预测的结果,决定要用哪条分支的目的地址作为下一个 fetch block 的地址。虽然有压缩能力,但是没有提到单个周期预测两条分支,所以只是扩大了等效 BTB 容量。和 Zen 1、Zen 2 一样。

L1BTB has 1024 entries and predicts with zero bubbles for conditional and unconditional direct branches, and one cycle for calls, returns and indirect branches.

Zen 3 的第一级 BTB 可以保存 1024 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;针对条件和无条件直接分支的预测不产生气泡,意味着它的延迟是一个周期。相比 Zen 2 容量翻倍,且延迟降低一个周期,猜测和使用 previous fetch block 有关。

L2BTB has 6656 entries and creates three bubbles if its prediction differs from L1BTB.

Zen 3 的第二级 BTB 可以保存 6656 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生三个气泡,意味着它的延迟是四个周期。

简单整理一下官方信息,大概有两级 BTB:

  • 1024-entry L1 BTB, 1 cycle latency
  • 6656-entry L2 BTB, 4 cycle latency

相比 Zen 1 和 Zen 2 有比较大的不同:去掉了原来很小的 L0 BTB,扩大了 L1 BTB,同时延迟缩短了一个周期;虽然 L2 BTB 有所缩小,但是延迟也变短了一个周期。

下面结合微架构测试,进一步研究它的内部结构。

微架构测试

在之前的博客里,我们已经测试了各种处理器的 BTB,在这里也是一样的:按照一定的 stride 分布无条件直接分支,构成一个链条,然后测量 CPI。

考虑到 Zen 3 的 BTB 可能出现一个 entry 保存两条分支的情况,并且还对分支的类型有要求,因此下面的测试都会进行四组,分别对应四种分支模式:

  • uncond:所有分支都是无条件分支:uncond, uncond, uncond, uncond, ...
  • cond:所有分支都是条件分支:cond, cond, cond, cond, ...
  • mix (uncond + cond):条件分支和无条件分支轮流出现,但 uncond 在先:uncond, cond, uncond, cond, ...
  • mix (cond + uncond):条件分支和无条件分支轮流出现,但 cond 在先:cond, uncond, cond, uncond, ...

虽然 Zen 3 使用 previous fetch block 来访问 BTB,但在这几种分支模式中,使用 previous fetch block 还是访问 current fetch block,结果都是唯一的,所以并不会对结果带来影响。

stride=4B

首先是 stride=4B 的情况:

可以看到,图像上出现了三个比较显著的拐点:

  • 第一个拐点是 4 条分支,CPI=1,对应 L1 BTB,没有达到完整容量,可能是因为分支太过密集
  • 第二个拐点是 2048 条分支,CPI=3.6;第三个拐点是 4096 条分支,CPI=4/4.2/4.4

Zen 3 在 stride=4B 的情况下 L1 BTB 表现比较一般,应该是牺牲了高密度分支下的性能;而主要命中的是 L2 BTB,在不同的分支模式下,测出来差不多的结果。为了验证这一点,统计了如下的性能计数器(来源:Processor Programming Reference (PPR) for AMD Family 19h Model 21h, Revision B0 Processors):

PMCx08B [L2 Branch Prediction Overrides Existing Prediction (speculative)] (Core::X86::Pmc::Core::BpL2BTBCorrect)

它代表了 L2 BTB 提供预测(准确地说,L2 BTB 提供了预测且和 L1 BTB 提供的预测结果不同,覆盖了 L1 BTB 的预测结果)的次数,当分支数不大于 4 的时候,这个计数器的值约等于零;此后快速上升,说明后续都是 L2 BTB 在提供预测。

更进一步观察,发现 2048 到 4096 的 CPI 上升,来自于 L1 BTB 完全失效:2048 条分支时,L1 BTB 还能提供约 10% 的预测,所以 CPI=0.1*1+0.9*4=3.7,但到 4096 条分支的时候,完全由 L2 BTB 提供分支,此时 CPI=4。

超过 4096 以后,则 L2 BTB 也开始缺失,出现了译码时才能发现的分支,如果这是一条 uncond 分支,那么会在译码时回滚,这一点可以通过如下性能计数器的提升来证明(来源:Processor Programming Reference (PPR) for AMD Family 19h Model 21h, Revision B0 Processors):

PMCx091 [Decode Redirects] (Core::X86::Pmc::Core::BpDeReDirect): The number of times the instruction decoder overrides the predicted target.

但在 L2 BTB 缺失后,如果译码器发现了 cond 分支,会把它预测为不跳转,所以要等到执行才能发现分支预测错误。这就导致了 cond 模式下 L2 BTB 溢出时 CPI=16,而 uncond 模式下 L2 BTB 溢出时 CPI=12,提前在译码阶段发现了 uncond 分支并纠正。

但译码器的纠正能力不是万能的:假如它首先发现了一条 cond 分支,在它其后又发现了一条 uncond 分支,它会用 uncond 分支去纠正,但实际上前面的 cond 分支会跳转,所以此时译码器纠正也无法提升性能,即使 BpDeReDirect 计数器的值看起来很大。

stride=8B

接下来观察 stride=8B 的情况:

  • 第一个台阶在所有分支模式下都是 1024 个分支,CPI=1,对应 1024-entry 的 L1 BTB
  • 第二个台阶不太明显,但是在 4096 附近在所有分支模式下都是一个拐点,CPI=4,对应 L2 BTB;在 mix (uncond + cond) 模式下,超过 4096 分支后 CPI 缓慢上升,到 6144 条分支 CPI=4.25,到 6656 条分支 CPI=4.85,之后 CPI 快速上升;在 mix (cond + uncond) 模式下,到 5888 条分支 CPI=5。

L2 BTB 的容量不太确定,超过 4096 后需要一个 entry 保存两条分支才能获得更多容量,但也带来了一定的额外的延迟。与此同时 4096 也对应了 32KB ICache 的容量,这会对分析带来干扰。

从 BpDeReDirect 计数器来看,uncond 分支模式下,当分支数量超过 4096 后,L2 BTB 从 4096 时无缺失,之后缺失快速提升,说明此时 L2 BTB 容量确实是 4096。在 mix (cond + uncond) 模式下,分支数超过 4096 时,BpDeReDirect 计数器略微上升,直到 6144 条分支后才有明显的上升。

stride=16B

继续观察 stride=16B 的情况:

相比 stride=8B,L1 BTB 的行为没有变化。4096 对应的 CPI 有所下降,从 BpL2BTBCorrect 性能计数器可以发现是 L1 BTB 起了一定的作用。在 mix (cond + uncond) 模式下,直到 5632 条分支还维持了 CPI=3.25,之后 CPI 缓慢上升,到 6656 条分支时 CPI=3.75,到 6912 条分支时 CPI=4。

CPI=3.25 可能是来自于 1 和 4 的加权平均:25% 的时候是 1 周期,75% 的时候是 4 周期,平均下来就是 1*0.25+4*0.75=3.25。这意味着 L1 BTB 还要保持 25% 的命中率。观察 BpL2BTBCorrect 性能计数器,发现它的取值等于 75% 的分支执行次数,意味着 L1 BTB 确实提供了 25% 的预测,L2 BTB 提供了剩下 75% 的预测。这一点是挺有意思的,意味着 L1 BTB 可能采用了一些对这种循环访问模式友好的替换策略:朴素的 LRU(或类 LRU)替换策略会导致 L1 BTB 出现 100% 缺失。

stride=32B

继续观察 stride=32B 的情况:

相比 stride=16B,L1 BTB 的行为没有变化,但是出现了一些性能波动。所有分支模式下,L2 BTB 的拐点都出现在 5120,但性能波动比较大,mix (cond + uncond) 模式下的 CPI 达到了 4.6。通过 BpDeReDirect 性能计数器的变化,可以确认这个拐点确实是来自于 L2 BTB 的缺失。

前面提到,译码器的纠正能力可能会给出错误的答案,在 stride=32B 时,就会出现一个很有意思的现象:

  • 超出 L2 BTB 容量后,mix (uncond + cond) 模式下 BpDeReDirect 占分支数量的 50%
  • 超出 L2 BTB 容量后,mix (cond + uncond) 模式下 BpDeReDirect 占分支数量的接近 100%

解释起来也并不复杂:stride=32B 的情况下,一个 64B cacheline 只有两条分支,那么:

  • mix (uncond + cond) 模式下,第一条分支是 uncond,译码器会发现并 redirect;第二条分支是 cond,译码器会无视它,不进行 redirect;所以最后是 50% 的 redirect 比例
  • mix (cond + uncond) 模式下,第一条分支是 cond,译码器会看到后面的 uncond 分支并 redirect;第二条分支是 uncond,译码器会发现并 redirect;所以最后是接近 100% 的 redirect 比例

顺带一提,uncond 模式下的 BpDeReDirect 占分支数量的接近 100%,cond 模式下的 BpDeReDirect 占分支数量的 0%,都是符合预期的。

stride=64B

继续观察 stride=64B 的情况:

相比 stride=32B,L1 BTB 的容量减半,达到了 512。之后出现了比较明显的性能波动,但四种分支模式下,拐点依然都是出现在 5120 条分支的位置。通过 BpDeReDirect 性能计数器的变化,可以确认这个拐点确实是来自于 L2 BTB 的缺失。由于 uncond 模式下,BTB sharing 不会工作,意味着 L2 BTB 至少有 5120 个 entry。

stride=128B

继续观察 stride=128B 的情况:

相比 stride=64B,L1 BTB 的容量进一步减小,达到了 256;L2 BTB 的性能依然波动剧烈,但四种分支模式下,拐点依然都是出现在 5120 条分支的位置。

考虑到 5120 这个拐点频繁出现,认为 L2 BTB 在不考虑 BTB entry sharing 的情况下,实际容量应该是 5120。那么剩下的 1536 个分支就是来自于压缩。

小结

测试到这里就差不多了,更大的 stride 得到的也是类似的结果,总结一下前面的发现:

  • L1 BTB 是 1024-entry,1 cycle latency,容量随着 stride 变化,大概率是 PC[n:5] 这一段被用于 index,使得 stride=64B 开始容量不断减半
  • L2 BTB 是 5120-entry,4 cycle latency;其中有 1536 个 entry 最多保存两条分支,前提是这两条分支在同一个 cacheline 当中,并且第一条是 cond,第二条是 uncond

Zen 1 到 Zen 3 的 BTB 的对比

下面是对比表格:

uArch AMD Zen 1 AMD Zen 2 AMD Zen 3
L0 BTB size 4+4 branches 8+8 branches N/A
L0 BTB latency 1 cycle 1 cycle N/A
L1 BTB size 256 branches 512 branches 1024 branches
L1 BTB latency 2 cycles 2 cycles 1 cycle
L2 BTB size w/o sharing 2K branches 4K branches 5K branches
L2 BTB size w/ sharing 4K branches 7K branches 6.5K branches
L2 BTB latency 5 cycles 5 cycles 4 cycles
Technology Node 14nm 7nm 7nm
Release Year 2017 2019 2020

Zen 3 在 Zen 2 的基础上,没有更换制程,而是通过 previous fetch block 的方式,减少 L1 BTB 的延迟到 1 cycle,顺带去掉了 L0 BTB。L2 BTB 的大小进行了调整,减少了共享的部分,而增加了不限制分支类型的 BTB entry 数量,同时减少了一个周期的延迟,不确定这个延迟是单纯通过优化容量实现的,还是说也依赖了 previous fetch block 的方法来减少周期,更倾向于是后者,因为 L1 和 L2 BTB 都减少了一个周期的延迟。

如果按照 Intel 的 tick-tock 说法,那么 Zen 2 相比 Zen 1 是一次 tick,更换制程,微架构上做少量改动;Zen 3 相比 Zen 2 是一次 tock,不更换制程,但是在微架构上做较多改动。Zen 4 是 2022 年发布的,使用的是 5nm 制程;Zen 5 是 2024 年发布的,使用的是 4nm 制程。总结一下规律,AMD 会花费两年的时间来升级制程,并且实际上,Zen 4 和 Zen 5 不仅更新了制程,还在前端微架构上有较大的改动。

AMD Zen 3 和 ARM Neoverse V1 的 BTB 的对比

AMD Zen 3 和 ARM Neoverse V1 都是在 2020 发布的处理器,下面对它们进行一个对比:

uArch AMD Zen 3 ARM Neoverse V1
L1/Nano BTB size 1024 branches 48*2 branches
L1/Nano BTB latency 1 cycle 1 cycle
L1/Nano BTB throughput 1 branch 1-2 branches
L2/Main BTB size w/o sharing 5K branches 4K*2 branches
L2/Main BTB size w/ sharing 6.5K branches 4K*2 branches
L2/Main BTB latency 4 cycles 2 cycles
L2/Main BTB throughput 1 branch 1-2 branches
Technology Node 7nm 5nm

虽然 AMD Zen 3 通过 previous fetch block 优化,实现了 1 cycle 下更大的 L1 BTB,但这一点在 2022 年发布的 ARM Neoverse V2 上被追赶:ARM Neoverse V2 的 L1/Nano BTB 也做到了 1024 的容量。

在 L2 BTB 方面,ARM Neoverse V1 占据了领先,无论是延迟还是容量;当然了,ARM Neoverse V1 的制程也要更加领先,ARM 采用的 5nm 对比 AMD 采用的 7nm。

更进一步,ARM Neoverse V1 实现了一个周期预测两条分支,即 two taken(ARM 的说法是 two predicted branches per cycle),在 2 cycle 的 Main BTB 上可以实现接近 AMD Zen 3 的 L1 BTB 的预测吞吐。AMD 也不甘示弱,在 2022 年发布的 AMD Zen 4 处理器上,实现了 two taken。

AMD Zen 2 的 BTB 结构分析

2025-07-08 08:00:00

AMD Zen 2 的 BTB 结构分析

背景

在之前,我们分析了 AMD Zen 1 的 BTB,接下来分析它的下一代微架构:2019 年发布的 AMD Zen 2 的 BTB,看看 AMD 的 Zen 系列的 BTB 是如何演进的。

官方信息

AMD 在 Software Optimization Guide for AMD EPYC™ 7002 Processors (Publication No. 56305) 中有如下的表述:

The branch target buffer (BTB) is a three-level structure accessed using the fetch address of the current fetch block.

Zen 2 的 BTB 有三级,是用当前 fetch block 的地址去查询,和 Zen 1 一样。

Each BTB entry includes information for branches and their targets. Each BTBentry can hold up to two branches if the branches reside in the same 64-byte aligned cache line and the first branch is a conditional branch.

Zen 2 的 BTB entry 有一定的压缩能力,一个 entry 最多保存两条分支,前提是两条分支在同一个 64B 缓存行中,并且第一条分支是条件分支。这样,如果第二条分支是无条件分支,分支预测的时候,可以根据第一条分支的方向预测的结果,决定要用哪条分支的目的地址作为下一个 fetch block 的地址。虽然有压缩能力,但是没有提到单个周期预测两条分支,所以只是扩大了等效 BTB 容量。和 Zen 1 一样。

L0BTB holds 8 forward taken branches and 8 backward taken branches, and predicts with zero bubbles

Zen 2 的第一级 BTB 可以保存 8 条前向分支和 8 条后向分支,预测不会带来流水线气泡,也就是说每个周期都可以预测一次。相比 Zen 1 容量翻倍。

L1BTB has 512 entries and creates one bubble if prediction differs from L0BTB

Zen 2 的第二级 BTB 可以保存 512 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生单个气泡,意味着它的延迟是两个周期。相比 Zen 1 容量翻倍。

L2BTB has 7168 entries and creates four bubbles if its prediction differs from L1BTB.

Zen 2 的第三级 BTB 可以保存 7168 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生四个气泡,意味着它的延迟是五个周期。

简单整理一下官方信息,大概有三级 BTB:

  • (8+8)-entry L0 BTB, 1 cycle latency
  • 512-entry L1 BTB, 2 cycle latency
  • 7168-entry L2 BTB, 5 cycle latency

从表述来看,除了容量以外基本和 Zen 1 一致,猜测是在 Zen 1 的基础上扩大了容量。

下面结合微架构测试,进一步研究它的内部结构。

微架构测试

在之前的博客里,我们已经测试了各种处理器的 BTB,在这里也是一样的:按照一定的 stride 分布无条件直接分支,构成一个链条,然后测量 CPI。

考虑到 Zen 2 的 BTB 可能出现一个 entry 保存两条分支的情况,并且还对分支的类型有要求,因此下面的测试都会进行四组,分别对应四种分支模式:

  • uncond:所有分支都是无条件分支:uncond, uncond, uncond, uncond, ...
  • cond:所有分支都是条件分支:cond, cond, cond, cond, ...
  • mix (uncond + cond):条件分支和无条件分支轮流出现,但 uncond 在先:uncond, cond, uncond, cond, ...
  • mix (cond + uncond):条件分支和无条件分支轮流出现,但 cond 在先:cond, uncond, cond, uncond, ...

stride=4B

首先是 stride=4B 的情况:

可以看到,图像上出现了三个比较显著的台阶:

  • 所有分支模式下,第一个台阶都是到 8 条分支,CPI=1,8 对应了 8-entry 的 L0 BTB
  • 所有分支模式下,第二个台阶都是到 256 条分支,CPI=2,对应了 512-entry 的 L1 BTB,只体现出了一半的容量;但在 mix (uncond + cond) 和 mix (cond + uncond) 模式下,分支从 256 到 512 时 CPI 缓慢上升,意味着 L1 BTB 的 512-entry 还是可以完整访问,只是带来了一定的开销:CPI 从 2 增加到了 2.5
  • 在 uncond 和 cond 模式下,第三个台阶到 4096 条分支,CPI=5,对应 L2 BTB,没有显现出完整的 7168 的大小
  • 在 mix (uncond + cond) 模式下,第三个台阶延伸到了 5120,超出了 4096,依然没有显现出完整的 7168 的大小
  • 在 mix (cond + uncond) 模式下,第三个台阶延伸到了 7168,显现出完整的 7168 的大小

和 Zen 1 不同,Zen 2 的 L1 BTB 出现了不同模式下容量不同的情况,原因未知,后续还会看到类似的情况。

Zen 2 的 L2 BTB 依然是带有压缩的,只有在 mix (cond + uncond) 模式下才可以尽可能地用上所有的容量,而其余的三种模式都有容量上的损失。

stride=8B

接下来观察 stride=8B 的情况:

现象和 stride=4B 基本相同,L1 BTB 从 256 到 512 部分的变化斜率有所不同,其余部分一致。

stride=16B

继续观察 stride=16B 的情况:

相比 stride=4B/8B,L0 BTB 和 L2 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量减半到了 128,意味着 L1 BTB 采用了组相连,此时有一半的 set 不能被用上。此外,比较特别的是,从 stride=16B 开始,CPI=5 的平台出现了波动,uncond 模式下 CPI 从 5 变到 4 再变到了 5,猜测此时 L1 BTB 也有一定的比例会介入。

stride=32B

继续观察 stride=32B 的情况:

相比 stride=16B,L0 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量进一步减到了 64,符合组相连的预期;L2 BTB 在 mix (uncond + cond) 模式下不再能体现出 5120 的容量,而是 4096:此时在一个 64B cacheline 中只有两条分支,第一条分支是 uncond,第二条分支是 cond,不满足 entry 共享的条件(必须 cond + uncond,不能是 uncond + cond),此时 uncond 和 cond 分别保存在两个 entry 中,每个 entry 只保存一条分支,因此 L2 BTB 只能体现出 4096 的容量。而 mix (cond + uncond) 模式依然满足 entry 共享的条件,所以依然体现出 7168 的容量。特别地,在 mix (cond + uncond) 模式下出现了非常剧烈的 CPI 抖动,可能出现了一些预期之外的性能问题。

stride=64B

继续观察 stride=64B 的情况:

相比 stride=32B,L0 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量进一步减到了 32,符合组相连的预期,但 cond 模式下依然保持了 512 的容量;L2 BTB 在 mix (cond + uncond) 模式下只能体现出 4096 的容量,此时每个 64B cacheline 都只有一条分支,不满足两条分支共享一个 entry 的条件。

stride=128B

继续观察 stride=128B 的情况:

相比 stride=64B,L0 BTB 的行为没有变化;除了 cond 模式以外,L1 BTB 的容量进一步减到了 16,符合组相连的预期,而 cond 模式下 L1 BTB 容量也减少到了 256;L2 BTB 的容量减半到了 2048,意味着 L2 BTB 也是组相连结构。

小结

测试到这里就差不多了,更大的 stride 得到的也是类似的结果,总结一下前面的发现:

  • L0 BTB 是 (8+8)-entry,1 cycle latency,不随着 stride 变化,全相连
  • L1 BTB 是 512-entry,2 cycle latency,容量随着 stride 变化,大概率是 PC[n:3] 这一段被用于 index,使得 stride=16B 开始容量不断减半;但 cond 模式下的行为和其余几种模式不同,直到 stride=128B 才开始容量减半
  • L2 BTB 是 4096-entry,5 cycle latency,容量随着 stride 变化,大概率是 PC[n:6] 这一段被用于 index,使得 stride=128B 开始容量不断减半;其中有 3072 个 entry 最多保存两条分支,前提是这两条分支在同一个 cacheline 当中,并且第一条是 cond,第二条是 uncond;因此最多保存 7168 条分支

Zen 1 和 Zen 2 的 BTB 的对比

下面是对比表格:

uArch AMD Zen 1 AMD Zen 2
L0 BTB size 4+4 branches 8+8 branches
L0 BTB latency 1 cycle 1 cycle
L1 BTB size 256 branches 512 branches
L1 BTB latency 2 cycles 2 cycles
L2 BTB size w/o sharing 2K branches 4K branches
L2 BTB size w/ sharing 4K branches 7K branches
L2 BTB latency 5 cycles 5 cycles
Technology Node 14nm 7nm
Release Year 2017 2019

可见 Zen 2 在容量上做了一定的扩展,但机制上比较类似;特别地,可能是观察到 cond + uncond 的压缩能够生效的比例没有那么高,所以只允许其中一部分 entry 被压缩,例如 4 路组相连,只有前 3 个 way 是可以保存两条分支;剩下的一个 way 只能保存一条分支。

AMD Zen 2 和 ARM Neoverse N1 的 BTB 的对比

AMD Zen 2 和 ARM Neoverse N1 都是在 2019 发布的处理器,下面对它们进行一个对比:

uArch AMD Zen 2 ARM Neoverse N1
L0/Nano BTB size 8+8 branches 16 branches
L0/Nano BTB latency 1 cycle 1 cycle
L1/Micro BTB size 512 branches 64 branches
L1/Micro BTB latency 2 cycles 2 cycles
L2/Main BTB size w/o sharing 4K branches 3K*2 branches
L2/Main BTB size w/ sharing 7K branches 3K*2 branches
L2/Main BTB latency 5 cycles 2-3 cycles
Technology Node 7nm 7nm

可见 AMD Zen 2 在 BTB 容量上有优势,但是延迟要更长;两者都在最后一级 BTB 上做了压缩,但是压缩的方法和目的不同:

  • AMD Zen 2 的压缩方法是,把同一个 64B cacheline 内一条 cond 和一条 uncond 指令放在同一个 entry 当中。这样做的好处是,当预测到 cond 分支不跳转的时候,可以直接根据 uncond 指令的信息,得到下一个 fetch block 的地址;但是也对代码的结构有要求,必须是在同一个 cacheline 中,依次出现一个 cond 和一个 uncond
  • ARM Neoverse N1 的压缩方法是,根据立即数范围对分支进行分类,如果分支的立即数范围比较小,就只占用一个 entry 的一半也就是 41 bit;如果分支的立即数范围过大,就占用一个完整的 82 bit 的 entry;这主要是一个减少 SRAM 占用的优化,避免了所有的分支都要记录完整的 82 bit 信息;对代码的结构要求比较小,只要是跳转距离不太远的分支,都可以存到 41 bit 内

二者都没有实现一个周期预测两条分支,即 two taken(ARM 的说法是 two predicted branches per cycle)。这要等到 2020 年的 ARM Neoverse N2/V1,或者 2022 年的 AMD Zen 4 才被实现。

注意到 AMD 的 Software Optimization Guide for AMD EPYC™ 7002 Processors (Publication No. 56305) 文档里,有这么一段表述:

Branches whose target crosses a half-megabyte aligned boundary are unable to be installed in the L0 BTB or to share BTB entries with other branches.

也就是说,如果两个分支要共享一个 BTB entry,那么它们的目的地址不能跨越 512KB 边界,也就是和分支地址的偏移量不超过 19 位。按 48 位虚拟地址计算,如果 BTB entry 只记录一条分支,最多需要记录目的地址的完整 48 位地址;如果现在 BTB entry 要存两条分支,这两条分支的目的地址都只需要记录 19 位,加起来也就 38 位,还可以空余 10 位的信息用来维护 BTB sharing 所需的额外信息。

所以说到底,无论是 AMD 还是 ARM,做的事情都是对一个固定长度的 entry 设置了不同的格式,一个格式保存的地址位数多,但是只能保存一个分支;另一个格式保存的地址位数少,但是可以保存两个分支。区别就是 AMD 对两个分支的类型和位置有要求,而 ARM 允许这两个分支毫无关系。这就是不同厂商的取舍了。

AMD Zen 1 的 BTB 结构分析

2025-07-07 08:00:00

AMD Zen 1 的 BTB 结构分析

背景

AMD Zen 1 是 AMD 在 2017 年发布的 Zen 系列第一代微架构。在之前,我们分析了 ARM Neoverse N1V1 的 BTB,那么现在也把视线转到 AMD 上,看看 AMD 的 Zen 系列的 BTB 是如何演进的。

官方信息

AMD 在 Software Optimization Guide for AMD Family 17h Processors (Publication No. 55723) 中有如下的表述:

The branch target buffer (BTB) is a three-level structure accessed using the fetch address of the current fetch block.

Zen 1 的 BTB 有三级,是用当前 fetch block 的地址去查询。

Each BTB entry includes information for branches and their targets. Each BTB entry can hold up to two branches if the branches reside in the same 64-byte aligned cache line and the first branch is a conditional branch.

Zen 1 的 BTB entry 有一定的压缩能力,一个 entry 最多保存两条分支,前提是两条分支在同一个 64B 缓存行中,并且第一条分支是条件分支。这样,如果第二条分支是无条件分支,分支预测的时候,可以根据第一条分支的方向预测的结果,决定要用哪条分支的目的地址作为下一个 fetch block 的地址。虽然有压缩能力,但是没有提到单个周期预测两条分支,所以只是扩大了等效 BTB 容量。

例如,有这么一段代码:

# fetch block entrypointentrypoint:# do somethingjnz targetA# do somethingjmp targetB

那么 jnz 和 jmp 指令可以放到同一个 entry 当中,一次读出来,然后对 jnz 指令进行分支方向预测:

  • 如果 jnz 预测为跳转,那么当前 fetch block 从 entrypoint 开始,到 jnz 结束;下一个 fetch block 从 targetA 开始
  • 如果 jnz 预测为不跳转,那么当前 fetch block 从 entrypoint 开始,到 jmp 结束;下一个 fetch block 从 targetB 开始

L0BTB holds 4 forward taken branches and 4 backward taken branches, and predicts with zero bubbles.

Zen 1 的第一级 BTB 可以保存 4 条前向分支和 4 条后向分支,预测不会带来流水线气泡,也就是说每个周期都可以预测一次。

L1BTB has 256 entries and creates one bubble if prediction differs from L0BTB.

Zen 1 的第二级 BTB 可以保存 256 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生单个气泡,意味着它的延迟是两个周期。

L2BTB has 4096 entries and creates four bubbles if its prediction differs from L1BTB.

Zen 1 的第三级 BTB 可以保存 4096 个 entry,但不确定这个 entry 是否可以保存两条分支,也不确定这个 entry 数量代表了实际的 entry 数量还是分支数量,后续会做实验证实;预测会产生四个气泡,意味着它的延迟是五个周期。

简单整理一下官方信息,大概有三级 BTB:

  • (4+4)-entry L0 BTB, 1 cycle latency
  • 256-entry L1 BTB, 2 cycle latency
  • 4096-entry L2 BTB, 5 cycle latency

下面结合微架构测试,进一步研究它的内部结构。

微架构测试

在之前的博客里,我们已经测试了各种处理器的 BTB,在这里也是一样的:按照一定的 stride 分布无条件直接分支,构成一个链条,然后测量 CPI。

考虑到 Zen 1 的 BTB 可能出现一个 entry 保存两条分支的情况,并且还对分支的类型有要求,因此下面的测试都会进行四组,分别对应四种分支模式:

  • uncond:所有分支都是无条件分支:uncond, uncond, uncond, uncond, ...
  • cond:所有分支都是条件分支:cond, cond, cond, cond, ...
  • mix (uncond + cond):条件分支和无条件分支轮流出现,但 uncond 在先:uncond, cond, uncond, cond, ...
  • mix (cond + uncond):条件分支和无条件分支轮流出现,但 cond 在先:cond, uncond, cond, uncond, ...

stride=4B

首先是 stride=4B 的情况:

可以看到,图像上出现了三个比较显著的台阶:

  • 所有分支模式下,第一个台阶都是到 4 条分支,CPI=1.25,比 1 周期略高,猜测是因为循环体比较小,循环结束的操作的开销没有平摊造成的;4 对应了 4-entry 的 L0 BTB
  • 所有分支模式下,第二个台阶都是到 256 条分支,CPI=2,对应了 256-entry 的 L1 BTB,意味着 L1 BTB 没有做一个 BTB entry 记录两条分支的优化,实际上就是 256 个 entry 保存 256 条分支
  • 在 uncond 和 cond 模式下,第三个台阶到 2048 条分支,CPI=5,对应 L2 BTB,没有显现出完整的 4096 的大小,意味着 L2 BTB 实际上只有 2048 个 entry,每个 entry 最多保存两条分支,而 uncond 和 cond 模式下,不满足每个 entry 保存两条分支的条件,所以只保存了 2048 条分支
  • 在 mix (uncond + cond) 模式下,第三个台阶一直延伸到了 3072,超出了 2048,意味着出现了两条分支保存在一个 entry 的情况,但并没有体现出完整的 4096 条分支的大小
  • 在 mix (cond + uncond) 模式下,第三个台阶延伸到了 4096,体现出完整的 4096 的 L2 BTB 大小

可以观察到,过了 L2 BTB 容量以后,性能骤降到十多个 cycle,此时还没有超出 L1 ICache 容量,这么长的延迟,即使是在 uncond 模式下,可以在译码的时候发现 uncond 分支并 redirect,也要 16+ 个周期,可见其流水线之长。

stride=8B

接下来观察 stride=8B 的情况:

现象和 stride=4B 基本相同,各级 BTB 显现出来的大小没有变化。

stride=16B

继续观察 stride=16B 的情况:

相比 stride=4B/8B,L0 BTB 的行为没有变化;L1 BTB 的容量减半到了 128,意味着 L1 BTB 采用了组相连,此时有一半的 set 不能被用上。此外,比较特别的是,从 stride=16B 开始,CPI=5 的平台出现了波动,CPI 从 5 变到 4 再变到了 5,猜测此时 L1 BTB 也有一定的比例会介入。L2 BTB 在 mix (uncond + cond) 模式下,拐点从 3072 前移到 2560。

stride=32B

继续观察 stride=32B 的情况:

相比 stride=16B,L0 BTB 的行为没有变化;L1 BTB 的容量进一步减到了 64,符合组相连的预期;L2 BTB 在 mix (uncond + cond) 模式下不再能体现出 3072 的容量,而是 2048:此时在一个 64B cacheline 中只有两条分支,第一条分支是 uncond,第二条分支是 cond,不满足 entry 共享的条件(必须 cond + uncond,不能是 uncond + cond),此时 uncond 和 cond 分别保存在两个 entry 中,每个 entry 只保存一条分支,因此 L2 BTB 只能体现出 2048 的容量。而 mix (cond + uncond) 模式依然满足 entry 共享的条件,所以依然体现出 4096 的容量。

stride=64B

继续观察 stride=64B 的情况:

相比 stride=32B,L0 BTB 的行为没有变化;L1 BTB 的容量进一步减到了 32,符合组相连的预期;L2 BTB 在 mix (cond + uncond) 模式下只能体现出 2048 的容量,此时每个 64B cacheline 都只有一条分支,不满足两条分支共享一个 entry 的条件。

stride=128B

继续观察 stride=128B 的情况:

相比 stride=64B,L0 BTB 的行为没有变化;L1 BTB 的容量进一步减到了 16,符合组相连的预期;L2 BTB 的容量减半到了 1024,意味着 L2 BTB 也是组相连结构。

小结

测试到这里就差不多了,更大的 stride 得到的也是类似的结果,总结一下前面的发现:

  • L0 BTB 是 (4+4)-entry,1 cycle latency,不随着 stride 变化,全相连
  • L1 BTB 是 256-entry,2 cycle latency,容量随着 stride 变化,大概率是 PC[n:3] 这一段被用于 index,使得 stride=16B 开始容量不断减半
  • L2 BTB 是 2048-entry,5 cycle latency,容量随着 stride 变化,大概率是 PC[n:6] 这一段被用于 index,使得 stride=128B 开始容量不断减半;每个 entry 最多保存两条分支,前提是这两条分支在同一个 cacheline 当中,并且第一条是 cond,第二条是 uncond;因此最多保存 4096 个分支

也总结一下前面发现了各种没有解释的遗留问题:

  • stride=4B/8B/16B 且为 mix (uncond + cond) 模式时,L2 BTB 体现出 3072/3072/2560 的容量,而非 4096:解析见后
  • L2 BTB 对应的 CPI=5 的台阶出现比较明显的,在 4-5 之间的波动:暂无解释

接下来尝试解析一下这些遗留问题背后的原理。部分遗留问题,并没有被解释出来,欢迎读者提出猜想。

解析遗留问题

stride=4B/8B/16B 且为 mix (uncond + cond) 模式时,L2 BTB 体现出 3072/3072/2560 的容量,而非 4096

前面测试出来,观察到两个奇怪的容量:3072 和 2560,分别有 3 和 5 的因子。下面通过进一步的实验,观察它的来源。

stride=16B 对应 2560 的 L2 BTB 容量

首先针对这个 2560 的拐点,做了一系列测试,在 stride=16B 的情况下,测试不同的 uncond/cond 分支的组合,下面是 64B cacheline 内四条分支的类型的不同组合(U 代表 Uncond,C 代表 Cond),以及该组合对应的容量:

  • CCCC: 2048(即 cond 模式)
  • CCCU: 2560
  • CCUC: 2560
  • CCUU: 2560
  • CUCC: 2560
  • CUCU: 4096(即 mix (cond + uncond) 模式)
  • CUUC: 2560
  • CUUU: 2560
  • UCCC: 2048
  • UCCU: 2560
  • UCUC: 2560(即 mix (uncond + cond) 模式)
  • UCUU: 2560
  • UUCC: 2048
  • UUCU: 2560
  • UUUC: 2048
  • UUUU: 2048(即 uncond 模式)

可以观察到,如果没有出现连续的 CU(CCCC/UCCC/UUCC/UUUC/UUUU),容量是 2048;如果出现了一组 CU(CCCU/CCUC/CCUU/CUCC/CUUC/CUUU/UCCU/UCUC/UCUU/UUCU),容量是 2560;出现了两组 CU(CUCU),就是 mix (cond + uncond) 模式,容量是 4096。

一种可能的猜想:

  • 如果没有出现连续的 CU,那么每个 branch 占用一个 entry,那么容量就是 2048 个 branch
  • 如果出现了一组 CU,那么一个 64B cacheline 里的 4 个 branch 对应 3 个 entry,那么前 2048 个 branch 对应 1536 个 entry,还剩下 512 个 entry,这些 entry 每个 entry 只放 1 个 branch(讨论见后),所以最后容量是 2048+512=2560 个 branch
  • 如果出现了两组 CU,那么每一组 CU 的两个 branch 对应一个 entry,容量是 4096 个 branch

但是也遗留了一个问题,就是只有一组 CU 的情况下,为啥剩下的 512 个 entry 只放 512 个 branch,而不能放 1024 个 branch,按理说是可能再次出现 cond + uncond 合并?这个问题暂时还没有解释。

由此可以看出,2560 的来源是 4 路组相连,然后其中一路发生了 cond + uncond 的合并,所以最终是 5 个分支保存到 4 路当中,再来一条分支就会放不下。

stride=4B/8B 对应 3072 的 L2 BTB 容量

带着上面的分析,再去观察 stride=4B/8B 时的 3072:3072 有 3 的因子,所以大概率是从 2 路组相连得来,其中一路出现了 cond + uncond 的合并,所以出现了 3 个 branch 占用 2 个 entry 的情况,最后体现出来就是 3072 的 L2 BTB 容量。

似乎到这里,3072 和 2560 分别的 3 和 5 的因子都能解释了,剩下的就是解析具体的组相连的结构。

组相连分析

那么到底是 2 路组相连,还是 4 路组相连呢,另外这个组相连的 set 是怎么构成的呢?

首先回忆一下,在 ARM Neoverse N1 中,连续的 32B 内能放 6 个分支,但是 stride=8B 的时候,一次就会往同一个 set 里增加 4 个分支,于是一个 set 内的分支数从 0 变到 4 再变到 8,拐点出现在 4 个分支,而不是 6 个分支。因此为了达到前面出现的 3072 和 2560 的拐点,新增的分支也得均匀地分到各个 set 当中。

前面根据 L2 BTB 的容量分析到,L2 BTB 的 Index 可能是 PC[n:6],但肯定不是简单的这么取,否则也会出现 ARM Neoverse N1 类似的问题。只能说明 PC[6] 往上有若干个 bit 是单独出现在 L2 BTB 的 Index 当中的,而 PC[5] 以下的 bit,可能以某种哈希函数的形式,参与到 Index 当中。

所以,L2 BTB 可能是以 PC[n:6] 作为 Index 去访问,然后内部有多个 bank,每个 bank 内部是 2 路组相连。bank index 是通过 PC 经过哈希计算得来,使得在 stride=4B/8B 的时候,体现出 2 路组相连,而在 stride=16B 的时候,体现出 4 路组相连。同时,分支还能够均匀地分布到各个 bank 当中,避免了和 ARM Neoverse N1 类似的情况的发生。