普通视图

发现新文章,点击刷新页面。
昨天以前Easton Man's Blog

龙芯3A6000:国产CPU中的一颗明星

作者 Easton Man
2024年3月19日 20:45

预计阅读时间: 37 分钟

本文翻译自 ChipsAndCheese,原文为英文,原文链接:https://chipsandcheese.com/2024/03/13/loongson-3a6000-a-star-among-chinese-cpus/ 原作者:clamchowder

本文中文翻译首发自我的博客 https://blog.eastonman.com/blog/2024/03/loongson-3a6000-a-star-among-chinese-cpus/ ,翻译已取得 ChipsAndCheese 编辑/原作者授权。因英文原文无公开的授权协议,本译文也禁止转载,如需转载请先联系我或 ChipsAndCheese。

龙芯视频的截图,视频位于 https://www.loongson.cn/news/show?id=638

现在,我们将看看龙芯新一代的 3A6000 CPU。3A6000 同样是一个四核的 CPU,时钟频率在 2.5GHz,但使用了新的 LA664 核心。相较于 3A5000 的 LA464 核心,LA664 是一个巨大且雄心勃勃的改进。尽管龙芯保持了相同的总体架构,但 LA664 拥有更宽、更深的流水线和更多的执行单元。让事情看起来更好的是,LA664 支持了 SMT(同时多线程)。如果正确实现的话,SMT 可以在很小的面积开销下增加多线程的性能。然而,要把 SMT 做好并不容易。

3A6000的性能测试

7-Zip 是一个文件压缩程序,它有很高的压缩率,但对CPU的要求也很高。7-Zip 几乎完全使用标量整数指令,因此 SIMD 扩展并不提供什么加速。这里,我们通过压缩一个大型的 ETL 性能追踪文件来对比性能。

3A6000 相比它的前身 3A5000 有 38% 的巨大性能提升。如果考虑 SMT 的话,这个提升还更大。在这个 workload 中每个核心只使用一个线程时,四核 LA664 与四核 Zen1 差不多。因此,LA664 的 IPC 性能非常好,因为它只运行在 2.5GHz 的频率上,但低频阻止了它超越 AMD 更新的产品。

当使用所有线程时,SMT 为 3A6000 提供了 20% 的性能提升。与此同时 AMD 的 SMT 在 Zen1 和 Zen2 上有 40% 以上的收益。SMT 的作用是提供了更多的显式并行性,帮助 CPU 隐藏延迟并更好地填满流水线。一方面,高 SMT 收益表明核心的 SMT 实现是经过精心调整的。另一方面,这又意味着核心在运行单线程时没有很好地隐藏延迟。

与 7-Zip 不同,libx264 视频编码大量使用 SIMD 指令。在 x86 CPU 上,编码器将使用 SSE、AVX、AVX2 这些扩展来加速,甚至是使用 AVX-512。在龙芯的 CPU 上,libx264 会使用 LSX 和 LASX 这两种 SIMD 扩展。在这里,我转码一个4K的《守望先锋》游戏剪辑来对比性能。

3A6000 和 Zen1 性能大概差不多,在使用所有线程和每个核心只运行一个线程的情况下互有胜负。Zen1 较高的 SMT 收益可能受到其弱的 AVX2 实现的限制。Zen2 和 3A6000 都有 30% 以上的 SMT 收益。因为架构经过了全面改进和有坚实的 AVX-512 实现,AMD 最新的 Zen4 架构在这些测试中遥遥领先。和之前一样,对于一个 2.5GHz 的 CPU 来说,3A6000 的表现还是非常令人钦佩的。

LA664 核心将龙芯从低性能的区域带入了能够与 AMD 和 Intel 的旧型号竞争的行列。Zen1 也和 Haswell 性能差不多,而这两种架构即使在今天也是仍能一战的。接下来让我们来看看是什么样的架构能让龙芯与那些高时钟频率的设计竞争。

核心架构

LA664 是一个 6 发射的乱序核心,拥有丰富的核内资源和较大的乱序窗口。它在这些方面已经可以与较新的 Intel 和 AMD 核心相媲美。

LA664 在 LA464 的基础上进行了改进。龙芯 3A5000 中的 LA464 核心是一个四发射设计,但在所有方面都相对保守。LA464 总体上是一个经得起考验的核心,没有明显的弱点,并为龙芯在 LA664 的改进提供了坚实的基础。毫不意外地,LA664 继承了 LA464 的总体架构。

当然,架构图不能覆盖全部的细节。类似分支预测、执行单元延迟和访存性能等因素可能会对性能产生巨大的影响。

龙芯3A6000的前端

分支预测

CPU 分支预测器的责任是指导前端取指,告诉 CPU 分支将会走向哪个方向。这非常重要,因为如果分支预测器预测错误,CPU 的后端将会在错误路径上浪费大量的性能、功耗和时间。

3A6000 的分支预测器拥有令人印象深刻的模式识别能力,在我们迄今为止看到的中国 CPU 中显然是最优秀的。它与我们在 3A5000 中所见到的大相径庭,并且几乎能够与最新的 Intel 和 AMD 的 CPU 相媲美。但是 AMD 一直在分支预测器能力上投入了大量精力,这使他们的 Zen3 架构仍然领先一些。

Zen3 通过使用覆盖重定向的预测器来实现这一点:大多数分支行为简单,可被一级预测器迅速处理;仅对具有长期历史依赖的分支,能力更强但速度较慢的二级预测器才需要介入。这种方案使 Zen3 的分支预测器能在不牺牲预测速度的前提下跟踪非常长的分支历史。

即使 3A6000 无法与 AMD 最新的核心匹敌,龙芯在这一领域取得的进步仍值得赞扬。3A5000 的预测器看起来更适合 2000 年代中期到 2010 年代初的高性能核心,而不是近十年的产品。龙芯在分支预测器的改进无疑是 3A6000 改善性能的一大因素。

分支预测器速度

分支预测器要快速且准确,以避免出现供指问题。分支目标缓冲(BTB)缓存分支的目标,让预测器在实际分支指令被从 ICache 中取出并译码之前就能提供一个推测的指令流。LA664 拥有 64 项的 L1 BTB,能够连续(或者说Zero-Bubble地)处理 taken 的分支。BTB 缺失的情况很可能简单地等待 64KB 的 L1i 中的指令被取出和译码,然后才计算分支目标。这在实际效果上相当于 L1i 充当一个 1K-4K 项的 L2 BTB。

相比之下,AMD 和 Intel 最近的架构都使用解耦的大型 L2 甚至 L3 BTB。只要分支 footprint 能被 BTB 覆盖,从下级 BTB 获取地址可以比从 ICache 获取更快,AMD 的 Zen4 就利用这一点来做到非常低延迟的分支处理。

此外,从指令缓存解耦 BTB 有助于维护高 IPC,因为预测器得以免受 L1i 缺失延迟的影响。但龙芯决定放弃一个大的、解耦的 BTB 并非独一无二。Tachyum Prodigy 也做出了同样的决定,因为他们发现用标准库达到他们的目标频率将会太贵。如同 Tachyum 的 Prodigy,3A6000 通过使用一个大的 64KB 指令缓存来补偿这一点。如果 L1i 缺失变少,那这种做法弱点就不那么明显了。

3A6000 似乎还有更加积极的 nextline 指令预取。上面的测试中,分支仅仅跳到到下一个 16B 对齐的块,因此 nextline 指令预取器可以很好地工作。相比之下,AMD 的预取完全由分支预测器驱动。一旦超过了 BTB 容量,就无法隐藏 L2 延迟。

间接分支预测

与直接分支相比,间接分支更难预测。间接分支并不是直接编码跳转目标,而是跳转到寄存器中的地址。3A6000 在间接分支预测方面做得非常好。

3A6000 可以跟踪总共 1024 个间接跳转目标,是 3A5000 的两倍。3A5000 只能跟踪大约 24 个间接分支,而 3A6000 可以轻易跟踪超过 128 个间接分支。相比之下,Zen2 也可以跟踪 1024 个间接跳转目标,因此 3A6000 在这一点上与较新的 x86 CPU 非常接近。

返回地址预测

返回指令作为一种特殊类型的间接分支,经常是以 call-return pair 的形式出现。许多处理器倾向于使用一个专用的返回地址栈。当分支预测器遇到一个调用指令时,它会将一个地址压入这个栈中。当它遇到一个返回指令时,它会从栈中弹出一个地址。有趣的是,3A6000 将返回地址栈的容量从 3A5000 的 32 项减少到了仅仅 16 项。如果核心的两个 SMT 线程都在运行,有效容量甚至可能进一步降低到 8 项。

将返回栈容量降低到 16 项在某种程度上是有道理的,因为即使是一个较小的返回栈也能覆盖到大多数调用/返回情况。龙芯可能是在返回地址栈溢出示回落到间接分支预测器,因为溢出时 3A6000 只会遭受轻微的性能损失。Intel 采用同样的这种策略。在这种情况下,返回栈地址可以被视为一个功耗和性能优化的措施,而不是性能关键路径上的一部分。

分支预测准确率

7-Zip 和其他压缩程序通常对 CPU 的分支预测器构成巨大的挑战。3A6000 的分支预测器表现出色,与 Zen1 在 MPKI 上持平。

在准确率方面,3A6000 与 Zen2 不相上下。在跨 ISA 对比时,准确率是一个更好的衡量标准,因为 3A6000 能够以更少的执行指令完成工作负载。MPKI 较高可能是因为龙芯的指令流中分支的比例更大。AMD 最新的 Zen4 架构仍然领先,但龙芯在分支预测器方面取得了值得赞扬的进步,它远远优于3A5000。

libx264 拥有较少并且更好预测的分支,但我们仍然可以看到测试 CPU 之间的差异。在这个测试中,ISA 差异对龙芯不利。3A6000 的 MPKI 较低,但这仅是因为它完成工作负载执行了更多的指令。当有更多的非分支指令要处理时,误预测的影响就变得不那么重要了。

3A6000 的分支预测器表现可以与 AMD 的 Zen2 相媲美。龙芯再次显示出他们在设计分支预测器能力上的竞争力。

指令获取

一旦分支预测器决定了指令流,就轮到指令缓存来为核心提供指令。与它的前辈一样,3A6000 拥有一个大型的 64KB 4 路 L1i。Intel 和 AMD 的 CPU 只有 32KB 的 L1i,相比来说 3A6000 有一个相当大的L1i。64KB 的 L1i 向一个 6 宽的译码器供指,这使 3A6000 有比它的前任宽 50% 的前端。

AMD 和Intel 的高性能 CPU 使用 uOp Cache,这可以让它们在提供更高吞吐量的同时避免指令译码开销。自 Zen 架构以来,AMD 的 uOp Cache 理论上可以在每周期提供一整行的 8 个 uOp,但是下游的重命名宽度限制了核心的吞吐量。

当代码溢出 L1i 时,龙芯 3A6000 保持了良好的吞吐量。3A5000 在运行 L2 中的代码时出现了不明原因的指令带宽不足。龙芯在 3A6000 中解决了这个问题,它可以在代码被 L2 覆盖时维持每周期 3 条指令的吞吐。

不幸的是,在指令 footprint 增大到 L3 以后,情况并没有那么乐观。看起来龙芯在 3A6000 上并没有改进他们从 L3 获取指令的能力。这是有些遗憾,因为 Golden Cove 和 Zen3 都可以每周期获取超过三个 4 字节指令,而龙芯的 CPU 连每周期两个都不行。

重命名和分配

指令被解码成 uOp 后,CPU 需要分配后端资源。这些资源追踪指令状态,允许 CPU 尽快执行指令,同时确保正确的程序行为和异常处理。这个阶段还通过寄存器重命名机制打破假依赖,并在执行引擎中实现了其他技巧,以暴露更多的指令级并行性。

LA664 的重命名阶段有 move elimination,对寄存器置零有特殊处理,类似于 x86 CPU 识别原地 XOR 的行为。重命名完全消除了这样的操作,意味着它们不会消耗更下游的执行资源。

测试项目3A6000 IPCZen 4 IPC
有依赖的move2.005.71
无依赖的move3.645.73
寄存器置零5.35 (li.d)5.73 (XOR)

龙芯针对寄存器 move 进行了一些优化。有依赖的 move r,r 指令可以每周期执行两条,所以 LA664 有时可以在重命名表中通过指针操作以打破这种依赖。然而,它不能以全速执行move,寄存器 move 仍然需要通过 ALU。

Intel 和 AMD 都有更激进的消除,能够全速消除寄存器 move,无论是否存在依赖。Intel 的 Golden Cove 还可以在重命名阶段消除带小立即数的加法,进一步减轻执行阶段流水的负担。

乱序执行

为了实现乱序执行,重命名和分配阶段需要在必要的队列和缓冲区中找到空位才可以继续。更大的结构允许核心有更大的乱序执行窗口,使其更能隐藏延迟并利用指令级并行性。龙芯 3A6000 拥有非常大型的乱序引擎,这是在 3A5000 基础上的巨大进步。

结构何时需要占用LA664 (龙芯 3A6000)LA464(龙芯 3A5000)Zen3Golden Cove
Reorder Buffer256 项128 项256 项512 项
整数寄存器堆目的寄存器为整数寄存器192 项128 项192 项280 项
向量/浮点寄存器堆目的寄存器为向量/浮点寄存器256bit 192 项 ,6KB 总容量256bit 128 项 ,4KB 总容量256bit 160 项 ,5KB 总容量512bit 228 项 + 256bit 104 项,16.875KB 总容量
调度队列等待执行48 项整数, 48 项浮点, 48 项访存32 项整数, 32 项浮点, 32 项访存4×24 项整数 ( 其中3个与AGU共享)
2×32 项浮点+ 64 项顺序队列
97 项运算,70 项Load,38 项Store
Load Queue读内存80 项64 项116 项*192 项
Store Queue写内存64 项44 项64 项114 项
分支缓冲控制流指令64 项26 项48 项 Taken, 117 项 Not Taken128 项
*Zen优化手册显示,Load队列有72项,但核心可以同时处理116个Load。为了与其他架构保持一致,使用了实测的116这一数据。

