我正在尝试加速可变位宽整数压缩方案,并且我对动态生成和执行汇编代码感兴趣。目前很多时间都花在了错误预测的间接分支上,并且根据发现的一系列位宽生成代码似乎是避免这种惩罚的唯一方法。
一般技术被称为“子程序线程”(或“调用线程”,尽管这也有其他定义)。目标是利用处理器有效的调用/调用预测以避免停顿。该方法在这里得到了很好的描述: 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
在实际使用中,它会是一个较长的调用序列,具有足够的长度,每个序列可能是唯一的,并且只调用一次。生成和调用代码在此处和其他地方都有很好的记录。但是除了简单的“不要这样做”或经过深思熟虑的“有龙”之外,我还没有找到太多关于效率的讨论。甚至英特尔文档也大多笼统地说:
8.1.3 处理自修改和交叉修改代码
处理器将数据写入当前正在执行的代码段以将该数据作为代码执行的行为称为自修改代码。IA-32 处理器在执行自我修改的代码时表现出特定于模型的行为,具体取决于代码已被修改的当前执行指针提前多远。...自修改代码将以比非自修改或普通代码更低的性能水平执行。性能恶化的程度将取决于修改频率和代码的具体特性。
11.6 自修改代码
对当前缓存在处理器中的代码段中的内存位置的写入会导致相关的缓存行(或多个行)无效。此检查基于指令的物理地址。此外,P6 系列和 Pentium 处理器检查对代码段的写入是否会修改已预取执行的指令。如果写入影响预取指令,则预取队列无效。后一种检查基于指令的线性地址。对于 Pentium 4 和 Intel Xeon 处理器,在目标指令已解码并驻留在跟踪缓存中的代码段中写入或窥探指令会使整个跟踪缓存无效。
虽然有一个性能计数器来确定是否发生了坏事(C3 04 MACHINE_CLEARS.SMC: Number of self-modifying-code machine clears detected)我想知道更多细节,特别是对于 Haswell。我的印象是,只要我可以提前足够远地编写生成的代码,指令预取还没有到达那里,并且只要我不通过修改同一页面上的代码来触发 SMC 检测器(四分之一 -页?)作为当前正在执行的任何内容,那么我应该获得良好的性能。但所有细节似乎都非常模糊:太近了?多远才够远?
试图把这些变成具体的问题:
Haswell 预取器运行的当前指令之前的最大距离是多少?
Haswell“跟踪缓存”可能包含的当前指令后面的最大距离是多少?
Haswell 上 MACHINE_CLEARS.SMC 事件的实际循环惩罚是多少?
如何在预测循环中运行生成/执行循环,同时防止预取器吃掉自己的尾巴?
如何安排流程,以便始终“第一次看到”每条生成的代码,而不是踩到已经缓存的指令?