如何编写在现代x64处理器上高效运行的自修改代码?

Nat*_*urz 14 64-bit assembly intel self-modifying dispatch

我正在尝试加速可变位宽整数压缩方案,我有兴趣在运行中生成和执行汇编代码.目前,大量时间花在错误预测的间接分支上,并且基于所发现的一系列位宽生成代码似乎是避免这种惩罚的唯一方法.

一般技术被称为"子程序线程"(或"调用线程",尽管这也有其他定义).目标是利用处理器有效的呼叫/返回预测,以避免停顿.这种方法在这里有详细描述: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
Run Code Online (Sandbox Code Playgroud)

在实际使用中,它将是一个较长的一系列调用,具有足够的长度,每个系列可能是唯一的,只调用一次.无论是在这里还是在其他地方,生成和调用代码都有详细记录.但是,除了简单的"不要做"或者考虑周全的"有龙"之外,我还没有找到很多关于效率的讨论.即使是英特尔文档也主要说明了一般性:

8.1.3处理自我修改和交叉修改代码

处理器将数据写入当前正在执行的代码段以将该数据作为代码执行的动作称为自修改代码.IA-32处理器在执行自修改代码时表现出特定于模型的行为,具体取决于当前执行指针在代码被修改之前的距离....自修改代码将以比非自修改或普通代码更低的性能级别执行.性能恶化的程度取决于修改的频率和代码的特定特征.

11.6自修改代码

对当前在处理器中高速缓存的代码段中的存储器位置的写入导致相关联的高速缓存行(或多个行)无效.此检查基于指令的物理地址.此外,P6系列和奔腾处理器检查对代码段的写入是否可以修改已经预取执行的指令.如果写入影响预取指令,则预取队列无效.后一种检查基于指令的线性地址.对于Pentium 4和Intel Xeon处理器,代码段中的指令的写入或窥探(其中目标指令已经被解码并驻留在跟踪高速缓存中)使整个跟踪高速缓存无效.后一种行为意味着在Pentium 4和Intel Xeon处理器上运行时,自我修改代码的程序可能会导致性能严重下降.

虽然有一个性能计数器来确定是否发生了不好的事情(C3 04 MACHINE_CLEARS.SMC:检测到自修改代码机器清除的数量)我想知道更多细节,特别是Haswell.我的印象是,只要我能够提前编写生成的代码,指令预取还没有到达那里,并且只要我不通过修改同一页面上的代码来触发SMC检测器(四分之一 - 页面?)作为当前正在执行的任何内容,那么我应该获得良好的性能.但所有的细节看起来都非常模糊:距离过近有多近?到目前为止还远吗?

试图将这些问题转化为具体问题:

  1. Haswell预取器运行的当前指令前面的最大距离是多少?

  2. Haswell"跟踪缓存"可能包含的当前指令后面的最大距离是多少?

  3. Haswell上MACHINE_CLEARS.SMC事件的周期实际惩罚是多少?

  4. 如何在预测循环中运行生成/执行循环,同时防止预取程序吃掉自己的尾部?

  5. 我如何安排流程,以便每个生成的代码始终"第一次看到",而不是踩到已经缓存的指令?

joh*_*und 2

非常好的问题,但答案并不那么容易......最终的结论可能是实验——现代世界不同架构中的常见情况。

无论如何,您想要做的并不完全是自修改代码。过程“decode_x”将存在并且不会被修改。所以,缓存应该没有问题。

另一方面,为生成的代码分配的内存可能会从堆中动态分配,因此,地址将距离程序的可执行代码足够远。每次需要生成新的调用序列时,您都可以分配新的块。

多远才足够?我认为这还不是目前的情况。该距离可能应该是处理器缓存线的倍数,这样就不会那么大了。我有 64 字节(L1)之类的东西。在动态分配内存的情况下,您将有很多页面。

在我看来,这种方法的主要问题是生成的过程的代码只会执行一次。这样,程序就会失去缓存内存模型的主要优点——高效执行循环代码。

最后——这个实验看起来并不难做。只需在这两种变体中编写一些测试程序并测量性能即可。如果你发表这些结果,我会仔细阅读。:)