与 3A5000 相比,诸如寄存器堆和 LSQ 等主要结构至少增加了 25% 的大小。LA464 的分支缓冲看起来太小了,而 LA664 解决了这个问题。LA664 最终拥有与 AMD 的 Zen3 相当的乱序能力。但 Zen3 和 LA664 与 Intel 的 Golden Cove 相比起来仍然小,Golden Cove 有着巨大的 512 项 ROB 和其他更大的结构。

来自龙芯视频的另一个截图,右侧展示了微结构参数。它们与微基准测试结果一致。

SMT实现

更大的乱序缓冲区对于提高单线程性能至关重要,但会遇到收益边际效应。SMT 通过向操作系统暴露多个逻辑线程,并在这些线程之间分配资源来对抗这种收益递减。因此,一个启用了 SMT 的 CPU 可以保持单线程的高性能,与此同时如果有多个线程活跃,它就像几个较小的核心一样工作。AMD、Intel 和龙芯都决定为每个核心设计两个 SMT 线程。

虽然 SMT 的好处很明显,但实现难度很大。CPU 必须能够动态地在多个线程之间切换。工程师必须决定在两个线程的模式下如何管理各种核心结构。具体来说这种情况下,一个结构的处理方式可以是:

  • 复制。每个线程获得一份完整的资源。在单线程模式下,第二份资源不使用。这不是一个面积高效的方式,但可能更容易 tuning 和验证。不需要担心线程饥饿,如果第二个线程变得活跃,也不需要清空部分资源。
  • 静态分区。每个线程获得一半的资源。当一个线程处于停止状态时,另一个线程可以使用所有资源,所以这种方法是面积上更高效的。它更难验证,因为当两个线程都工作时,资源需要清空一半并重新配置。但由于将结构切分为两半确保了线程之间某种程度的公平性,所以 tuning 仍然不是太难。
  • 使用 water-mark。在 2T 模式下,一个线程可以占用资源直至 high water-mark。这种做法更灵活、有更多潜在的性能,但 tuning 更难。较高的 water-mark 可能会提高一个线程的性能,但对另一个(饥饿的)线程有严重的影响。
  • 竞争性共享。这就是自由竞争:即使另一个线程活跃,一个线程也可以使用所有的资源。这种情况灵活性和潜在的性能都可以达到最大。例如,如果一个线程运行浮点代码,另一个运行纯整数代码,竞争性共享调度器将允许两个线程用满他们最需要的资源。但 tuning 和验证变得更难。线程饿死可能会更加常见,工程师必须小心避免这种情况。

龙芯选择了一个保守的 SMT 实现,其中大多数资源都是静态分区的,包括 ROB、寄存器堆和 LSQ。

3A6000 的调度器是用 water-mark 的。运算单元调度器的高水位标记似乎在 30 项左右,所以在 3A6000 上运行的线程即使在其 SMT 兄弟线程活跃时也几乎可以使用与在 3A5000 上一样多的调度器大小。

结构3A6000Zen 2
Reorder Buffer静态分区
每线程约 122 项
静态分区
整数寄存器堆静态分区
每线程约 69 项
竞争共享(可通过 QoS 等调整)
向量/浮点寄存器堆静态分区
每线程约 64 项
竞争共享(可通过 QoS 等调整)
Load Queue静态分区
每线程约 38 项
竞争共享
Store Queue静态分区
每线程约 30 项
静态分区
整数调度队列Watermarked
每线程最大 30 项
竞争共享
访存调度队列Watermarked
每线程最大 36 项
竞争共享
浮点调度队列Watermarked
每线程最大 30 项
Watermarked
每线程最多 64 项 SQ + NSQ (总共 100 项)

AMD Zen2 的重排序缓冲使用静态分区,并对浮点调度队列和非调度队列采用某种水位标记方案。但在其他方面,AMD 采取了非常激进的 SMT 策略。寄存器堆、LSQ 和整数调度队列是竞争性共享的。这可以部分解释Zen2 令人印象深刻的 SMT 收益。

对于龙芯来说,采取一个不那么激进和更容易验证的方法是合理的。3A6000 是他们首个支持 SMT 的 CPU,过于雄心勃勃总是容易导致失败。

整数执行

3A6000 的整数执行单元看起来相比前代变化最少,但调度器容量增加 50% 应该会有更好的 ALU 端口利用率。与 3A5000 一样,3A6000 有四个端口的 ALU 能够执行常见操作。两个端口可以处理分支,两个用于整数乘法。这个配置与 Zen2 大致相似,但龙芯有两个整数乘法单元,而 Zen2 只有一个。Zen2 有更大的总调度容量,但 Zen2 是分布式调度器,与龙芯的统一调度器并不能直接比较。Zen2 上可能会出现其中一个 16 项的队列提前填满,这在重命名阶段将会由于资源不足发生阻塞。

龙芯在整数除法的吞吐量和延迟上进行了改进,从 3A5000 的每周期 0.11 个指令和 9 个周期的延迟提高到每周期 0.25 个指令和 4 个周期的延迟。除法器的改进是一个奇怪的选择,因为大多数代码会避免使用 DIV 指令,因为它在历史上就非常慢。也许是因为龙芯需要从头开始构建他们的软件生态系统,加速除法操作可能是一个值得的投资。

向量和浮点执行

龙芯的 3A5000 拥有 256 位的向量能力和 LASX 扩展,但实现较为保守,只有两个 256 位执行单元。3A6000 彻底改进了FPU,它现在有四个执行单元。所有四个管道都可以处理 256 位向量加法,这给 3A6000 提供了非常强大的浮点性能。竞品的 x86 CPU 只能每个周期执行两个 256 位向量加法。256 位向量乘法和基础向量整数操作性能与 Zen2 相似。

奇怪的是,标量浮点操作没有得到同样的增强。只有两个单元可以处理标量浮点加法。更奇怪的是,标量浮点乘法似乎和向量乘法使用不同的流水线。

即使龙芯增加了额外的浮点单元,融合乘加(FMA)的峰值吞吐量还是保持不变。LA464 和 LA664 都可以每个周期执行一个 FMA 操作,它们的 FMA 吞吐量是 AMD 的 Zen2 或 Intel 的 Skylake 的一半。

指令LA664LA464Zen3Golden Cove
256bit FP64 向量加法吞吐量4 per cycle2 per cycle2 per cycle2 per cycle
256bit FP64 向量加法延迟3 cycle latency5 cycle latency3 cycle latency3 cycle latency
256bit FP64 向量乘法吞吐量2 per cycle2 per cycle2 per cycle2 per cycle
256bit FP64 向量乘法延迟5 cycle latency5 cycle latency3 cycle latency4 cycle latency
256bit FP64 向量 FMA 吞吐量1 per cycle1 per cycle2 per cycle2 per cycle
256bit FP64 向量 FMA 延迟5 cycle latency5 cycle latency4 cycle latency4 cycle latency

除了吞吐量外,龙芯改善了执行延迟。浮点加法的延迟为 3 个周期,与 Zen3 相等。然而,AMD 的 Zen3 和 Intel 的 Golden Cove 仍然有更低的浮点执行延迟。特别是 Intel,它可以在 2 个周期延迟内完成浮点加法,而且还是以更高的时钟频率运行。

和整数调度队列一样,浮点调度队列容量增加了 50% 至 48 项。单独就这一点来说可能就已经提供了比增加执行单元更多的浮点性能提升,两者结合使 3A6000 在向量和浮点工作负载中表现强大。

AMD 的 Zen2 也有一个四流水线 FPU,并且也在所有四个单元处理基础向量整数操作。然而,它使用一个聪明的非调度队列,即使它的 36 项调度队列填满,也能让后端保持更多的inflight浮点或向量操作,尽管它不能利用这些指令来找到额外的指令级并行性。

地址生成

龙芯在 LA664 中相比于 LA464 显著强化了地址生成功能。LA464 拥有两个通用的 Load/Store 单元。LA664 将这分为两个 Load 和两个 Store 单元。

这意味着,LA664 每周期可以处理比 Zen2 更多的标量内存操作,与 Golden Cove 处理的内存操作量相同。Zen3 和 Golden Cove 在灵活性方面稍微领先于 LA664,可以每周期发出 3 个 Load。但 LA664 的 2 Load 每周期已经是从 LA464 的 1 Load 每周期的大幅升级。在 Store 方面,LA664、Zen3 和 Golden Cove 都是每周期 2 次标量 Store,而 LA464 只能每周期处理一个 Store。

LA664LA464Zen3Golden Cove
每周期标量 Load2133
每周期标量 Store2122
AGU 数量(标量内存操作发射宽度)4234

LA664 可以每周期处理两个 256 位向量访存,这些访存可以是任意 Load/Store 组合。

内存序

一旦地址计算完毕,Load/Store 单元必须确保内存访问符合 ISA 的内存模型。Load 可能需要从之前的 Store 中获取结果。与之前的 3A5000 一样,3A6000 可以在大约 7 个周期的延迟中处理 Load 被包含于之前的 Store 的情况。只有部分是重叠的情况会产生 14 个周期的惩罚,可能是因为 Load 被阻塞直到 Store 退休并写回到 L1d。

Zen2 具有相同的 7 周期转发延迟和 14 周期的部分重叠惩罚。但 Zen2 可以以一个额外周期的惩罚处理 64B 缓存行间转发,而龙芯在这种情况下表现不佳。由于 Zen2 的时钟频率更高,所以龙芯 7 周期的转发延迟感觉有些长。Goldmont Plus 的目标频率也是 2.xGHz 接近 3GHz 的范围,它就是 5 周期的 Store-Load 转发延迟以及转发不能处理时的 10 周期延迟。

使用 Henry Wong 的方法测量的 Store-Load 转发延迟,代码由 Clam 编写。

LA664 的 Store-Load 转发行为看起来与其前代相似。但 LA664 消除了非对齐 Store 的 10 周期惩罚,将其降低到仅 3 周期。这比 Zen2 稍好,Zen2 对于非对齐的 Store 需要 2 到 5 个周期的处理时间。

跨越 64B 边界的 Store-Load 转发仍然处理不佳,但惩罚从 31 周期降低到了可以接受的 21 周期。

缓存和内存访问

良好的缓存和内存层次结构对于任何现代高性能 CPU 的数据供给至关重要。3A6000 保留了类似的缓存层次结构,但在各个点上有细微的改进。

由于龙芯未能提高时钟速度,他们通过减少缓存访问路径中的流水线长度来降低延迟。

延迟

L1d 延迟从四个周期降低到三个周期。我认为低频 CPU 的设计目标应该是 3 周期 L1d 延迟,很高兴看到3A6000 实现了这一点。

许多现代 CPU 使用 L2 作为中级缓存,使 L1 缺失不需要到相对高延迟的 L3 中查询。3A6000 继续使用256KB L2 缓存,类似于较早的 Intel 架构。最近的 AMD 和 Intel CPU 趋向于使用更大的 L2 缓存。Zen4 已经转向使用 1MB L2 缓存,而 Intel 的 Raptor Lake 选择了巨大的 2MB L2。尽管龙芯未能实现更大的 L2,但他们将延迟从 14 个周期降低到 12 个周期。假设 L1 以同样的速度处理命中和缺失的话,L1 到 L2 的路径上可能只减了一拍。

内存层级3A60003A5000Zen 2 (3950X)
L1d64 KB
3 周期延迟
64 KB
4 周期延迟
32 KB
4 周期延迟
L2256 KB
12 周期延迟
256 KB
14 周期延迟
512 KB
12 周期延迟
L316 MB
38 周期延迟
16 MB
40 周期延迟
16 MB
37 周期延迟
DRAM104.19 ns
~263 周期延迟
144.52 ns
~361 周期延迟
77.25 ns
~341 周期延迟

Zen2、3A6000 和 3A5000 都有一个四核共享的大型 16MB L3 缓存。3A6000 减少了几个周期的 L3 延迟,尽管这可能是因为查询 L2 加快了两个周期。

最后,DRAM 延迟从 144ns 改进到了 104ns。3A5000 的 DDR4 控制器很糟糕。它只是恰好被较低的时钟频率“拯救”了,使得它对 IPC 的影响没那么大。3A6000 获得了大大改善后的内存控制器。104ns 还是不够好,但以周期数计的话,它的延迟降低到了比高时钟频率的 3950X 要低。因此 3A6000 至少通过减少 DRAM 访问延迟的周期数来缓解了它的低时钟频率劣势。然而,104ns 对于一个单片 DDR4-2666 的配置来说并不是很好。

尽管 3A6000 在延迟的周期数上具有竞争力,我们必须考虑实际的延迟,因为龙芯无法将其 CPU 的时钟频率提升到现代 AMD 和 Intel CPU 运行的水平。这并不是对 LA664 有利的情况。在包括 L1 在内的每一缓存它都较慢。这解释了为什么 LA664 在拥有更多乱序能力和更高 IPC 的情况下,仍然稳定地输给 Zen2。

带宽

带宽对于向量化、多线程应用程序尤其重要。3A6000 在很大程度上继承了它前身的内存层次结构,但龙芯再次在各个地方进行了改进。

3A5000 已经拥有与 Intel 的 Skylake 或 AMD 的 Zen2 相似的每周期 L1d 带宽。3A6000 通过加倍写入带宽进行了改善。基本上 3A6000 的 L1d 可以每周期完成两次 256 位访问,无论是读取还是写入。

因此,尽管时钟频率较低,3A6000 拥有令人印象深刻的 L1d 写入带宽。LA664 因此成为了和 Golden Cove 一样唯二的两个具有每周期 512 字节存储带宽的消费级 CPU。

