【发布时间】:2013-07-18 06:29:53
【问题描述】:
我正在尝试加速可变位宽整数压缩方案,并且我对动态生成和执行汇编代码感兴趣。目前很多时间都花在了错误预测的间接分支上,而根据发现的一系列位宽生成代码似乎是避免这种惩罚的唯一方法。
一般技术被称为“子程序线程”(或“调用线程”,尽管这也有其他定义)。目标是利用处理器有效的调用/调用预测以避免停顿。该方法在这里得到了很好的描述: http://webdocs.cs.ualberta.ca/~amaral/cascon/CDP05/slides/CDP05-berndl.pdf
生成的代码将只是一系列调用,然后是返回。如果有 5 个宽度为 [4,8,8,4,16] 的“块”,它看起来像:
call $decode_4
call $decode_8
call $decode_8
call $decode_4
call $decode_16
ret
在实际使用中,会是一个更长的调用序列,有足够的长度,每个序列都可能是唯一的,并且只调用一次。生成和调用代码在此处和其他地方都有很好的记录。但是除了简单的“不要这样做”或经过深思熟虑的“有龙”之外,我还没有找到太多关于效率的讨论。甚至Intel documentation 也大多是泛泛而谈:
8.1.3 处理自修改和交叉修改代码
处理器将数据写入当前执行代码的行为 以代码形式执行该数据的段被调用 自修改代码。 IA-32 处理器表现出特定于模型的行为 执行自我修改的代码时,取决于提前多远 当前执行指针代码已被修改。 ... 自修改代码的执行性能将低于 非自修改或正常代码。表现程度 恶化将取决于修改的频率和 代码的具体特征。
11.6 自修改代码
写入当前代码段中的内存位置 缓存在处理器中导致相关的缓存行(或行) 作废。此检查基于的物理地址 操作说明。此外,P6 系列和 Pentium 处理器检查 对代码段的写入是否可以修改具有 被预取执行。如果写入影响预取 指令,预取队列无效。后一项检查是 基于指令的线性地址。对于奔腾 4 和 英特尔至强处理器,代码中指令的写入或窥探 段,其中目标指令已被解码并驻留 在跟踪缓存中,使整个跟踪缓存无效。后者 行为意味着自我修改代码的程序可能会导致严重的 在 Pentium 4 和 Intel Xeon 上运行时性能下降 处理器。
虽然有一个性能计数器来确定是否发生了坏事(C3 04 MACHINE_CLEARS.SMC:检测到自修改代码机器清除次数)我想知道更多细节,特别是对于哈斯韦尔。我的印象是,只要我可以提前足够远地编写生成的代码,指令预取还没有到达那里,并且只要我不通过修改同一页面上的代码来触发 SMC 检测器(四分之一 -页?)作为当前正在执行的任何内容,那么我应该获得良好的性能。但所有细节似乎都非常模糊:太近了?多远才算够远?
试图把这些变成具体的问题:
-
当前指令前面的最大距离是多少 Haswell 预取器曾经运行过吗?
-
当前指令后面的最大距离是多少 Haswell 的“跟踪缓存”可能包含?
-
MACHINE_CLEARS.SMC 事件的实际循环惩罚是多少 在哈斯韦尔?
-
如何在预测的循环中运行生成/执行循环,而 防止预取器吃掉自己的尾巴?
-
如何安排流程,以便生成的每段代码都是 总是“第一次看到”而不是踩着指令 已经缓存了?
【问题讨论】:
-
确实很有趣的问题,但我觉得你列出的大部分观点都是研究问题,即我怀疑找出答案的方法是做适当的实验。
-
实验可能确实是最好的方法。如今,自修改代码被认为是邪恶的和设计缺陷。有时它可能是必要的,但由于人们避免它,所以不会有太多可用的信息......
-
实验肯定是必要的,但鉴于动态语言 JIT 编译的最新进展,我希望有一些更好的指南。但最大的区别在于 JIT 通常只执行一次,之后就不会被修改。
-
JIT 不必执行一次,您可以根据您达到的代码分析级别进行多级优化。
-
2. SnB 系列(包括 Haswell)中的 uop 缓存不是跟踪缓存。每种方式(最多 6 个微指令的组,Agner Fog 称之为一行)都知道它正在缓存的 x86 指令的地址范围。无条件跳转(如 JMP)确实结束了一条路/线,但 uops 在某种程度上必须是静态连续的。
标签: assembly 64-bit intel dispatch self-modifying