动态生成代码是众所周知的技术,例如加快interpreted languages,domain-specific languages等等。无论您是想要工作low-level(接近1:1与汇编),还是high-level,您都可以找到帮助您的图书馆。
请注意自修改代码和动态生成代码之间的区别。前者意味着已执行的某些代码将被部分修改,然后再次执行。后者意味着某些代码(在磁盘上的进程二进制文件中不是静态存在的)被写入内存然后执行(但不一定会被修改)。区别可能在下面很重要,或者仅仅因为人们将自修改代码视为气味,但动态生成的代码是一种很好的性能技巧。
通常的用例是生成的代码将被执行多次。这意味着重点通常在于生成代码的效率,在较小程度上是编译时间,最重要的是实际编写代码的机制,使其可执行并开始执行。
但是,想象一下,您的用例是生成将执行一次的代码,并且这是没有循环的直线代码。生成代码的“编译”过程非常快(接近memcpy
速度)。在这种情况下,将代码写入内存并执行它的实际机制对性能变得很重要。
例如,执行的代码总量可以是10或更多GB。很明显,你不想只是在没有重复使用的情况下将全部写入一个巨大的缓冲区:这意味着将10GB写入内存,也许还要读取10GB(取决于生成和执行是如何交错的)。相反,你可能想要使用一些合理大小的缓冲区(比如适合L1或L2缓存):写出缓冲区的代码,执行它,然后用下一个代码块覆盖缓冲区,等等。 / p>
问题是,这似乎引起了自修改代码的幽灵。虽然“覆盖”已完成,但您仍然会覆盖已作为指令执行的某一点的内存。新编写的代码必须以某种方式从L1D到L1I以及associated performance hit is not clear。特别是,有been reports只是写入已执行的代码区域可能会受到100个周期的惩罚,并且写入次数可能很重要。
在x86上生成大量动态生成的直线代码并执行它的最佳方法是什么?
答案 0 :(得分:1)
我认为你不必要地担心。您的情况更像是一个进程退出并且其页面被重用于另一个进程(加载了不同的代码),这不应该导致自修改代码惩罚。它与进程写入自己的代码页时不一样。
当已经预取或解码到跟踪高速缓存时,自修改代码惩罚很重要。我认为,在代码生成器开始用下一位覆盖它时,任何生成的代码都不可能仍然在预取队列或跟踪缓存中(除非代码生成器很简单)。
以下是我的建议:将页面分配到L2的某些部分(如Peter所建议的),用代码填充它们并执行它们。然后在下一个更高的虚拟地址映射相同的页面,并用下一部分代码填充它们。您将获得读取和写入的缓存命中的好处,但我不认为您将获得任何自修改代码惩罚。您将使用10 GB的虚拟地址空间,但继续使用相同的物理页面。
每次开始执行修改的指令之前,请使用CPUID之类的序列化操作,如英特尔SDM的8.1.3和11.6节所述。
答案 1 :(得分:0)
我不确定通过使用大量的直线代码而不是带有循环的小代码来获得更多性能,因为在指令缓存中持续抖动这么长时间会产生很大的开销,并且在过去的几年中,条件跳跃的开销已经变得更好。当英特尔按照这些方针提出要求时,我很怀疑,他们的一些陈述相当夸张,但在常见情况下它已经有了很大的改进。如果为了简单起见,你仍然可以总是避免调用指令,即使对于树递归函数,也可以通过在最坏的情况下有效地模拟“堆栈”(可能是“堆栈”)来实现“堆栈”。
这留下了两个我能想到的理由,你想要坚持只在现代计算机上执行过一次的直线代码:1)它太复杂,无法弄清楚如何用更少的东西来表达需要计算的内容使用跳转的代码,或2)它是一个非常异构的问题,实际上需要这么多代码。 #2在实践中非常罕见,尽管在计算机理论意义上是可能的;我从来没有遇到过这样的问题。如果它是#1并且问题是如何有效地将跳转编码为短跳转或近跳转,there are ways。 (我最近刚刚回到x86-64机器代码生成的侧面项目,经过多年没有触及我的汇编器/链接器,但它还没有准备好使用。)
无论如何,有点难以知道绊脚石是什么,但我怀疑如果你能想出一种方法来避免生成千兆字节的代码,你会得到更好的性能,即使它在纸面上看起来不是最理想的。无论哪种方式,通常最好尝试几种选择,如果不清楚的话,看看哪种方法最适合实验。我有时会发现令人惊讶的结果。祝你好运!