LA664 的 256KB L2 表现与其前身基本类似,每周期读带宽为 21-22 字节,写带宽相同。因此,3A6000 的 L2 带宽仍然低于 AMD 或 Intel 最近的任何 CPU。与 Intel 的 CPU 相比差距特别大,后者有每周期 64字节的 L2 带宽。

在 L3 中,LA664 比其前代增加了 33% 的带宽。每周期从 L3 获取 18.7 字节的带宽让龙芯可以对抗较老的 Intel CPU,但 AMD 特别强大的 L3 仍然遥遥领先。

设计共享缓存引入了额外的挑战,因为缓存带宽在服务多个客户时还要增加。当所有硬件线程都活跃时,3A6000 可以给每个核心每周期提供 16.55 字节。这比单个核心在没有其他核心竞争的情况下只少了一点点,是非常好的表现。旧的 3A5000 在所有四个核心加载时只能为每个核心每周期提供 10 字节多一点,与单一核心的 13.7 字节相比。这表明 3A5000 的 L3 结构存在竞争,而 3A6000 在很大程度上解决了这个问题。AMD 在多个核心一起冲击缓存时再次提供了出色的每核心 L3 带宽,每核心每周期超过 24 字节。

龙芯的 3A5000 有一个极其糟糕的 DDR4 控制器。幸运的是,3A6000 有一个更好的控制器。技术上它支持 DDR4-3200,但我们在使用这个速度运行双通道内存时无法稳定启动。当装配双通道 DDR4-2666 时,3A6000 实现的 DRAM 读带宽大致与 Core i5-6600K 相当。这个酷睿芯片的第一代 DDR4 控制器只能在完全稳定的情况下处理 DDR4-2133 的速度(至少在我的样本上是这样),但仍然比 3A6000 用更快的内存实现更好的 DRAM 读带宽。

尽管 3A6000 的 DRAM 读取性能一般,但它也有一个小 trick:当检测到缓存行被完全覆写而且没有对其先前内容的依赖时,可能会避免获取这个缓存行的所有权。这可能有助于某些访问模式,但好处可能会受限,因为程序通常有更多的读取而不是写入操作。

核心到核心延迟

这个测试使用原子比较和交换操作在核心之间传递值。

龙芯的性能没有带来任何惊喜(惊吓),这是一件好事。

结语

龙芯的工程师有许多值得骄傲的地方。设计与 Zen2 相当的分支预测器并不容易,考虑到龙芯在 3A5000 中的表现,这一成就更显卓越。同样地,SMT 非常难以实现正确。龙芯在大幅扩展 3A5000 的乱序引擎和修复其 DDR4 控制器的同时,设法完成了这两件事。这些巨大的性能提升使 LA664 与 Zen1 在单核性能上相当。

3A6000 是中国为使其经济减少对外国 CPU 依赖所做努力的一部分。在这方面,3A6000 是向前迈出的一步。Zen1 在今天仍然非常可用,所以中国消费者可能会发现 3A6000 的性能对于轻量级日常任务已经是可以接受的了。龙芯的软件生态系统比性能更影响芯片的可用性。

但龙芯还有一个成为世界级 CPU 制造商的次级目标,与西方公司如 Intel 和 AMD 并肩。在这方面,他们还有很长的路要走。Zen1 级别的单线程性能值得赞扬。但我们必须记住,Zen1 之所以能够从 Intel 那里夺取市场份额,是因为它将便宜的的 6 核和 8 核产品带入了消费者平台,而不是因为它能够以核心对核心的方式赢得 Skylake。3A6000 只是一个四核产品,因此缺少了 Zen1 最大的优势。

来自 https://mp.weixin.qq.com/s/dMXDPrsCS4rOj7AOPyYOcg

龙芯还将 3A6000 与 Intel 的 Core i3-10100 相比较——一个带有 6MB 缓存和 4.3 GHz 睿频时钟的四核 Skylake 产品。虽然从名义上看是第 10 代产品,i3-10100 更类似于 2015 年的 Core i7-6700K。Intel 的第 10 代更为人所知的是将 10 核 CPU 带入消费者阵容,而 6 核和 8 核构成中端产品。除了更多核心,像 i5-10600K 和 i7-10700K 这样的部件还享有更高的睿频频率。3A6000 将无法与这些产品竞争。针对同一时代的 Zen2 产品,龙芯也将遇到挑战。我们已经看到,3950X 在相同核心数量的测试中轻松超过 3A6000。当 Zen2 的更多核心开始发挥作用时,这一差距将只会变得更大。

今天,Intel 的 Golden Cove 衍生产品和 AMD 的 Zen4 在 3A6000 之上还有更大的领先优势。四核产品在 AMD 和 Intel 的消费者产品阵容中已经几乎消失。龙芯的 3A6000 可能是我们从中国看到的最有前途的 CPU,远比围绕 A72 进行的笨拙尝试更加令人兴奋。但龙芯的工程师们仍有大量工作要做。我们期待看到他们接下来能够实现什么。

本文翻译自 ChipsAndCheese,原文为英文,原文链接:https://chipsandcheese.com/2024/03/13/loongson-3a6000-a-star-among-chinese-cpus/ 原作者:clamchowder

本文中文翻译首发自我的博客 https://blog.eastonman.com/blog/2024/03/loongson-3a6000-a-star-among-chinese-cpus/ ,翻译已取得 ChipsAndCheese 编辑/原作者授权。因英文原文无公开的授权协议,本译文也禁止转载,如需转载请先联系我或 ChipsAndCheese。

The post 龙芯3A6000:国产CPU中的一颗明星 first appeared on Easton Man's Blog.

现代分支预测:从学术界到工业界

作者 Easton Man
2023年12月31日 20:51

预计阅读时间: 24 分钟

Branch Prediction Is Not A Solved Problem

–Intel

我从 2022 年底开始在香山开源高性能处理器团队里实习,到现在已满一年了,一直主要负责前端取指模块中分支预测单元的改进。到了年底了,略有一点空闲时间来写一下目前为止在分支预测方面的见闻。

本文假定读者对数字电路设计和高性能 CPU 设计都有一定了解,知道什么是“分支预测”以及“分支预测”对于高性能 CPU 来说的重要意义,如果你完全不了解这部分内容,但又感兴趣的话,建议先去看一些相关的基础书籍和文章。

另外本文基本上仅探讨条件分支的预测,也即只进行 T/NT 的方向预测,其他类型的分支如果大家感兴趣我以后再写。

希望本文能被一些刚参加完龙芯杯,或者通过一生一芯学习的同学阅读。如果想要尝试做分支预测器方面的工作,或者到香山前端团队来实习,那么在雄心壮志开始准备大战预测算法提高准确率之前,请先停下来了解一下真正的高性能处理器设计对分支预测的需求。

分支预测发展历史

最古老的分支预测方法就是饱和计数器,这里假设读者已经充分了解什么是饱和计数器和用饱和计数器来预测分支的方法了。

饱和计数器只能作出偏向性的预测,并不能充分利用单个分支内部和分支之间的关联信息,因此很快就发展出了能够利用分支历史信息的预测器,例如 GShare 预测器:

GShare预测器示意图
来自 An Analysis of Correlation and Predictability:
What Makes Two-Level Branch Predictors Work

GShare 预测器将分支的 PC 与一个全局历史寄存器 XOR 以后作为 index 来选择 PHT 中的一个饱和计数器,然后使用这个计数器给出预测方向,更新时同样也通过同样的方式索引到同一个饱和计数器并更新。

这种方式将不同全局分支历史下的同一条分支的预测分给了不同的饱和计数器,使得 GShare 预测器能够区分不同的历史。这样做带来的第一个好处是能够正确预测循环退出了。

另一个好处是,分支之间的相关性可以得到有效利用,下图是常见的分支间相关性的例子

分支间相关的例子,来源同上面的图

以上的两个例子恰好对应局部分支历史和全局分支历史。循环退出的预测只需要单个 PC 的历史就可以解决,而分支间相关的例子则必须要全局的分支历史。可见不同的分支需要的历史是不同的,由此发展出了锦标赛预测器,由局部历史和全局历史的两个预测器分别独立预测,然后再由一个饱和计数器构成的选择结构选择使用哪个子预测器提供的方向预测。著名的 Alpha 21264 处理器就是使用了锦标赛预测器。

Alpha 21264 的分支预测器,来自其论文

Alpha 21264 是经典的超标量乱序处理器,它的论文值得大家一看 The Alpha 21264 microprocessor

现代分支预测算法

Branch prediction research is basically about separating easier and more difficult branches, and using a simple predictor for the easy branches and a complex predictor for the difficult ones.

— Onur Mutlu (Computer Architecture and Digital Design, ETHZ)

现代的分支预测器算法只有一种——TAGE,几乎所有的学术研究和商业高性能处理器都使用 TAGE 或者 TAGE 的变种。TAGE 预测器是最早在 2006 年由 Andre Seznec 提出的,提出当时即获得当届分支预测锦标赛冠军。然后接下来直到 2016 年的所有分支预测锦标赛都由TAGE预测器的变种获得。

我在 2022 年参加龙芯杯比赛复现 TAGE 预测器的时候,对 Mutlu 教授在课程中的这句话感受颇深。分支本身的预测难度不尽相同, 如果使用复杂的分支预测器预测简单的分支,那么大概率会浪费存储空间。TAGE 预测器的设计和这一个思想不谋而合,它使用多个不同历史长度的子预测器,并且使用一套机制自动地在这些子预测器中分配表项。这样能够做到使用短历史预测简单分支,长历史预测复杂分支,同时也能够避免存储空间的浪费。

以下是 TAGE 预测器的框图

对预测机制细节感兴趣的读者可以参考原论文 A case for (partially) TAgged GEometric history length branch prediction

解耦合前端与覆盖重定向

像 TAG E这种复杂程度的预测器是没有可能当拍就能得到预测结果的,本文撰写的时期(2023年)附近的半导体工艺对于 SRAM 微缩已经遇到了困难,目前 SRAM 访问就需要接近一个时钟周期或者一个完整的时钟周期了。另外考虑到每个 PC 都访问复杂预测器在功耗上的开销,商业处理器也很少做非常激进的省拍策略(如提前流水等)。

故 TAGE 预测器的返回结果到下一次的预测之间就出现了空拍,这是难以接受的,对性能的影响非常大。所以很自然的就出现多级预测器——覆盖重定向这种策略。这种设计一般使用简单的预测器进行 zero-bubble 的预测,然后再使用复杂预测器对简单预测器的结果进行验证,如果预测不一致,就会发出一个前端重定向,重新开始一个预测流水。这样的做法可以在简单预测器准确率较高的情况下,省去复杂预测器的访问延迟。

下图是一个 SiFive P870 的设计,可以明显得看出具有 Cascading BP 的的设计。

覆盖重定向的设计带来了新的问题——预测流水和取指流水,也即 ICache 流水,的速度不匹配。预测流水出现前端重定向的时候,ICache 可能仍能取指,ICache 出现缺失的时候,BP 就处于阻塞状态。为了避免相互影响,现代高性能处理器普遍采用了解耦合前端(或称为分离式前端),只有少量的处理器设计采用传统的设计。

下图是高通在 HPCA 2019 上发表的论文 Elastic Instruction Fetching 中的配图

学术与产业落地

我曾经一度认为商业公司秘密发展了新的分支预测算法,而且不予公开,并且感到十分不高兴。

年轻的我.jpg

后来我很快认识到工业界中使用的分支预测算法非常的保守,甚至大部分的商业处理器都只使用了 TAGE 预测器或者 TAGE-L 等价的预测器。

香山处理器的分支预测器也从雁栖湖的 TAGE-SC-L 变成了南湖和昆明湖的 TAGE-SC,然后甚至正在考虑去掉 SC。

这样的情况可能和很多人的认识相悖,尤其是刚参加完龙芯杯或者一生一芯学习的同学。也和一些刚毕业的研究生或者长时间从事学术研究的人的认识有冲突。分支预测准确率对于高性能处理器的影响是非常巨大的,为什么这些在相同存储下能够取得更好预测准确率的算法没有被工业界采用?

接下来我们就讨论一下工业界对于分支预测算法更关注什么?

工业界关注什么?

首先,工业界和学术界的区别在哪里?学术界相信大家都很好理解,只要一个算法在相同的存储空间下能够取得更好的准确率,就可以认为这是一个更好的算法。但是在商业高性能处理器的设计中,还必须要考虑实际的延迟,以及性能、功耗、面积的平衡。

更新延迟

首先是一个非常明显的问题,部分学术界的研究也已经涉及到了,那就是在真实处理器中,分支预测器的更新只能在后端执行完毕以后才能拿到准确的分支执行情况。现代的预测算法都依赖分支的历史,如果历史不准确,将会极大地影响预测的准确率。然而下一条分支的预测开始时,上一条分支还远没有执行完毕,现代处理器中 in-flight 的分支数量普遍可达数十条。那么下一条分支如何拿到前一条分支的方向用于历史的计算?

目前普遍采用的做法是推测更新历史,也就是在预测后立即更新分支历史供下一条分支使用,然后在分支误预测发生时对历史进行恢复。这个做法能够较好的解决历史不及时的问题,但是 Pattern History Table(PHT)的更新依然可以是可以探讨的设计。是在分支执行完就更新?还是等到 commit 以后才更新?总体来说越早更新 warmup 越快,越晚更新更新得越准确。

推测更新历史的做法就引起了下一个问题——为什么现代处理器的分支历史都是全局历史?前文刚刚讨论了局部历史和全局历史对分支预测有不同的贡献,给任何预测器加入局部历史几乎都可以进一步提高预测准确率,那么为什么没有人用?只有部分处理器使用了专门的 Loop 预测器(Loop iteration count 就是一种特殊的局部历史)。这就涉及局部历史推测更新后的恢复困难问题。

历史维护

局部历史维护的困难可以参考 MICRO 2019’的论文 Towards the adoption of Local Branch Predictors in Modern Out-of-Order Superscalar Processors

