首页 > 解决方案 > AMD64缓存优化策略——栈、符号、变量和字符串表

问题描述

介绍

我将在 GNU 汇编器 (GAS) 中为 Linux x86-64 编写我自己的 FORTH“引擎”(特别是针对我桌子上的 AMD Ryzen 9 3900X)。

(如果成功的话,我可能会用类似的想法为retro 6502和类似的自制电脑制作固件)

我想添加一些有趣的调试功能,如将已编译代码的注释保存在带有附加字符串的“N​​OP words”中,这在运行时不会做任何事情,但是当反汇编/打印出已经定义的单词时,它也会打印那些注释,所以它不会丢失所有的标题(ab-c)和评论(这里有这个特别的小技巧),我可以尝试用文档定义新单词,然后以一些好的方式打印所有定义并制作新的那些我认为很好的图书馆。(并切换到忽略“生产版本”的评论)

我在这里阅读了太多关于优化的内容,几周后我无法理解所有这些内容,因此我将推出微优化,直到它遇到性能问题,然后我将从分析开始。

但我想从至少体面的架构决策开始。

我还明白了什么:


问题:

由于有很多“堆”长大(嗯,没有使用“免费”,所以它也可能是堆栈,或者堆栈长大)(以及两个向下增长的堆栈)我不确定如何实现它,所以CPU缓存会以某种方式很好地覆盖它。

我的想法是使用一个“大堆”(并在需要时使用 brk() 增加它),然后在其上分配大块对齐的内存,在每个块中实现“较小的堆”,并在旧的已满。

我希望,缓存会自动获得最常用的块,并在大多数情况下首先保留它,而较少使用的块将大部分被缓存忽略(分别它只会占用一小部分并一直被读取和踢出) ,但也许我做的不对。

但也许有更好的策略呢?

标签: assemblyoptimizationx86-64gnu-assemblerforth

解决方案


您进一步阅读的第一站可能应该是:

所以我会推出微优化,直到它遇到性能问题,然后我将从分析开始。

是的,开始尝试一些东西可能很好,这样你就可以使用硬件性能计数器来分析一些东西,这样你就可以将你所读到的关于性能的东西与实际发生的事情联系起来。因此,在您深入优化整体设计理念之前,您可以获得一些您尚未想到的可能细节的想法。你可以从非常小的规模开始学习很多关于 asm 微优化的知识,比如在某个地方没有任何复杂分支的单个循环。


由于现代 CPU 使用分离的 L1i 和 L1d 缓存以及一级 TLB,因此将代码和数据彼此相邻放置并不是一个好主意。(尤其不是读写数据;自修改代码是通过在任何存储中刷新整个管道来处理的,该存储太靠近管道中任何运行中的任何代码。)

相关:为什么编译器将数据放在 PE 和 ELF 文件的 .text(code) 部分以及 CPU 如何区分数据和代码?- 他们没有,只有经过混淆的 x86 程序才能做到这一点。(ARM 代码有时会混合代码/数据,因为 PC 相关负载在 ARM 上的范围有限。)


是的,确保您的所有数据分配都在附近应该有利于 TLB 局部性。硬件通常使用伪 LRU 分配/驱逐算法,该算法通常可以很好地将热数据保存在缓存中,通常不值得尝试手动clflushopt进行任何帮助。软件预取也很少有用,尤其是在数组的线性遍历中。如果您知道以后要在哪里访问很多指令,有时这是值得的,但 CPU 无法轻易预测。

AMD 的 L3 缓存可能会像Intel 一样使用自适应替换,以尝试保留更多可重用的行,而不是让它们轻易地被那些往往不会被重用的行驱逐。但是 Zen2 的 512kiB L2 以 Forth 标准来说是比较大的;您可能不会有大量的二级缓存未命中。(乱序执行可以做很多事情来隐藏 L1 未命中/L2 命中。甚至隐藏 L3 命中的一些延迟。)当代 Intel CPU 通常使用 256k L2 缓存;如果您正在为通用的现代 x86 进行缓存阻塞,那么 128kiB 是一个不错的块大小选择,假设您可以写入,然后在获得 L2 命中时再次循环。

L1i 和 L1d 缓存(每个 32k),甚至 uop 缓存(高达 4096 uop,每条指令大约 1 或 2 个),在像 Zen2 这样的现代 x86 上(https://en.wikichip.org/wiki/amd/microarchitectures /zen_2#Architecture ) 或 Skylake,与 Forth 实现相比相当大;可能大部分时间所有东西都会在L1缓存中命中,当然还有L2。是的,代码局部性通常很好,但是 L2 缓存比典型 6502 的整个内存多,你真的不用担心:P


解释器更关心的是分支预测,但幸运的是 Zen2(以及自 Haswell 以来的英特尔)有 TAGE 预测器,即使有一个“大中央调度”分支,也能很好地学习间接分支的模式:分支预测和解释器的性能 - Don不要相信民间传说


推荐阅读