【问题标题】:Why does instruction cache alignment improve performance in set associative cache implementations?为什么指令缓存对齐可以提高集合关联缓存实现中的性能?
【发布时间】:2019-12-30 06:36:48
【问题描述】:

我有一个关于指令缓存对齐的问题。我听说对于微优化,对齐循环以使其适合高速缓存行可以稍微提高性能。我不明白为什么这样做会起作用。

我了解缓存命中的概念及其对计算速度的重要性。

但似乎在集合关联缓存中,相邻的代码块不会映射到同一个缓存集合。因此,如果循环穿过一个代码块,CPU 仍应获得缓存命中,因为该相邻块尚未被前一个块的执行驱逐。这两个块都可能在循环期间保持缓存。

所以我能想到的是,如果对齐可以帮助的说法是真的,那一定是来自某种其他的影响。

切换缓存行有成本吗?

缓存命中是否存在差异,一种是命中,另一种是命中当前正在读取的同一缓存行?

【问题讨论】:

    标签: cpu-architecture memory-alignment cpu-cache micro-optimization


    【解决方案1】:

    将整个函数(或函数的热点部分,即通过它的快速路径)保留在更少的缓存行中可减少 I-cache 占用空间。因此它可以减少缓存未命中的数量,包括在大部分缓存都处于冷态的启动时。在缓存行结束之前结束循环可能会给硬件预取时间以获取下一个。

    访问 L1i 缓存中存在的任何行都需要相同的时间。 (除非您的缓存使用 way-prediction:这会引入“慢速命中”的可能性。有关该想法的提及和简要说明,请参见 these slides。显然 MIPS r10k 的 L2 缓存使用了它,并且Alpha 21264 的 L1 指令缓存也是如此,在其 2 路关联 64kiB L1i 中具有“分支目标”与“顺序”方式。或者查看任何在您使用 Google 搜索时出现的学术论文cache way prediction 就像我一样。)


    除此之外,影响并不在于高速缓存行边界,而是超标量 CPU 中对齐的指令获取块。你是对的,影响不是来自你正在考虑的事情。

    有关超标量(和无序)执行的介绍,请参阅 Modern Microprocessors A 90-Minute Guide!

    许多超标量 CPU 使用对其 I-cache 的对齐访问来执行其第一阶段的指令获取。让我们通过考虑具有 4 字节指令宽度1 和 4 宽 fetch/decode/exec 的 RISC ISA 来简化。 (例如 MIPS r10k,虽然 IDK 如果我要弥补的其他一些东西完全反映了那个微架构)。

       ...
     .top_of_loop:
        insn1                ; at address 16*n + 12
          ; 16-byte boundary here
        insn2                ; at address 16*n + 0
        insn3                ; at address 16*n + 4
        b  .top_of_loop      ; at address 16*n + 8
    
        ... after loop       ; at address 16*n + 12
        ... after loop       ; at address 16*n + 0
    

    在没有任何类型的循环缓冲区的情况下,获取阶段每次执行时都必须从 I-cache 中获取循环指令。但这每次迭代至少需要 2 个周期,因为该循环跨越两个 16 字节对齐的提取块。它无法在一次未对齐的提取中提取 16 个字节的指令。

    但是如果我们对齐循环的顶部,它可以在单个循环中获取,如果循环体没有其他瓶颈,则允许循环以 1 个循环/迭代运行。

       ...
        nop                  ; at address 16*n + 12         ; NOP padding for alignment
     .top_of_loop:       ; 16-byte boundary here
        insn1                ; at address 16*n + 0
        insn2                ; at address 16*n + 4
        insn3                ; at address 16*n + 8
        b  .top_of_loop      ; at address 16*n + 12
    
        ... after loop       ; at address 16*n + 0
        ... after loop       ; at address 16*n + 4
    

    对于不是 4 条指令的倍数的更大循环,仍然会在某处进行部分浪费的提取。不过,通常最好它不是循环的顶部。尽早将更多指令送入流水线有助于 CPU 发现和利用更多指令级并行性,以处理在指令获取上没有纯粹瓶颈的代码。

    一般来说,将分支目标(包括函数入口点)对齐 16 个可能是一个胜利(代价是较低的代码密度带来的更大的 I-cache 压力)。如果您在 1 或 2 条指令内,一个有用的权衡可以是填充到 16 的下一个倍数。例如所以在最坏的情况下,一个 fetch 块至少包含 2 或 3 条有用的指令,而不仅仅是 1 条。

    这就是 GNU 汇编器支持 .p2align 4,,8 的原因:如果距离 8 个字节或更近,则填充到下一个 2^4 边界。事实上,GCC 确实将该指令用于某些目标/架构,具体取决于调整选项/默认值。

    在非循环分支的一般情况下,您也不想在缓存行的末尾附近跳转。那么你可能马上就会有另一个 I-cache 未命中。


    脚注 1:

    该原理也适用于具有可变宽度指令的现代 x86,至少当它们解码后的 uop 缓存未命中迫使它们实际从 L1I 缓存中获取 x86 机器代码时。并且适用于较旧的超标量 x86,例如 Pentium III 或 K8,没有 uop 缓存或环回缓冲区(无论对齐如何,都可以使循环高效)。

    但是 x86 解码非常困难,需要多个流水线阶段,例如对于一些简单的find指令边界,然后将指令组提供给解码器。如果预解码可以赶上,只有初始提取块对齐,阶段之间的缓冲区可以隐藏解码器的气泡。

    https://www.realworldtech.com/merom/4/ 展示了 Core2 前端的详细信息:16 字节获取块,与 PPro/PII/PIII 相同,馈送可扫描多达 32 字节并查找多达 6 条指令之间的边界的预解码阶段IIRC。然后将另一个缓冲区提供给完整的解码阶段,该阶段可以将多达 4 条指令(5 条带有 test 或 cmp + jcc 的宏融合)解码为多达 7 条微指令......

    Agner Fog's microarch guide 提供了一些关于优化 x86 asm 以解决 Pentium Pro/II 与 Core2 / Nehalem 与 Sandybridge 系列以及 AMD K8/K10 与 Bulldozer 与 Ryzen 的提取/解码瓶颈的详细信息。

    现代 x86 并不总是从对齐中受益。代码对齐会产生影响,但它们通常并不简单,也不总是有益的。事物的相对对齐可能很重要,但通常对于诸如哪些分支在分支预测器条目中相互别名,或者 uop 如何打包到 uop 缓存中。

    【讨论】:

    • @Aaron:不是预取,只是宽取以支持超标量执行。 (您通常需要比后端更广泛的获取,以帮助确保前端成为瓶颈的时间更少)。 “预取”是指任何需求加载之前启动缓存。
    • @Aaron:但是,是的,在“正常”缓存中,获取任何存在的行都需要相同的时间。 (一些缓存使用方式预测,这可能会使这不正确,但这不是正常优化建议的原因。)
    • 这里有紫外线。对齐效果甚至适用于 uop 高速缓存,例如,因为指令在每个周期只能从一个集合传送。因此,任何跨越两组的循环每次迭代的运行速度都不能超过 2 个周期 - 类似于您给出的 icache 示例。在 x86 上,uop 缓存集边界对应于内存中指令布局,与 icache 相同,但可能具有不同的粒度(Skylake 上为 64B,Haswell 上为 32B)。 Si 对齐也可以证明对 uop 缓存有益。
    • @BeeOnRope:是的,但这仅适用于 Skylake 客户端/服务器和 Kaby Lake,其中环回缓冲区 (LSD) 被微码禁用。 Broadwell 和早期都很好,Coffee Lake 和以后都很好。 (en.wikichip.org/wiki/intel/microarchitectures/…)。如果循环很小并且适合 uop 缓存,则可以从 LSD 运行微小的循环。
    • @PeterCordes - 对,2:1 小循环示例可能通常不适用于那些具有功能 LSD 的 uarches。我认为尽管仍然有很多情况下 LSD 没有启动,并且 uop 缓存对齐很重要,例如,带有禁用 LSD 的指令的循环,有很多跳转的循环,一个较大的循环中的小行程计数循环,这样 LSD 就不会启动,等等。
    猜你喜欢
    • 2013-09-27
    • 1970-01-01
    • 2021-02-24
    • 1970-01-01
    • 1970-01-01
    • 2012-02-27
    • 1970-01-01
    • 2011-07-01
    • 1970-01-01
    相关资源
    最近更新 更多