因为全局历史整个处理器只需要维护一份(或少量几份用于推测更新-恢复用),一般都直接使用寄存器来存储全局历史。但是局部历史是需要按照分支来维护,即便是只维护部分活跃的分支,也需要较大的表,因此几乎必须使用 SRAM 来存储。

上图是按照分支预测锦标赛中软件实现的 Loop 预测器设计的对应硬件的框图,可以看到需要一个 Branch History Table(BHT)用于记录目前的循环计数。

使用 SRAM 存储带来的问题就是,恢复历史时无法在一拍内恢复或者甚至无法在短时间内恢复。目前已有的几种解决办法是

  • 维护 in-flight queue,预测时同时查询 queue 获得新的历史,SRAM 内仅存储 commit 后的分支信息,不需要恢复 SRAM,对预测时序和功耗影响大。
  • 维护 recover queue,SRAM 内推测更新历史,恢复时通过 walk queue 来恢复 SRAM 内的信息,对恢复速度要求高。
  • 设计机制,让 SRAM 内历史不准确的时候不参与预测,可以较慢恢复 SRAM 内的信息。

更多设计细节的评估和对比可以参考上面的论文。

这种维护的困难使得局部历史在现代处理器的分支预测器中很少见到。

Path History

无论是哪一种分支历史,由于推测更新的维护方式,都必须先出现在 BTB 中,才能进入历史。然而,正常程序中会出现大量从不跳转的分支(如 debug 或错误处理),没有人愿意为了存储这些分支耗费多几倍的 BTB 存储开销。现代处理器普遍只有曾经跳转过的分支才会存储进入 BTB 中,这给维护 Branch History 带来了困难,如果有新分支突然 taken,那么它附近的分支由于历史的变化,基本需要重新 warmup。有的处理器为了让有限的历史寄存器放入更多有意义的信息,可能会对分支做进一步的过滤,只有 T/NT 均出现至少一次的分支才会进入历史,这样需要重新 warmup 的概率也就上升了。

因此工业界更倾向于使用 Path History。Path History就是将跳转的 Target 经过哈希计算进入历史中,这种历史天然地不受不跳转的分支影响。通过论文观察到 ARM 和 Intel 都使用了 Path History 来代替 Branch History。

ARM的PHR
Intel PHR 中 footprint 的哈希
Intel 的 PHR 历史维护

主频 & 预测延迟

如果说前面的几个方面只是增加了复杂分支预测算法落地的难度,那么预测延迟就是阻碍复杂算法落地的直接障碍。举一个例子,TAGE-SC-L 预测器中的 Statistical Corrector(SC)是为了纠正 TAGE 对于一些偏向性大的分支预测不好的部件,同时 SC 还可以作为 TAGE 的补充,如果 TAGE 由于哈希或者别的 corner case 出现失效,那么 SC 可以在一定程度上挽救问题。

但是 SC 需要使用 TAGE 的结果作为输入,还需要做所有表项的加法规约,因此在现代处理器的主频下,延迟比 TAGE 多1-2拍。这样多出来的延迟可能就把将存储分给 SC 的优势给抵消了,如果 SRAM 时序允许的话,给 TAGE 增加额外的 10% 的空间可能能取得更好的效果。SC 增加的延迟在学术上是欠评估的,因为 SC 提出的场景是分支预测锦标赛,该比赛并不要求模拟实际的预测延迟,因此复杂的预测算法更有可能因为合理的存储空间分配而获得更好的效果。

根据我的观察,SC 目前在大的商业处理器上应用很少,可能是加入这个部件增加的功耗、面积、验证成本等已经超过了它带来的误预测率降低的好处了。

Multi-branch ahead

根据统计,桌面应用的平均基本块长度约在5条指令附近(4-10),因此在设计宽译码、宽发射的高性能处理器时,如果每周期只预测一条分支,那么供指能力是不足的。现代高性能处理器普遍具备每周期预测两条指令的能力,但是根据设计能力和目标的不同,一般对两条指令的性质会有一些限制。最常见的是要求本周期的第一条指令不能跳转,这种限制大大降低了设计难度,因为在这样的限制下,两个基本块是连续的,每周期只会产生一个新的 PC,不需要做大量额外的设计就可以支持。

平均基本块长度和平均跳转长度的对比(注意单位是Bytes)

从上图中可以看到,基本上仅支持这种限制较严格的 2 Branch per cycle 就可以提升每周期的平均供指数量。香山处理器昆明湖目前也仅支持这种情况,实际在 SPEC CPU 2006 上可以做到平均每周期6-7条指令的供指能力。

近年来也有处理器能够支持 2-Taken per cycle的情况,如 ARM 和 Intel 的最新产品,但同时也有对两条分支的类型或者支持的数量做限制。

BTB设计

BTB 设计也是重要的一环,目前的设计方向有以下几个:

  • 多级 BTB
  • 下级 BTB 与 Cache 共用存储
  • BTB 压缩
  • L0 BTB / 快速预测器的设计
  • L1 BTB 的类型

其中除了 L1 BTB 的类型,其他的都比较好理解。那么为什么需要单独讨论 L1 BTB 的类型呢?因为 L0 BTB 大多使用寄存器,排布灵活,L2 BTB 有压缩等设计,类型也可以灵活。唯独 L1 BTB 在预测的关键路径上,不能使用复杂排布,又需要足够大的容量,因此需要使用 SRAM,对读写有较为严格的限制。因此 L1 BTB 的表项设计和索引方式是前端设计中的重点,这里的设计也会大幅影响 TAGE 预测器和整个前端的设计。

今年的 MICRO 2023’恰好有一篇BTB的类似综述文的文章 Branch Target Buffer Organizations ,感兴趣的读者可以找来看看。其中有对 L1 BTB 的三种常见类型的描述和对比

  • I-BTB:紧耦合前端,使用 ICache+预译码充当大容量 BTB。采用这样设计的有苹果和 SiFive。这种设计的优点是不需要为 BTB 使用额外的存储,缺点是紧耦合前端供指能力稍弱,另外 ICache 存储分支的效率是较低的,超过 ICache 容量之后,将无法使用分支信息指导指令预取。
  • R-BTB:使用减少的空间来存储一定范围内的分支(如 64B),在范围内支持一定数量(如8条)分支,BTB 使用对齐的 PC 索引,通过 offset 来确定具体的分支。优点是使用独立的 BTB 和解耦前端后,前端对分支的处理能力大幅加强,IBM 的 z15 处理器设计中,BTB 的总容量甚至可以 cover L2 Cache 的大小。缺点是当分支密度高的时候无法全部覆盖。
  • B-BTB:每个表项内存储一个基本块(或一个 Fetch Stream)的信息(下称取指块),取指块内可以有多个分支,有最长的取指块大小,使用 PC 低位索引,高位做 Tag。这种做法解决了 R-BTB 对分支密度的限制,能够处理绝大多数的情况。缺点是一个分支可能出现在多个表项内,会浪费一定的存储空间。

香山南湖和昆明湖的设计都是使用 B-BTB 的设计,取指块中允许两个 Branch Slot,第一个仅允许条件分支,第二个允许所有类型的分支。然后预测时如果第一个分支预测不跳转,那么可以按照离第二个分支的距离来供指。

功耗

功耗这一块其实我知之甚少,因为香山还没有开始做低功耗设计(狗头保命),但其实有许多大方向是低功耗设计共性的。首先,因为分支具有难度的差别,还有类型的差别,复杂的预测器和 ITTAGE 这类的间接跳转预测器是没有必要每个PC每个周期都访问的。另外 TAGE 中,每个历史表对于程序片段中的有用程度也是不一样的,所以通过统计的方法,是可以在保留大部分性能的情况下省去很多 SRAM 的功耗。

另外 SRAM 与 SRAM 天差地别,如果设计时给 SRAM 留了时序裕度,选用稍慢的 SRAM 可能可以大幅降低功耗。

一定要预测吗?

最后想谈一下从体系结构的角度,分支一定要预测吗?近年来分支预测算法发展陷入停滞,原因是 TAGE 预测器的算法已经足够优秀,能够在存储耗尽之前充分地挖掘一条分支的“可预测性”,后续的改进也基本针对 corner case 的优化和存储替换算法、置信度等的一些小优化。

那么,剩下的分支误预测怎么办,还有办法吗?此时就需要跳出固有的思维框架,不再仅思考纯硬件的做法,就会发现有新的天地。实际计算机的设计是一个系统工程,需要每一层紧密配合才能达到最好的性能。此处我们介绍两个已经实用的方法,有没有别的新的方法留待读者探究。

谓词化指令

谓词化指令其实就是赫赫有名的条件执行指令,例如 cmov 指令就是著名的谓词化指令。设计这种指令的初衷是很多难预测的分支是和程序数据紧密相关的,甚至直接依赖于数据,那么如果将这些分支转化为 cmov 或类似的条件转送/执行指令,就可以只出现数据流的变化,而不出现控制流的变化。

谓词化指令在 x86 体系和 ARM 体系中都有大量的使用,RISC-V 在 2023 年也通过了 Zicond 扩展。

软硬结合

这里讲的软硬结合大致是指 hint 类型的指令,苹果已有专利说明其设计的处理器中,有针对 macOS/iOS 进程间调用返回的长跳转做 hint 和指令预取的优化,具体的优化只能翻专利了。

结语

其实还有更多的具体设计细节,这里由于篇幅考虑(实际是我偷懒)就不详细展开了,在这里提一个列表共大家思考

  • 单端口 SRAM 的读写冲突处理
  • 预测器 ctr 降低更新延迟的影响,加快 warmup 的方法
  • B-BTB 中的多个分支对于方向预测器容量需求不对称的问题
  • 预测器中 SRAM 分 bank 的方法
  • TAGE 折叠历史的维护方法
  • 各种预测器和 BTB 的索引哈希

现在看了这么多设计考虑,你还认为预测准确率是分支预测器唯一的评价指标吗?

参考文献

The post 现代分支预测:从学术界到工业界 first appeared on Easton Man's Blog.

2021年终总结:你所热爱的,就是你的生活

作者 Easton Man
2022年1月3日 22:27

预计阅读时间: 10 分钟

跟风写一个年终总结,迟了几天,属于错峰写文了。

引言:身份的焦虑

此段是我看到@milkice2021年终总结关于评价能力指标的问题,有感而发,再加上之前读了一本书叫《身份的焦虑》,觉得这本书有回答这个问题。这本书的主要论点如下:

  • 一个人焦虑是一个人的问题,所有人都焦虑就是社会问题
  • 现代社会的阶级流动性好,与历史上任何的时期相比都是
  • 大家都认同“世上无难事,只要肯登攀”这样的理念
  • 潜台词就是“如果生活不好/社会地位不高,原因就是不够努力”
  • 人总是喜欢与一些和自己地位相近而又略高的人比较
  • 因此大家越来越焦虑
  • 但是其实这并不合理,因为社会地位影响因素太多了,而大部分并不是努力能够改变的

这本书其实还有提出一些解决办法,但是我个人觉得实在是有点玄学,就不在此列出,感兴趣的可以去看。

我出生在并不富裕但也并不贫穷的家庭,可以说我的家庭比相当多的人家庭条件还是要好上一些的,然后家长也比较重视教育,使得我能够享受到很好的教育资源,从小学中学直到大学。因此毫无疑问的是我已经比绝大多数人,所谓的“芸芸众生”要好上那么一些,然而我并不能摆脱这些“焦虑”源泉,经常能在各种地方看到各种各样的大佬,每一个都可以顶礼膜拜好多天。

所以如果一定要量化一个评价标准的话,我个人认为应该将一个人的各种“评分”Normalize到一个相同的平均值和标准差,再来评价一个人所谓的“努力程度”。

我所在的计算机系已经属于努力和成绩比较接近线性的学科了,我觉得我应该为此感到庆幸。

回顾

上一次写年终总结已经是2018年了,那个时候我是高二,现在是大二,三年过去了。2018年的时候我在用typecho写博客,还维护一个我自己重构的主题,2020年高考完重建博客的时候换用了Wordpress。2018年的时候想考去中科大少创班,想着2019年6月考完就解放了,还可以去USTCLUG,结果造化弄人,6月考完得知落榜的第二天,我就收到了清华的一本优惠,然后造化继续弄人,2020年自主招生就取消了。然后我就到了哈工大深圳。现在想来,也不算是遗憾了,命里有时终须有,我在计算机系能够找到我热爱的事情,希望以后能一直热爱下去。

博客

首先是2021年本博客的一些数据和统计,全年访客共5889人,访问页面数11042。2018年的时候因为维护主题,也有4000多的访客量,但是跳出率很高,也就是大多数人都只看一个页面就走了。(以及下面这张图的跳出率是错的)

访客统计图

数据统计用的是umami,符合GDPR。显然5月份写的《现代处理器》因为在V2EX上宣传了一下,带来了很多的访客。全年访问最多的也是它,有2.09k的访问量。Referer最多的是Google,有3.15k的访问量。

2021年的博文一共写了36703个字,写的还是《现代处理器》,有9075字。到目前为止,博客一共发布了46k个字。

野鸡运维

2021年维护了一堆又一堆的机器,有自己的,也有学校的

最长的uptime是一个代理机器,有一年24天,前两天重启了,截个图纪念一下

tuptime

2021年持续运行的服务有:

  • Gitea 自建的Git服务
  • RSSHub 将网页转化为RSS

申请了一个ASN号,成为了HostUS受害者,但是目前为止基本处于荒废状态,DN42里也注册了ASN,但是至今也没有Peer。2022年希望能有时间玩这个吧。

GitHub

2021年代码量前4的Repo

GitHub Repo Wrapped

2021年使用最多的语言,看来我写了很多Go。C++主要是那个ASC练习题,Zig主要就是Zesty-Core这个玩具操作系统。

GitHub Language Wrapped

2021往acme.sh交了PR,还往podman交了PR,虽然只是一个小改动。

实习

