AMD64 缓存优化策略 - 堆栈、符号、变量和字符串表
Strategy for AMD64 cache optimization - stacks, symbols, variables and strings tables
简介
我将在 GNU 汇编程序 (GAS) 中为 Linux x86-64(特别是针对我的 table 上的 AMD Ryzen 9 3900X)编写自己的 FORTH“引擎”。
(如果成功,我可能会使用类似的想法为复古 6502 和类似的 home-brewed 计算机制作固件)
我想添加一些有趣的调试功能,例如在带有附加字符串的“NOP words”的编译代码中保存注释,这在 运行 时间内什么都不做,但是当 disassembling/printing 在已经定义的单词中,它也会打印那些评论,所以它不会丢失所有 headers (a b -- c) 和类似的评论(这里有这个特别的小技巧),我将能够尝试定义新的带有文档的单词,然后以某种不错的方式打印所有定义,并从中创建新的库,我认为这很好。 (并且已经切换到忽略“生产发布”的评论)
我在这里读了太多的优化,我无法在几周内理解所有这些,所以我会先进行微优化,直到它遇到性能问题,然后我才会开始分析。
但我想从至少体面的架构决策开始。
我还明白了什么:
- 如果程序 运行 主要来自 CPU 缓存,而不是来自内存,那就太好了
- 缓存以某种方式“自动”填充,但相关 data/code 紧凑且尽可能接近可能会有很大帮助
- 我确定了一些适合缓存的区域和一些不太好的区域 - 我按重要性排序:
- 汇编代码 - 引擎和“+”等基本词 - 一直使用(固定大小,.text 部分)
- 两个堆栈——也一直在使用(动态的,我可能会使用 rsp 作为数据堆栈并独立实现 return 堆栈——还不确定,哪个是“本地的”,哪个是“模拟的”)
- 第四字节码 - 定义和编译的词 - 在运行时间使用,当速度很重要时(仍在增加大小)
- 变量、常量、字符串、其他内存分配(在运行时间使用)
- 单词名称(“DUP”、“DROP”- 仅在编译阶段定义新单词时使用)
- 评论(每天使用一条左右)
问题:
因为有很多“堆”在增长(好吧,没有使用“免费”,所以它也可能是堆栈,或者堆栈在增长)(还有两个堆栈在向下增长)我不确定如何实现它,所以 CPU 缓存将以某种方式适当地覆盖它。
我的想法是使用一个“大堆”(并在需要时用 brk() 增加它),然后在其上分配大块的对齐内存,在每个块中实现“小堆”并将它们扩展到旧的填满后又一大块。
我希望,缓存会自动获取最常用的块,首先在大部分时间保留它,而较少使用的块将大部分被缓存忽略(分别它只会占据一小部分并被读取和踢出一直都在外面),但也许我没做对。
但也许对此有更好的策略?
您进一步阅读的第一站应该是:
- What Every Programmer Should Know About Memory?回复:缓存
- https://agner.org/optimize/ 回复:关于编写高效 asm 的所有其他内容。
- https://uops.info/ 以获得更好版本的 Agner Fog 指令表。
- 另请参阅 https://whosebug.com/tags/x86/info
中的其他链接
so I will put out microoptimalisation until it will suffer performance problems and then I will start with profiling.
是的,开始尝试可能很好,这样您就可以使用 HW 性能计数器进行分析,这样您就可以将您正在阅读的有关性能的内容与实际发生的事情相关联。因此,在您过度优化整体设计理念之前,您会获得一些您尚未想到的可能细节的想法。您可以从 非常 小规模的东西开始学习很多关于 asm 微优化的知识,比如某个地方没有任何复杂分支的单个循环。
由于现代 CPUs 使用分离的 L1i 和 L1d 缓存以及一级 TLB,将代码和数据放在一起并不是一个好主意。 (尤其不是读写数据;自修改代码由flushing the whole pipeline on any store too near any code that's in-flight anywhere in the pipeline处理。)
相关: - 他们没有,只有经过混淆的 x86 程序才这样做。 (ARM 代码有时会混合 code/data,因为 PC 相关负载在 ARM 上的范围有限。)
是的,确保所有数据分配都在附近应该对 TLB 局部性有利。硬件通常使用伪 LRU allocation/eviction 算法,该算法通常可以很好地将热数据保存在缓存中,通常不值得尝试手动 clflushopt
任何东西来帮助它。软件预取也很少有用,尤其是在数组的线性遍历中。如果您知道以后要从哪里访问相当多的指令,这有时是值得的,但是 CPU 无法轻易预测。
AMD 的 L3 缓存可能会使用 adaptive replacement like ,以尝试保留更多可重用的行,而不是让它们轻易地被那些不会被重用的行驱逐。但是 Zen2 的 512kiB L2 以 Forth 标准比较大;您可能不会有大量的二级缓存未命中。 (并且无序执行可以做很多事情来隐藏 L1 未命中/L2 命中。甚至隐藏 L3 命中的一些延迟。)当代英特尔 CPU 通常使用 256k L2 缓存;如果您是通用现代 x86 的缓存阻塞,128kiB 是一个不错的块大小选择,假设您可以写入然后在获得 L2 命中时再次循环。
在像 Zen2 (https://en.wikichip.org/wiki/amd/microarchitectures/zen_2#Architecture) 或 Skylake 这样的现代 x86 上,L1i 和 L1d 缓存(每个 32k),甚至 uop 缓存(高达 4096 微指令,每条指令大约 1 或 2 个)是与 Forth 实现相比相当大;可能大部分时间所有内容都会命中 L1 缓存,当然还有 L2。是的,代码局部性通常很好,但是 L2 缓存比典型 6502 的整个内存还多,您真的不用担心太多:P
解释器更关心的是分支预测,但幸运的是 Zen2(以及自 Haswell 以来的英特尔)有 TAGE 预测器,即使只有一个“大中央调度”分支,它也能很好地学习间接分支的模式:分支预测和解释器的性能 -
不要相信民间传说
简介
我将在 GNU 汇编程序 (GAS) 中为 Linux x86-64(特别是针对我的 table 上的 AMD Ryzen 9 3900X)编写自己的 FORTH“引擎”。
(如果成功,我可能会使用类似的想法为复古 6502 和类似的 home-brewed 计算机制作固件)
我想添加一些有趣的调试功能,例如在带有附加字符串的“NOP words”的编译代码中保存注释,这在 运行 时间内什么都不做,但是当 disassembling/printing 在已经定义的单词中,它也会打印那些评论,所以它不会丢失所有 headers (a b -- c) 和类似的评论(这里有这个特别的小技巧),我将能够尝试定义新的带有文档的单词,然后以某种不错的方式打印所有定义,并从中创建新的库,我认为这很好。 (并且已经切换到忽略“生产发布”的评论)
我在这里读了太多的优化,我无法在几周内理解所有这些,所以我会先进行微优化,直到它遇到性能问题,然后我才会开始分析。
但我想从至少体面的架构决策开始。
我还明白了什么:
- 如果程序 运行 主要来自 CPU 缓存,而不是来自内存,那就太好了
- 缓存以某种方式“自动”填充,但相关 data/code 紧凑且尽可能接近可能会有很大帮助
- 我确定了一些适合缓存的区域和一些不太好的区域 - 我按重要性排序:
- 汇编代码 - 引擎和“+”等基本词 - 一直使用(固定大小,.text 部分)
- 两个堆栈——也一直在使用(动态的,我可能会使用 rsp 作为数据堆栈并独立实现 return 堆栈——还不确定,哪个是“本地的”,哪个是“模拟的”)
- 第四字节码 - 定义和编译的词 - 在运行时间使用,当速度很重要时(仍在增加大小)
- 变量、常量、字符串、其他内存分配(在运行时间使用)
- 单词名称(“DUP”、“DROP”- 仅在编译阶段定义新单词时使用)
- 评论(每天使用一条左右)
问题:
因为有很多“堆”在增长(好吧,没有使用“免费”,所以它也可能是堆栈,或者堆栈在增长)(还有两个堆栈在向下增长)我不确定如何实现它,所以 CPU 缓存将以某种方式适当地覆盖它。
我的想法是使用一个“大堆”(并在需要时用 brk() 增加它),然后在其上分配大块的对齐内存,在每个块中实现“小堆”并将它们扩展到旧的填满后又一大块。
我希望,缓存会自动获取最常用的块,首先在大部分时间保留它,而较少使用的块将大部分被缓存忽略(分别它只会占据一小部分并被读取和踢出一直都在外面),但也许我没做对。
但也许对此有更好的策略?
您进一步阅读的第一站应该是:
- What Every Programmer Should Know About Memory?回复:缓存
- https://agner.org/optimize/ 回复:关于编写高效 asm 的所有其他内容。
- https://uops.info/ 以获得更好版本的 Agner Fog 指令表。
- 另请参阅 https://whosebug.com/tags/x86/info 中的其他链接
so I will put out microoptimalisation until it will suffer performance problems and then I will start with profiling.
是的,开始尝试可能很好,这样您就可以使用 HW 性能计数器进行分析,这样您就可以将您正在阅读的有关性能的内容与实际发生的事情相关联。因此,在您过度优化整体设计理念之前,您会获得一些您尚未想到的可能细节的想法。您可以从 非常 小规模的东西开始学习很多关于 asm 微优化的知识,比如某个地方没有任何复杂分支的单个循环。
由于现代 CPUs 使用分离的 L1i 和 L1d 缓存以及一级 TLB,将代码和数据放在一起并不是一个好主意。 (尤其不是读写数据;自修改代码由flushing the whole pipeline on any store too near any code that's in-flight anywhere in the pipeline处理。)
相关:
是的,确保所有数据分配都在附近应该对 TLB 局部性有利。硬件通常使用伪 LRU allocation/eviction 算法,该算法通常可以很好地将热数据保存在缓存中,通常不值得尝试手动 clflushopt
任何东西来帮助它。软件预取也很少有用,尤其是在数组的线性遍历中。如果您知道以后要从哪里访问相当多的指令,这有时是值得的,但是 CPU 无法轻易预测。
AMD 的 L3 缓存可能会使用 adaptive replacement like
在像 Zen2 (https://en.wikichip.org/wiki/amd/microarchitectures/zen_2#Architecture) 或 Skylake 这样的现代 x86 上,L1i 和 L1d 缓存(每个 32k),甚至 uop 缓存(高达 4096 微指令,每条指令大约 1 或 2 个)是与 Forth 实现相比相当大;可能大部分时间所有内容都会命中 L1 缓存,当然还有 L2。是的,代码局部性通常很好,但是 L2 缓存比典型 6502 的整个内存还多,您真的不用担心太多:P
解释器更关心的是分支预测,但幸运的是 Zen2(以及自 Haswell 以来的英特尔)有 TAGE 预测器,即使只有一个“大中央调度”分支,它也能很好地学习间接分支的模式:分支预测和解释器的性能 - 不要相信民间传说