暑假出去实习了,因为暑假没事干很无聊,因为老师建议尽早实习感受一下。当时投递的是高性能计算岗,先是笔试考了一些简单知识,还有几道算法,然后是电话面试,基本上是问简历上写的项目和几道算法,我感觉我答得也不算好,因为我本来不搞OI,算法设计这门课也还没上,所以好多算法都不知道或者写错了。

最后还有一个实际项目的测试,是五一的前两天做的,要求写一个矩阵乘法,还有一个“生产者-消费者”模型。这个应该是我第一次写SIMD的intrinsic,最后矩阵乘法的效率应该也没有多高,好像大概是10%左右吧,当时没有分析达到了峰值性能的多少,还被导师指点了一下。后来我也有参考别人的做法自己写,能够达到50%左右的效率,和OpenBLAS差不多了,MKL在当时那台机上能达到70%,真不知道英特尔是怎么写的。“生产者-消费者”模型就是要求用Redis的队列实现,Redis我直接用了个C++的库,大概写的差不多就交了。

最终还真过了,然后7月初开始就去实习了,一直到8月20号左右。在公司做的是JPEG实时解压这个方案的调研,结果是花了6核的CPU才能做到30帧,毕竟要解压JPEG成YUV420,然后再通过一个已有SIMD的库变换成为特定视角的图,我通过每帧流水线式的并行解码达到了30帧,但是当时leader觉得这个方案计算量还是太大,不知道最后有没有用上。

竞赛

2021年其实参加了三个竞赛,如果把1月截止的ASC20-21也算上就算是四个竞赛了。3-4月份跟着某位大佬去华为的软件挑战赛,结果卷不过别人,拿了个三等奖。然后就到了暑假,暑假一边实习,一边做CPC的初赛,还有IKCEST的初赛和复赛。IKCEST我们两个人都是不是做CV的,比赛还有性能要求,只好现学现做,甚至搞上了TensorRT这种东西。

CPC那边得益于两个给力的队友,成功去了现场的决赛,还拿到了银奖。初赛的时候两个队友在上小学期的CPU设计与实践,我主要抄论文写汇编计算核,队友写分块保证正确性。复赛的时候变成了大项目,其实时间也不是很多,基本上是刚刚好把能做的初步优化都做了,然后就到了决赛了。有些队伍做的算子融合我们都一个都没做,不过这次比赛让我知道了先做Profile再做优化的重要性,还有接下来的ASC可能要引入自动化测试,免得像CPC那样决赛前一天晚上大家通宵解决正确性问题。

入场照

社团

2021年春天我有想法做一个镜像站,问了几个老师和同学,就变成了组织一个Linux用户协会这种类型的社团,可惜我一无建立社团的经验,二无宣传拉人的经验,所以镜像站建起来一个雏形以后就没有接下来的动作。直到秋天21级的本科生进来了,突然有了很多志同道合的小伙伴,于是就很快有很多的人,还搞了两场Talk,虽然都是@萝佬讲的。希望LUG能越办越好,毕竟大家都卷AI在我看来也不是什么好事。

放一个LUG徽标在这里

HITSZ LUG

2022展望

首先是希望ASC22能进决赛(小小的愿望都不可以满足吗),今年队友都很好,学校也是给了很大的硬件支持,自己的水平经过一年也有所提升,希望今年能圆梦ASC吧。

然后希望ASC的工作能像清华他们那样写一篇论文吧。

另外秋季学期的课程学得不是很认真,希望春季能在兼顾竞赛的同时学好课程吧,能把学分绩卷上去一点,俺也要保研名额!

题图来自@mrthetrain,感谢

The post 2021年终总结:你所热爱的,就是你的生活 first appeared on Easton Man's Blog.

最优停止理论与导师选择

作者 Easton Man
2021年12月8日 00:17

预计阅读时间: 10 分钟

这是我的概率论小论文,是经由《如何选择心仪对象》启发,加入了一些我自己的想法和论文查询,经过重新排版以后发布到博客上。如果你希望看由Latex生成的PDF,可以在这里找到。

问题背景

最近我阅读到了一篇文章,描述最优停止理论在研究生导师选取上的应用。文章使用了经典麦穗问题的结论,我认为对于这个场景,有更合适的最优停止策略。

想象有以下情景:某同学在参加保研面试。面试之前他与这些老师互不了解,聊完之后他对老师们有了自己的评价;但出乎他意料的是,每个老师在面试结束时都表达出对某同学的满意,当即给出offer,并要求他现场决定是否接受。某同学只有一次选择机会,offer接受后不能再反悔拒绝,拒绝后也不能再反悔接受。在这样的情境下,这位同学该如何在他们中选出评价最高(即最喜欢)的一位呢?

此类问题其实是非常古老的问题,被人们研究了很多年了,策略通常为“观望-选择”,因主要研究最佳的停止观望时间,此类问题称为最优停止问题。最优停止问题又称麦穗问题、秘书问题、博士结婚问题等,大家应该对以下的故事有所耳闻:

在很久之前,苏格拉底的三个弟子向他请教,问他怎样才能找到自己理想的伴侣。他什么话都没说,直接把三个弟子带到一片麦田里,让弟子们每人挑一根最大的麦穗。做这一切的前提是,不能走回头路,也就意味着他们只有一次选择的机会。

这三个弟子呢,颇有意思,因为他们的选择大不相同。

第一个弟子刚走进去没多久,就摘了一根自以为很大的麦穗,心里不禁洋洋得意。但他越走越发现自己做错了选择,因为后面的麦穗远大于他手里的麦穗。

第二个弟子不同于第一个弟子,他恰好相反,他一直往前走,总觉得最大的麦穗都在前面,所以他看到比较大的麦穗早就错过了。

第三个弟子,也是三个人里比较聪明的一个了,他不慌不忙地边走边思考,然后把麦田分成了三份,走第一段路时,他只看不摘;继续往前走时,他会将现在的麦穗与之前看到过的麦穗进行比对。最后他挑选了一根他认为最大的麦穗。当然,他也是这三人中拿到最大麦穗的人。

对于经典的麦穗问题,最受公认的最优停止时间是麦穗总数N的1/e,也即N/e。以下我们将会讨论这个结论的来源和我们“导师选取”问题的结论。

经典结论

我们将这个问题用数学语言描述:考虑一组N个独立服从[0,1]上均匀分布的随机变量X_1,X_2…X_N然后我们按编号从小到大遍历这些变量,对于每个变量,我们只能做出“保留”或者“丢弃”两种选择,并且我们只有一次保留机会。那么应该如何制定策咯,使得我们选取到最大变量的概率最大?

我们主要关注于一种简单的策略,就是通过某种方法设定一个阈值c,在选取过程中一旦出现值超过阈值c的变量,那么立即选取。很自然地有这样一个策略:对于前m个变量,我们一律不选,对于从m+1号开始的变量,一旦有比前面更大的变量,我们就立即选取。这种先观望,后选取的策略,以下称为m策略。

现在讨论如何选取m的值。

对于某个特定的m,如果最大值出现在第i个位置,m策略选取到最大值的概率为

\begin{aligned}
P(m,i)=
& 0 & {i \leq m} \\
& \frac{m}{i-1} & {i>m}
\end{aligned}


所以如果我们使用m策略,成功选取到最大值的概率为

\begin{aligned}
P(m) &= \sum_{i=m+1}^{n} \frac{1}{n} P(m,i) \\
&= \frac{m}{n} \sum_{i=m+1}^{n} \frac{1}{i-1}
\end{aligned}


当n和m都较大的时候,令r= m/n,可将上式右侧的调和级数近似为ln(m/n),可得:

\begin{aligned}
P(m) & \approx \frac{m}{n} \ln\frac{n}{m} \\ \Rightarrow P(r) & = -r\ln r
\end{aligned}

求导得:

\begin{aligned}
P'(r) = -1-\ln r
\end{aligned}


当r=1/e时,P'(r)=0,P(r)达到最大值1/e

所以对于麦穗问题的m策略,m的最优值就是N/e(取整数)。

然而,选取研究生导师和麦穗问题还有不同之处:1) 我们并不需要选到最好的导师,只是希望选取到好的导师。2) 我们不能允许我们糟糕的选择策略使得我们没有导师!

导师选取问题

首先用数学语言描述“导师选取”问题:对于N个导师,我们有主观评价分X_1,X_2,…,X_N,然后我们希望通过某种策略选取导师。这种策略应当使选取的导师的评价分的期望尽可能高,同时也能保证我们能够选到老师(有学上)。除此之外,在现实中,评价导师并不能使用量化的分数,而只能通过比较,认为某导师较好。因此,我们设定虽然每位导师都有评价分X_i,但我们选择的时候并不能得知具体的分X_i,而是只能得到导师评价分之间的关系。

因此我们无法使用任何动态阈值或使用不同于任何导师评价分的阈值。我们只能使用类似m策略的方式,为了区分,下称t策略。t策略是指通过遍历前m个导师确定一个阈值t=max{(X_1,X_2,…,X_m)}然后从第m+1开始,只要某导师的评价高于t,那么立即选取该导师,如果直到第N-1位导师都没有找到评价大于t的,那么选取最后一位导师。

我们希望选取合适的m使得最终我们选取的导师的评价分的期望尽量的大。

首先我们计算阈值t的分布函数

\begin{aligned}
F(t) &= F_{max}(t) \\
&= P(\max(X_i) \leq t) \\
&= \prod P_i(X \leq t) \\
&= t^m
\end{aligned}


由此可以得到阈值t的概率密度函数

\begin{aligned}
f(t) &= F'(t) \\
&= m t^{m-1}
\end{aligned}

对于确定的阈值t,有两种情况:1) 我们成功选到了心仪的导师。 2) 我们只好选了最后一位导师。

分别有概率

\begin{aligned}
P_2 &= t^{N-m-1} \\
P_1 &= 1 - P_2 \\
&= 1 - t^{N-m-1}
\end{aligned}

而这两种情况对应的评价分的期望为E_1 = (1+t)/2,E_2 = 1/2

于是我们可以计算出对于确定的m,选取导师的评价分的期望为

\begin{aligned}
E(x) &= \int_{0}^{1} f(t) (P_1 E_1 + P_2 E_2) dt \\
&= \int_{0}^{1} mt^{m-1} \left[ \frac{1}{2} t^{N-m-1} + (1-t^{N-m-1})(\frac{1+t}{2}) \right] dt \\
&= \frac{2m+1}{2(m+1)} - \frac{m}{2N}
\end{aligned}

当期望E(x)最大时,有

\frac{\partial E}{\partial m} =\frac{1}{2(m+1)^2} - \frac{1}{2N} = 0

解得

m = \sqrt{N} -1

可见如果我们希望以期望为指标,那么我们的最优停止时间将是\sqrt{N}-1。也就是从第\sqrt{N}位导师开始,只要出现比前面评价分高的导师,我们就立即选取。

讨论

我们发现,麦穗问题和“导师选取”问题的结论其实有很大不同,分别为m=N/e和m=\sqrt{N}-1。这里显然有一个增长速率上的差异。当N足够大时,按照\sqrt{N}-1进行的策略需要观察的元素数量远小于按照N/e的策略。

这是因为选择到最大元素X_k要求在[1,k-1]的元素中,我们选定的阈值c既是[1,m]中最大的,也是[1,k-1]中最大的,这样的情况下要求我们选定的阈值较高,才更有可能选取到最大元素。而在按照期望为指标的策略中,我们并不需要选取最大的元素,而是期望较大就可以了,因此需要的观望次数也较少。

现实生活中有许多可以应用最优停止理论的地方,其中有相当一部分并不完全符合经典麦穗问题类的前提,而是与本文提出的修正情况相符合,因此本文得出的结论具有一定的现实意义。

结语

当然以上提出的情景和我没有任何关系,因为我在写概率论论文,没有导师给我发offer。希望我能够有应用这个结论的场景,因为这意味着我至少不会失学了。同时也祝愿所有同学都不失学,并且能找到对象。

参考

The post 最优停止理论与导师选择 first appeared on Easton Man's Blog.

论大学之义

作者 Easton Man
2021年8月7日 22:31

预计阅读时间: 16 分钟

此演讲稿经授权首发于本站,任何转载或引用需署名并保留原文链接。

强调大学之义有其现实因素。青年兴则国家兴,青年强则国家强。提高青年学习和奋斗的自觉性是谈论大学之义的原因之一。大学前奋发图强,大学后碌碌无为,乃当今大学生的常态。我所属大学在广东乃至全国的分数都不低,工科亦属中国第二,世界第四;但是身边许多大学生仍没有自己的人生目标,入大学如出狱般无拘无束。我们学院衡水中学进来的十数名学生,以其分数若在广东,实是清北之材;但他们的大学成绩排在倒数,只因大学后早上旷课,晚上喝酒,学业荒废。“衡水模式”之弊在于,过度注重时间管理和考试,以至学生没有空余时间来思考人生真正的意义,发掘自己真正的兴趣所在,让学生变成了在高压管理制度之下随波逐流的机器。但这样的惯性在大学无以为继,因为上大学后,没有衡水的老师家长推着学生学习。只有想通了上大学的意义,在大学才有动力去奋斗。

谈论大学之义的必要性之二,在中国经济发展对大学生的专业选择提出了新要求。现在某些职校生比普通大学生更容易就业。这主要是因为经过改革开放,社会经济条件产生巨变。四十年前“唯学历论”盛行,因为彼时大学生很少,能上大学的往往在智商上,自觉性上,更胜他人;现今大学生很多,除“物以稀为贵”的影响外,当下中国不再是四十年前敢闯敢拼就能干出一番事业的社会。生产力的提升与社会分工的日渐细化互为因果,而分工细化导致在大学综合水平相差无几的情况下,选择一个好的专业比一所名气更大的大学,对人生的影响要更为正面。转行在当下显得不那么容易。一个学化学的学生如果从来没有接触过计算机,完全不知道计算机的体系架构和组成原理,那么他将无法满足企业的需求。倘若他想对计算机基本原理了解个大概,则需要许多时间;若要精通则又要数年时间。企业几乎不可能聘用一个不熟悉业务甚至可能带来损失的人,因为当下有诸多熟悉业务的大学生可供选择。分工细化和私营企业的增多,私有制经济的蓬勃发展,导致社会“唯学历论”向“唯能力论”的转变。甚至于国企也开始进行整合重组,加强对内部员工的能力要求。现今中国社会需要的是干实事的人,需要的是某领域的专才。以名气为主的大学选择方式日渐式微,而只有想清楚自己上大学的真正原因,青年学生才能正确选择专业,发展特长,过属于自己的人生。

“大学之道,在明明德,在亲民,在止于至善”。私以为今日之大学,也应承大学之道。中国大学是青年学生提高知识水平的地方,是青年学生培养专业能力的地方,也是青年学生提高思想觉悟的地方。欧洲最初的大学是一帮知识分子为了摆脱教会的束缚,为获取学术自由,为讨论学术观点聚集而成的组织,后来逐渐演化为教育机构。故当今大学担负两个责任:一是教书育人,即培养本科生和研究生;二是研究考察,即探索世界,发掘知识。二者之间,孰轻孰重?从大学的发源上讲,研究考察为重;从大学承担的社会功能来看,教书育人为重。教书育人一是传授知识,而是建设思想。传授知识必非纸上谈兵,空有理论;而是理论与实践相结合,以提高学生的动手能力为首要。目前中国大学有延续高中填鸭式教育的现象,而缺失技能的培养。直接证明即中国大学普遍不重视实验,没有像美国大学那样的课程大作业。所谓“大作业”就是实践过程,要求学生利用学到的知识做出实际的东西,包括但不限于社会调查研究(社会类学科课程)、可运行程序(计算机类)、对某个数理化问题的探究结论(数理化课程)等。这些大作业不只是对创新性的培养,也是对技能的培养。如果不上机写程序,C语言课教的永远只是理论上的C语言;如果不做问题探究,不做实验室里的实验,化学永远只是纸上的化学,生物课也不能教会学生怎么制造生物试剂。除传授知识和培育技能外,大学的目的既然是培养中国之栋梁,非外国之栋梁,那么仍需要对学生进行思想建设,培养家国情怀和社会责任。综合而论,传递知识、技能、思想这三点共同构成了大学在青年学生的人生中扮演的角色。

“见贤思齐焉,见不贤而内自省也”。很多人知道在大学能见天下贤士,拓展自身见识,即大学相比高中能为青年学生提供更加广阔的视野和资源。人们觉得这就是上清北的好处,即能够见识到全国各地最顶尖的学生。这样的说法正确在于北京的确有最顶尖的人才和学生,错误在于认为其他大学没有顶尖的学生。此种认识的根源在于“唯分数论”,在于单一的人才评定标准。其谬误在于以高考成绩作为评价学生是否优秀的唯一标准,而在当今中国“唯学历论”向“唯能力论”转化的情势之下,显然是站不住脚的。鼓励上清北的真正原因在于清北作为首都学校,拥有全国最丰富的资源。这一点从每年国家的经费拨款就能看出来。但资源丰富不代表任何人都能享有,在人类社会中存在资源向少数集中的趋势,大多数勉强上清北的学生会被边缘化。我自己在理学院的成绩排名第四,但如果放在计算机系则排不上号。正因如此我在理学院给辅导员和教师提的意见大部分都得到采纳;而我在计算机课上提的意见基本均被无视。做机器学习需要专门的显卡,我作为理学院的学生向老师申请就能拿到6张;而在计算机学院则根本找不到这样的渠道。同时,资源也包含教师资源——这体现在专业选择上。对要学人工智能和机器学习的学生,最优秀的选择是南京大学,因为南京大学有全国最顶级的教授周志华,南京大学的人工智能学院也是他亲手筹办,课程设置和培养体系可谓是全国典范。所以要知道具体上哪所大学,必先了解该大学具备的资源,包括硬件资源和教师资源,而不可人云亦云。即专业比学校更重要。

专业选好了能保证学生的知识水平和在社会上的被需要程度,但这只是大学里第三重要的。第二重要的是能力的培养。这里能力广泛地指在学科领域的能力,在人际交往上的能力,在自我管理上的能力,在深入思考社会问题上的能力等。由于“唯能力论”的当下社会要求,只知学习与工业需求脱节的大学课程是行不通的。这也是当下这么多人选择读研的原因。读研热潮的根本在于本科无法锻炼业界真正需要的能力,只能把出来工作的时间往后拖延,期望读研时能够学到更多实践能力。其实这纯粹是空想。在中国,知识的传授方式沿袭苏联的灌输式传统。研究生唯一的优势就是项目和实验做得多,即动手实践机会多。那么为何不在本科就更多地进行实践活动?“纸上得来终觉浅,绝知此事要躬行”。能力是在实践中培养的,真正的知识也是在实践中获取的。比如在学微积分这件事情上,如果一个人终其一生均是平凡的屠夫,那么学微积分对他无半点大用。只有成为工程师,成为数学家,成为物理学家,把不论是所谓思维方式还是微积分本身在生活中真正地实践出来,才算是真正懂得了微积分,微积分才算是真正地影响了他的人生。但实践需要平台和资源,所以在大学要跟对导师,要主动跟导师做项目。导师的作用就是给学生实践的机会。同时实践并不仅限于学科领域的实践,也涉及到人际交往的实践,社会活动意义上的实践如义工活动等。这一点每个青年学生须自行发掘其中方法,但首要是形成这样的认识,即提升自我能力的迫切需要,而非践行“大学享乐主义”。

思想是行动的指南,在大学中最重要的正是青年学生的思想。为什么人当为国奉献?如果想不通这个问题,青年学生必不能发自真心地为人民服务。从人类物质与精神追求的关系来看,物质富足的人必然要寻求自我价值的实现,否则内心就会感到空虚。为人民服务正是青年实现自我价值的最好方法。当个人在群体中奉献自我,他才会感受到社会认同。人生的意义只存在于人类群体当中,而不存在于个人;或者说只有当一个人置身于社会之中时,他所做的除开谋生之外的一切事情,才能够说是有意义的,因为人对意义的感知正是在群体当中发源的。古代无数文人志士为民请命,正是此种奉献精神的体现。从个人成就的来源上讲,每个人生活在社会中,他们所取得任何成就都不仅来源于他们自己。现代互联网大亨的成功更多是国家政策的扶持,是当今中国的经济条件给了他们机会,而这个条件是全体人民共同劳动的结果。如果没有大家的共同劳动,社会生产力就不能得到提升,互联网就没有市场需求,他们就不会成为富翁。同样,学生享有的教育资源是所有老师和学生共同努力建设的结果,也是国家政策扶持,社会群众支持的结果。学校的钱来自于国家,国家的钱来自于纳税人,而纳税人就是全体劳动人民。因此人不回馈社会则与“不孝子孙”同,任何人特别是青年学生,应当以服务社会为己任。这在学生的身份上落实正是服务老师,服务学生。看见学校的课程办得不合理,青年应当勇于提出建议;看见其他学生对专业的就业方向感到迷茫,有能力的可以主动站出来为其他人开设讲座,解除疑惑;没打过竞赛的学生在赛前感到无所适从,有经验的学生可以主动请缨传授自己的方法,也可以像学院老师建议请业界工程师前来开讲;学校某些政策制定不得人心,青年学生也应当站出来维护公理。为人民服务的方法除此之外仍有许多,但以形成其意识为首要。

“路漫漫其修远兮,吾将上下而求索”。认识到位了,更重要的是行动。所谓“知行合一”,知而不行则不为真知。“行”是唯一“知”,实践出真知。在繁忙的高三学业中抽出时间来思考自己的人生价值,思考自己的大学选择,是非常必要的。

作者简介:徐兆彬,2019年广东实验中学南山一班毕业,现就读于哈尔滨工业大学(深圳)理学院,目前成绩排学院第四,曾获BDCI(CCF大数据与计算智能大赛)全国第七名,第九届泰迪杯数据挖掘挑战赛全国二等奖,CCPC(中国大学生程序设计竞赛)威海站银牌,全国大学生数学竞赛黑龙江省省级一等奖,Kaggle平台上两场比赛的第一名。大一入学即进实验室跟随户保田教授做自然语言处理的文本生成领域,目前已向人工智能领域顶级会议ICLR投稿一篇论文。曾任微软俱乐部主席,为学生开展多次竞赛讲座。同时在理学院建立竞赛群体分享竞赛经验,帮学生提升自身能力,明了专业未来发展方向;也曾多次向学院提出课程改革建议并得到采纳。

此演讲稿是经大佬授权首发于本站,希望能够使更多的人得到启发,任何转载或引用需署名并保留原文链接。

The post 论大学之义 first appeared on Easton Man's Blog.

现代处理器结构

作者 Easton Man
2021年5月17日 23:10

预计阅读时间: 36 分钟

一个简短的、直接的现代处理器微架构设计介绍。

当今的机器实在是非常原始,它们只能理解区区几个简单的指令,比如“向左”、“向右”和“生产汽车”。

Today’s robots are very primitive, capable of understanding only a few simple instructions such as ‘go left’, ‘go right’ and ‘build car’.

John Thomas Sladek

警告:本文旨在以非正式和风趣的语言讲述严肃的科学。

警告2:长文!预计阅读时间36分钟。

本文主要向计算机专业的低年级学生和对现代处理器结构感兴趣的读者介绍有关处理器微架构的一些概念。具体来说,有以下几个方面:

  • 流水线(超标量执行、乱序执行、超长字指令、分支预测)
  • 多核和超线程(同步超线程 SMT)
  • SIMD指令集(SSE、AVS、NEON、SVE)
  • 缓存和缓存机制

听起来内容很深奥,但是,不要害怕!这篇文章将带你快速地了解这些看似只有处理器设计从业者或者是体系结构专家才能了解的东西。也许你很快就可以和你的同学/朋友吹牛了🤣

超10G!

超10G!全人类感谢你!

——某up

主频越高,CPU性能越好,这似乎是很多人的误区(不包括以上引用的up主),但是,从上古时期开始,CPU的性能和主频就没有直接关系。那么这个刻板印象是从哪里来的呢?🤔

让我们来看看一些上古(上世纪90年代末)的处理器数据…

主频型号SPECint95SPECfp95
195 MHzMIPS R1000011.017.0
400 MHzAlpha 2116412.317.2
300 MHzUltraSPARC12.115.5
300 MHzPentium II11.68.8
300 MHzPowerPC G314.811.4
135 MHzPOWER26.217.6
1997年的处理器性能

SPEC是一个当年常用的性能测试工具,乔布斯在宣布苹果的Macbook由IBM PowerPC平台转向Intel的酷睿平台的时候就在发布会上展示了SPEC的性能提升。

从表中可以看到,为什么300MHz的处理器有这么不同的性能差异?为什么低主频的CPU反而吊打高主频的?

什么?你说这都是上古的数据?那就来一个最近的:

超到一半人类感谢你(5GHz)的Intel i9-9900K居然被M1吊打?

是的,你没有看错,这就说明显然除了主频以外还有一些什么东西,那就是——

流水线和指令级并行

指令在处理器中是一个接一个的执行的,对吗?不完全对。这样的说法可能是直观的,但是并不是事实,实际上,从80年代开始,CPU就不再是完全顺序执行每个指令了。现代处理器可以同时执行不同指令的不同阶段,甚至有的处理器也可以完全同时地执行多个指令。

让我们来看一看一个简单的四级流水线是怎么构成的。指令被分成四个部分:取指、译码、执行和写回

如果CPU完全顺序执行,那么每条指令需要花费4个周期才能执行完毕,IPC=0.25(Instruction per cycle)。当然,古老一点的时期更喜欢使用CPI,因为当时的处理器普遍不能做到每周期执行一条指令。但是现在时代变了,你能接触到的任何一个桌面级处理器都可以在一个周期内执行一条、两条甚至是三条指令。

顺序执行的处理器

正如你所看到的,实际上CPU内负责运算的组件(ALU)十分的悠闲,甚至只有25%的时间在干活。什么?怎么压榨ALU?我看你很有资本家的天赋嘛…

好吧,现代处理器确实有手段压榨这些ALU(对,现代处理器也不止一个ALU)。一个很符合直觉的想法就是既然大部分的阶段CPU都不是完全占用的,那么将这些阶段重叠起来就好了。确实,现代处理器就是这么干的。

流水线执行的处理器

现在我们的处理器大多数时候一个周期可以执行一条指令了,看起来不错!这已经是在没有增加主频的情况下达到四倍的加速了。

从硬件的角度来看,每级流水线都是由该级的逻辑模块构成的,CPU时钟就像一个水泵,每次把信号(或者也可以说是数据)从一级泵到下一级,就像这样:

流水线微架构

事实上,现代处理器除了以上这样简单的结构,首先还有很多额外的ALU,比如整数乘法、加法、位运算、浮点数的各种运算等等,几乎每种常用的运算都有至少一个ALU。其次,如果前一条指令的结果就是下一条指令的操作数,那么为什么还要把数据写回寄存器呢?因此就出现了Bypass(前递)通路,用于在这种情况下直接将数据重新送到运算器的输入端口。综合起来,详细一点的流水线微架构应该长这样:

详细的流水线微架构

更深的流水线——超级流水线!

自从CPU主频由于某种原因(某种神秘力量?😈)很多年没有大的进步以来(对的,超频榜第一还是AMD的推土机架构CPU),流水线的设计几乎成为了CPU厂商的竞赛主场。加深的流水线首先可以继续增大实际的IPC(理论上限仍是1),其次可以避免流水线对时序的影响。这与晶体管的特性有关,感兴趣的读者可以上网搜一搜多级流水线结构为什么会影响时序和最终综合出的主频。

在2000-2010年间,这种竞赛达到了最高峰,那时候的处理器甚至可以有高达31级的流水线。但是超深的流水线带来的是结构上的复杂和显著增大的动态调度模块设计难度,因此,从那以后就没有再出现过使用这么多级流水线的CPU了。作为对比,目前(2021年)的处理器多半视应用场景的不同采用10-20级不等的流水线。

x86和其它CISC处理器通常有着更深的流水线,因为他们在取指和译码阶段有数倍的任务要做,所以通常使用更深的流水线来避免这一阶段带来的性能损耗。

多发射——超标量处理器

既然整数的运算器和浮点数的运算器以及其它的的一些ALU互相之间都是没有依赖的,自己做自己的事情,那为什么不进一步压榨它们,让他们尽可能地一起忙起来呢?这就出现了多发射和超标量处理器。多发射的意思是处理器每个周期可以“发射”多于一条的指令,比如浮点运算和整数运算的指令就可以同时执行且互不干扰。为了完成这一点,取指和译码阶段的逻辑必须加强,这就出现了一个叫做调度器或者分发器的结构,就像这样:

超标量处理器微架构

或者我们来看一张实际的Intel Skylake架构的调度器,图中红圈的就是负责每周期“发射”指令的调度器。

Skylake 调度器

当然,现在不同的运算有了不同的“数据通路”,经过的运算器也不同。因为不同的运算器内部可能也分不同的执行阶段,于是不同的指令也就有了不同的流水线深度:简单的指令执行得快一些,复杂的指令执行得慢一些,这样可以降低简单指令的延迟(我们很快就会涉及到)。某些指令(比如除法)可能相当耗时,可能需要数十个周期才能返回,因此在编译器设计中,这些因素就变得格外重要了。有兴趣的读者可以思考梅森素数在这里的妙用。

超标量处理器中指令流可能是这个样子的:

现代处理器一般都有相当多的发射端口,比如上面提到的Intel Skylake是八发射的结构,苹果的M1也是八发射的,ARM最新发布的N1则是16发射的处理器。

显式并行——超长指令集

当兼容性不成问题的时候(很不幸,很少有这种时候),我们可以设计一种指令集,显式地指出某些指令是可以被并行执行的,这样就可以避免在译码时进行繁复的依赖检验。这样理论上可以使处理器的硬件设计变得更加简单、小巧,也更容易取得更高的主频。

这种类型的指令集中,“指令”实际上是“一组子指令”,这使得它们拥有非常多的指令,进而每个指令都很长,例如128bits,这就是超长指令集(VLIW)这个名字的来源。

超长指令集处理器的指令流和超标量处理器的指令流十分的类似,只是省去了繁杂的取指和译码阶段,像这样:

VLIW指令流

除了硬件结构,超长指令集处理器和超标量处理器十分的相似,尤其是从编译器的角度来看(我们很快也会谈到)。

但是,超长指令集处理器通常被设计成不检查依赖的,这就使得它们必须依赖编译器的魔法才能保证结果的正确,而且,如果发生了缓存缺失,那它们不得不整个处理器都停下来,而不是仅仅停止遇到缓存缺失问题的那一条指令。编译器会在指令之间插入“nops”(no operations)——即空指令,以保证有数据依赖的指令能够正确地执行。这无疑增加了编译器的设计难度和编译所需的时间,但是这同时节省了宝贵的处理器片上资源,通常也能有略好的性能。

现在仍在生产的现代处理器中并没有采用VLIW指令集的处理器。Intel曾经大力推行过的IA-64架构就是一个超长指令集(VLIW)架构,由此设计的“Itanium”系列处理器在当时也被认为是x86的继承者,但是由于市场对这个新架构并不感冒,所以最终这个系列没有发展下去。现代硬件加速最火热的方向是GPU,其实GPU也可以看作是一种VLIW架构的的处理器,只不过它将VLIW架构更进一步,使用“核函数”代替指令,大大增加了这种体系结构的可扩展性,有兴趣的读者也可以了解相关方面的内容。

数据依赖和延迟

我们在流水线和多发射这条路上能走多远?既然多发射和多级流水线这么好,那为什么不做出50级流水线、30发射的处理器?我们来讨论以下两条指令:

a = b * c;
d = a + 1;

第二条指令依赖于第一条指令——处理器在完成前一条指令之前无法执行下一条指令。这是一个很严重的问题,这样一来,多发射就没有用武之地了,因为无论你制造出了多少发射的处理器,这两条指令还是只能顺序地执行(除去取指等部分)。有关依赖和消除的问题我们会在后面讨论。

如果第一条指令是一个简单的加法指令,那么加法器在执行完毕后可以通过Bypass通路(前递)将数据传回ALU的输入端口并继续计算,这样流水线才可以正常工作。但是很不幸,第一条指令是一个需要多周期才能完成的乘法(目前的大多数CPU没有使用单周期乘法,因为复杂的逻辑通常会损害主频),这样的话,处理器为了等待第一条指令完成就不得不往流水线中加入若干“气泡”也就是类似于“nops”的指令来保证运算的正确性。

一条指令到达运算器的输入端口和执行结果可用之间需要耗费的CPU周期称为指令的延迟。流水线越深,指令的延迟就越高,所以更深的流水线如果无法有效地填满,那么结果只能是很高的指令延迟而无益于处理器的性能。

从编译器的角度(考虑了Bypass,硬件工程师口中的延迟通常不包括Bypass),现代处理器的指令延迟通常是:整数乘加和位操作1周期,浮点数乘加2-6周期不等,sincos这种复杂指令10+周期,最后是可能长达30-50周期的除法。

访存操作的延迟也是一个很麻烦的问题,因为它们通常是每条指令最开始执行的步骤,这使得它们造成的延迟很难用别的方式补偿。除此以外,他们的延迟也很难预测,因为延迟很大程度上取决于缓存是否命中,而缓存是动态调度的(我们很快也会讲到)。

分支和分支预测

另外一个流水线的重要问题就是分支,我们来看一看接下来的一段程序:

if (a > 7) {
    b = c;
} else {
    b = d;
}

编译成的汇编程序将会是这样:

    cmp a, 7    ; a > 7 ?
    ble L1
    mov c, b    ; b = c
    br L2
L1: mov d, b    ; b = d
L2: ...

现在想象一个流水线处理器来执行这一段程序。当处理器执行到第二行,也就是第二行的跳转命令到达处理器的执行器的时候,它肯定已经把后面的所有指令都提前从内存中取出存并完成了译码工作了。但是,究竟跳转的是哪一条指令?是3,4行还是第5行?在跳转命令到达执行器之前我们并不知道应该跳转到哪里。在一个深流水线的处理器中,似乎不得不停下来等待这个跳转命令,再重新往流水线中填入新的指令。这当然是不可接受的,程序中,尤其是循环时,分支跳转的命令占比很大,如果每次都等待这条命令的完成,那么我们的流水线就不得不经常地暂停,而我们通过流水线取得的性能提升也将不复存在。

于是现代处理器会做出猜测。什么?处理器竟然靠猜,我还以为发达的处理器设计行业能给出更好的解决方案呢!先不要着急,实际上程序中分支的跳转是有规律的,现代处理器分支预测的准确度通常能达到99%以上(虽然分支预测也是Intel的spectre和meltdown漏洞的来源👿)。

处理器会沿着预测的分支执行下去,这样一来,我们的处理器就可以保持流水线和运算器的占用,并高速地执行下去。当然,执行的结果还不能作为最终的结果,只有在分支跳转命令的结果出来以后,预测正确的结果才会被写回(commit或retire)。那猜错了怎么办?那处理器也没有好的办法,只能重新从另一个分支开始进入流水线,在高度流水化的现代处理器里,分支预测错误的代价(分支预测的错误惩罚)是相当高昂的,通常会达到数十个CPU周期。

这里的关键在于,处理器如何做出预测。通常而言,分支预测分为静态和动态两种。

静态分支预测即处理器做出的猜测与运行时的状态无关,而对跳转的优化由编译器完成。静态预测通常有一律跳或者往后跳预测不跳,往前跳转则预测跳。后者通常效果更好,原因是循环中一般会有大量向前的跳转指令。

动态分支预测则是根据跳转指令的历史决定是否跳转。一个最简单的动态分支预测器就是2位饱和计数器,它是一个四个状态的状态机,特点是只有连续两次预测错误才会更改预测方向。它已经能在大部分场合下取得90%以上的预测正确率。

2位饱和计数器 Author: Afog CC BY-SA 3.0

这种预测器在交替出现跳和不跳的分支指令时表现不佳,于是人们又发明了n级自适应分支预测器,它的原理与2位饱和计数器类似,不过它能够记住过去n次的历史,在重复的跳转模式中表现优异。

不幸的是,分支预测是各个CPU厂商的核心竞争力之一,大多数优秀的分支预测技术也是重要的商业机密,于是在这个方面并没有太多可以深入的。Cloudflare最近发布了一篇博文深入测试了x86和ARM的M1上分支预测器的特征,有兴趣的读者可以看看。

去除分支语句

由于分支这实在是处理器不喜欢的东西,于是人们便想要尽量减少分支语句的使用。而以下这种情况很常见,在求取最大最小值或者是条件赋值的时候经常被使用(第1,2行):

cmp a, 7       ; a > 7 ?
mov c, b       ; b = c
cmovle d, b    ; if le, then b = d

于是人们就设计出了第3行这种指令。这样的指令是在特定条件下将d的值赋给b,而并不引入分支,只需要在条件不满足的时候不进行写回(commit/retire)就可以了。这种指令被称为条件转移指令,编译器中经常使用这种trick来避免进行跳转。

我们古老的x86架构一开始并不支持条件转移指令,MIPS、SPARC也不例外,而Alpha架构从设计之初就考虑了这类指令(RISC-V这样的新指令集当然也有)。ARM则是第一个采用全可预测指令的指令集,这一点很有趣,因为早期的ARM处理器通常采用很浅的流水线,分支预测的惩罚很小。

指令调度、寄存器重命名和乱序执行

如果分支和长延迟的指令会带来流水线气泡,那么能不能把这些气泡占据的处理器时间用来干有用的事情呢?为了达到这个目的,就需要引入乱序执行。乱序执行允许处理器将部分指令的顺序打乱,在执行长延迟指令的同时执行一些别的指令。

历史上有两种方式来达到乱序执行的目的:软件的和硬件的。

软件的途径很好理解,就是通过编译器与体系结构的强耦合,在编译阶段就生成好无相互依赖,易于处理器调度的指令。在编译阶段进行指令重排又被称为静态指令调度,优点是软件实现可以更灵活(众所周知,软件什么都能干),通常软件也可以有足够的存储空间来分析整个程序,因此可以获得更优的指令排布。当然缺点也是显而易见的,由于编译器需要深入地了解体系结构相关的信息,如指令延迟和分支预测惩罚等,对可移植性造成了很大的困难。因此现代处理器更加常用的是硬件方式。

硬件方式主要是通过寄存器重命名来消除读—读和写—写假依赖。寄存器重命名就是对不同指令调用的相同寄存器使用不同的物理硬件存储,在写回阶段再对这些指令和寄存器进行排序,这样这些假依赖就不再是产生流水线气泡的原因了。注意,写—读依赖是真正的数据依赖,虽然像前递这样的技术可以降低延迟,但是并没有能够解决这种依赖的办法。现代处理器中也并非仅仅只有如16个通用寄存器和32个浮点寄存器等等,通常都有成百上千的物理寄存器在CPU的片上。寄存器重命名的算法最有名的便是Tomasulo算法,有兴趣的读者可以搜索一下。

硬件方式的优点在于降低了编译器的体系结构耦合度,提高了软件编写的便捷性,通常硬件乱序执行的效果也不必软件的差。而缺点在于依赖分析和寄存器重命名都需要耗费宝贵的片上空间和电力,但对于性能的提升却没有相应的大。因此,在一些更加关注低功耗和成本的CPU中,会采用顺序执行,如ARM的低功耗产品线,Intel Atom等。

多核和超线程

我们之前讨论了各种指令集并行的方法,而很多时候它们的效果并不是很好,因为相当一部分的程序没有提供细粒度的并行。因此,制造更“宽”更“深”的处理器效果相当有限。

但是CPU的设计者又想了,如果本程序中没有足够并行的没有相互依赖的指令,那么不同的程序之间肯定是没有数据依赖的(指令级数据依赖),那么在同一个物理核心上同时运行两个线程,互相填补流水线的空缺,岂不美哉?这就叫做同步多线程(SMT),它提供了线程级的并行化。这种技术对于CPU以外的世界来说是透明的,就仿佛真的CPU数量多了一倍似的,因此现在人们也常说虚拟核心。

从硬件角度来说,同步多线程的实现需要将所有与运行状态有关的结构数量都翻倍,比如寄存器,PC计数器,MMU和TLB等等。幸运的是,这些结构并不是CPU的主要部分,最复杂的译码和分发器,运算器和缓存都是在两个线程之间共享的。

当然,真实的性能不可能翻倍,理论上限还是取决于运算器的数量,同步多线程只是能够将运算器更好地利用而已。因此在例如游戏画面生成这样地并行度本来就很高地任务中,SMT几乎没有任何地效果,反而因为偶尔地线程切换而带来一定地性能损失。

SMT处理器的指令流看起来大概是这样的:

SMT处理器的指令流

太好了!现在我们有了填满哪些流水线气泡的方法了,而且绝无任何风险。所以,30发射的处理器我们来啦!对吗?不幸的是,不对。

虽然IBM曾在它的产品中使用过8线程的核心,但是很快我们就会看到,现代处理器的瓶颈早已不单单是CPU本身了,访存延迟和带宽都成为了更加迫切需要解决的问题。而同时使用8个MMU,8个PC,8个TLB怎么看也不是一个缓存友好的做法。因此,现在已经很少听到有多于一个核心两个线程的处理器了。

数据并行——SIMD指令集

除了指令级并行和超线程,在现代处理器中还有一种并行化的设计——数据并行化。数据并行化的思想是将同一条指令不同数据进行并行化,而不是对不同的指令进行并行。所以使用数据并行化的指令集通常又称为SIMD指令集(单指令多数据),也有称为向量指令集的。

在超级计算机和高性能计算领域,SIMD指令集被大量的使用,因为通常科学计算会处理极多的数据而对于每个数据的操作并不复杂,而且基本没有相互依赖性。在现代的个人计算机中,SIMD指令集也大量的存在,哪怕是最为廉价的手机中,SIMD指令集也有它的身影。

SIMD指令集的工作原理就像下图所示的那样:

SIMD指令原理

从硬件的角度来说,实现这样的并行化并不难,这就像每次都执行同一个指令的超标量处理器,CPU设计厂商唯一需要做的就是增大寄存器的容量而已。Intel在过去20年正在不断地增大可以并行的向量长度,从SSE的128bits到AVX512的512bits。而ARM从ARMv8a开始便从NEON的128bits飞跃到了SVE的2048bits长度,甚至还支持可变长度。

现代的x86-64处理器都支持SSE指令集,所以现在的编译器如果编译64位平台的目标文件,会自动的将SSE指令集加入用于优化。由于SIMD指令集发展迅速,不少指令的延迟甚至和传统的标量命令不相上下,而且SSE指令集也拥有操作单个操作数的指令,现代编译器在默认情况下对于单个浮点数的操作也会使用SSE指令集而不是使用传统的x87浮点指令。另外,几乎所有的体系结构都拥有自己的SIMD指令集。

像渲染画面或者科学计算这种简单而重复的任务很适合SIMD指令集,事实上,GPU的工作原理也与SIMD类似。但不幸的是,在大多数普通(没有经过特别的思考而写出的)代码中,SIMD指令集并不能被很好的应用。现代编译器全部都有不同程度的循环自动向量化(使用SIMD指令集),但是当程序的编写者没有很好地考虑数据的依赖性和内存布局(马上就会谈到)时,编译器往往不能对代码进行什么优化。而幸运的是,通常通过简单的改动,就可以编译器明白某些循环是可以被优化的,进而大幅的提升程序的运行速度。

内存和内存墙

现代处理器实在是太快了,以至于它们大多数时候都在等待内存响应,而不是干正事。

——佚名(忘记出处了)

自从计算机发明以来,处理器的发展速度远远超过存储的发展速度,以下是一个对比图:

Performance of processors and memory through the years 1980-2010
处理器和内存速度对比

对于现代处理器来说,内存访问非常的昂贵。

  • 在1980年,CPU访问一次内存通常只需要一个周期。
  • 在2021年,CPU访问内存大约需要300-500个周期。

当我们考虑到,我们在CPU上使用了那么多种手段来压榨运算器,使IPC能够突破1,这样来看,内存就更慢了。以下是一个表格,展示了如果处理器周期看作是1秒,访问其它的存储器需要的时间。

事件延迟等效延迟
CPU周期0.2ns1s
L1缓存访问0.9ns4s
L2缓存访问3ns15s
L3缓存访问10ns50s
内存访问100ns8分钟
固态硬盘访问10-100us15-150小时
机械硬盘访问1-10ms2-18月
等效延迟表

可以看到,现代CPU实在是太快了,程序编写者现在要比过去花费更多的精力来使他们的程序能够充分利用CPU的性能,而不是卡在内存操作上。

为了解决这个严重的问题,处理器设计者们也想出了办法,也就是上面的表格中已经出现了的缓存。80年代的CPU由于没有内存墙的问题,所以基本都没有设计缓存。而现代CPU通常而言都有高达三级的缓存(某些低功耗和移动端的CPU只有两级),理解这些缓存是怎么样工作的有利于程序设计者写出更快的程序。

缓存

Cache这个词读音和Cash(现金)一样,而不是kay-sh、ca-shay或者Cake!

现代处理器为了解决内存墙,使用多级的缓存来避免内存延迟的影响。一个典型的缓存结构是这样的:

等级大小延迟物理位置
L1 cache32 KB4 cycles每个核心内部
L2 cache256 KB12 cycles每个die或每个核心
L3 cache6 MB~21 cycles整个处理器共享或每die共享
RAM4+ GB~117 cycles主板上的内存条上
缓存等级

令人高兴的是,现代处理器的缓存机制出奇的有效,L1缓存的命中率在大多数时候高达90%,这说明在大多数情况下内存访问的代价仅仅是几个周期而已。

缓存能够取得这么好的效果主要是因为程序具有很好的局部性。分为空间局部性和时间局部性。时间局部性说的是当程序访问一块内存时,很有可能接下来连续访问这一块内存。空间局部性是说程序访问一块内存,那么它很可能也许要访问附近的内存。为了利用好这样的局部性,内存中的数据是一块一块地从内存条上复制到缓存中的,这些快被称为缓存行

从硬件的角度来说,缓存的工作原理和键值对表很类似。Key就是内存的地址,而Value则是对应的数据。事实上Key并不一定是完整的地址,通常是地址的高位一部分,而低位被用来索引缓存本身。用物理地址和虚拟地址来作为Key都是可行的,也各有好坏(就像所有的事情一样)。使用虚拟地址的缺点是进程的上下文切换需要刷新缓存,这非常昂贵。使用物理地址的缺点则是每次查缓存都需要先查页表。因此现代处理器通常采用虚拟地址作为缓存索引,而使用物理地址作为缓存行的标记。这样的方法又被称作“虚拟索引——物理标记”缓存。

缓存冲突和关联度

理想状态下,缓存应当保存最近最常使用的数据,但是对于CPU上的硬件缓存来说,有效维护使用状态的算法不能满足严格的延迟要求(例如Linux内核页缓存使用的LRU,有兴趣可以看我前面的文章Linux内核页面置换算法),也难以用硬件实现,所以通常处理器使用简单的方法:每一个缓存行直接对应内存的几个位置。由于对应的几个位置不太可能同时访问,因此缓存是有效的。

这样的做法非常快速(本来缓存的设计目的就是这样的),但是当程序的确不断地来回访问同一个缓存行对应的不同位置的时候,缓存控制单元不得不反复从内存中装载数据,非常耗时,这被称为缓存冲突。解决办法就是不限制每一个内存区域只对应一个缓存行,而是对应几个,这个几个就被称作是缓存关联度

当然,最快的方法是每个内存区域对应一个缓存行,这被叫做直接映射缓存,而使用4个关联度的缓存被称作4通道关联缓存。内存可以装载到任意一个缓存行的缓存叫做全关联缓存。使用关联的缓存带来的好处是大大减少了缓存冲突而保持查询延迟在一个合理的范围内。这也是现代处理器通常使用的方法。

致谢

Modern Microprocessors: A 90-Minute Guide! 2016 By Jason Robert Carey Patterson

The compiler will optimize that away 2021 By RoyalSloth

The Intel Skylake Mobile and Desktop Launch, with Architecture Analysis 2015 By AnandTech

Skylake(Client) Microarchitecture 2020 By WikiChip

深入阅读

The post 现代处理器结构 first appeared on Easton Man's Blog.

Linux内核页面置换算法

作者 Easton Man
2021年4月3日 20:23

预计阅读时间: 9 分钟

最近来自Google的Yu Zhao向Linux内核提交了一个Patch,修改了内核内存管理模块中的页面置换算法,提出了多级LRU,本文带你了解页面置换算法和多级LRU的优势。

什么是页和页面置换

我们知道,在几乎所有的现代操作系统和处理器上都使用了内存分页机制。某些嵌入式操作系统和特殊情况下,处理器资源很紧张,不需要考虑进程间安全问题,甚至没有线程这一概念时,内存分页也就没有必要了。内存分页是为了虚拟内存机制而提出的,大多数现代操作系统上为了进程间的隔离,每个进程拥有自己独立的地址空间,这就是虚拟内存。虚拟内存需要与物理内存有一个映射,但是问题来了,这个映射表是需要占用内存空间的,尤其是64位处理器上,需要映射的地址空间非常庞大,如果对地址做一一映射,映射表将会比物理内存还要大。这时就需要对内存进行分页映射,即将内存分为一定大小,如4KB大小的页,这些页面与物理内存中的4KB大小的页面一一对应,这就是内存分页机制。但是页表仍然很大,现代体系结构中一般使用多级页表来大幅减少页表所占用的空间,例如Linux中的四级页表:

Linux四级页表

其中PGD、PUD等分别是一、二、三、四级页表项,Offset是页内部的地址。Linux在最新的Intel Xeon处理器上甚至使用了五级页表,具体处理器支持就不赘述了。值得注意的是,页表需要硬件支持,也就是CPU的内存管理单元(MMU)的支持。以下是一个Intel Xeon Gold 6230的MMU支持,可以看到40 bits physical,也就值它的MMU支持40位的硬件寻址,48 bits physical也就是目前Linux四级页表所支持的虚拟地址空间大小。

那页面置换又是什么?

页面置换也来自一个几乎所有的现代操作系统都拥有的功能——交换机制。当物理内存接近满的时候,操作系统为了使整个系统仍然可用,会将一部分不常使用的页面移到磁盘上,为更经常使用的页面腾出空间。当然,这样的做的代价是会降低性能,这个机制也不是万能的,当活跃使用的内存也借近物理内存大小的时候,系统依然会变得几乎不可用,不过至少这样的机制使得系统有机会自己恢复,而不是不得不由OOM Killer杀死一些进程。这个交换机制显然需要一个优秀的算法来判断哪一部分页面是“陈旧的”,这就引出了这篇文章的主题——页面置换算法。

页面置换算法

操作系统发展了50年,页面置换算法也在不断地改进,我们先来盘点一下曾经出现过的页面置换算法有哪一些。

  • 最优算法想得真美,甚至关于“最优”定义也取决于实际场景。
  • NRU(最近未使用)算法:一个简易的算法,维护一个链表,每次淘汰末端的页,每次读取时将页面移至表头。是可以接受的算法,实现简易,性能损耗小。
  • FIFO:维护一个队列,每次直接清理队列头的页面。几乎无性能开销,但是效果不好。
  • Second Chance FIFO:维护一个循环队列,每次清理访问标志位为0的页面,每次访问后将标志位置为1,这样只要周期内页面有访问,就会免遭清理,除非所有的页都为1。由于使用循环队列,内存操作较多,故一般使用下面一个算法。
  • 时钟算法:指针和环形链表/队列实现SCFIFO,消除了内存操作,也是较为常用的算法,效果较好,比较NRU有较大改善。
  • LRU(最近最少使用)算法:对于最优算法在大多数情况下很好的近似,效果优异,但是较难实现,目前的算法要不就是时间复杂度较高,要不就是空间复杂度较高。大多数时候还需要硬件支持。
  • NFU算法:由软件模拟的LRU,使用的频度Freq由系统时钟中断处理程序统计,效果较好,但是有一定的性能开销,且没有实现“最近”。
  • 老化算法:在NFU基础上加入频度老化,实现了“最近”,越久的使用次数权重越低。

Linux内核现在使用的页面置换算法是两级的软件LRU,也就是分为active和inactive类型的两个链表,并实现软件LRU(NFU)算法,由于基于硬件的LRU在当今的体系结构中无法实现,下文中我们把这一种算法简称为LRU。

两级LRU的问题

TL;DR

现行的算法CPU开销太大,而且经常做出错误的决策。

粒度

Active/Inactive这两种分类在现在的硬件环境下,实在是一个相当粗的粒度。要知道,Linux统治的服务器领域中,内存上T的并不少见,甚至是很常见。Intel近几年推广的傲腾内存更是对内存调度提出了更高的要求。这个活动/非活动的分类并不是一个好的分类方法,像文件存取这样的情境下,经常会有周期性的内存操作,而这时页面会在两个表中来回移动,并不能很好表现这个页面是否是活跃的。而许多正在执行的可执行文件的代码段常常被移除,仅仅是因为它们在相当短的一段时间内没有被使用。这导致了在安卓和ChromeOS上交互进程的反应延迟。

倒排页表扫描的性能问题

由于LRU算法需要周期性地根据倒排页表(rmap)以更新页面的访问计数器,但是,rmap拥有相当复杂的数据结构,局部性很差,导致在扫描rmap的时候CPU缓存命中率很不理想。现代CPU的缓存命中率对执行速度由极大的影响,缓存命中率低下可以使CPU的执行速度下降到峰值的10%甚至更低。

多级LRU

Yu Zhao最近提交的Patch中提出了Multigenerational LRU算法,旨在解决现在内核使用的两级LRU的问题。这个多级LRU借鉴了老化算法的思路,按照页面的生成(分配)时间将LRU表分为若干Generation。在LRU页面扫面的时候,使用增量的方式扫描,根据周期内访问过的页面对页表进行扫描,除非这段时间内访问的内存分布非常稀疏,通常页表相对于倒排页表有更好的局部性,进而可以提升CPU的缓存命中率。

在邮件列表中Yu Zhao说明了他使用更改后的算法在几个模拟场景中的情况:

  • Android:减少了18%的low memory kill,进而减少了16%的冷启动。
  • Borg(Google的云资源编排系统):活跃内存占用降低了。
  • ChromeOS:非活跃页面减少了96%的内存占用,减少了59%的OOM Kill。

结语

Linux内核页面置换算法的修改影响非常巨大,哪怕只是“微小”的调整。因为Linux的使用场景并不仅限于常用的几种情况,因此这个Patch需要经过相当长的一段讨论和各个开发者的测试才可能最终被合并进入主线。虽然大部分的内核开发者还没有发表对此的意见,但已有部分人对这个新算法持有积极的态度和较好的评价,所以我也认为这个Patch将会最终进入Linux内核主线。

[2023-01-07] Update: MLRU 已在 Linux 6.2 合并进入主线内核

深入阅读

The post Linux内核页面置换算法 first appeared on Easton Man's Blog.
❌
❌