这个问题被认为是过于宽泛,可能是因为我在尝试“展示我的工作”而不是提出低成本问题时包含了研究。为了解决这个问题,让我用一句话总结整个问题(感谢@PeterCordes提供的短语):
如何从JIT生成的代码(我正在生成的)高效地调用(x86-64)预先编译的函数(我控制,可能超过2GB)?
仅凭这一点,我怀疑也会被认为是“过于宽泛”的。特别是缺少“您尝试过什么”。因此,我感到需要添加附加信息来显示我的研究/思考以及我所尝试的内容。以下是这种有点意识流的东西。
请注意,下面没有一个问题是我希望得到答案的问题;它们更多是修辞性的。它们的目的是说明为什么我无法回答上述问题(尽管我进行了研究,但在这个领域缺乏经验,无法像@PeterCordes那样做出确定性陈述,例如“分支预测隐藏了从内存中提取和检查函数指针的延迟,假设它预测良好。”)。还要注意,这里的Rust组件在很大程度上与此无关,因为这是一个汇编问题。我包含它的原因是预先编译的函数是用Rust编写的,所以我不确定是否有什么Rust可以做的(或者指示LLVM做的)可以在这种情况下具有优势。完全可以回答不考虑Rust的答案;实际上,我期望情况就是这样。
把以下内容视为数学考试后面的草稿:
注意:我在这里混淆了内部函数的术语。正如评论中指出的那样,“预先编译的函数”是一个更好的描述。以下我将缩写为AOTC函数。
我正在使用Rust编写JIT(尽管Rust仅涉及我的问题的一部分,但大部分与JIT约定相关)。我有在Rust中实现的AOTC函数,我需要能够从我的JIT生成的代码中call
这些函数。我的JITmmap(_,_,PROT_EXEC,MAP_ANONYMOUS | MAP_SHARED)
一些页面用于jitted代码。我有我的AOTC函数的地址,但不幸的是它们比32位偏移大得多。我现在正在尝试决定如何发出对这些AOTC函数的调用。我考虑了以下选项(这些不是要回答的问题,只是演示为什么我无法自己回答此SO线程的核心问题):
(Rust具体)以某种方式使Rust将AOTC函数放置在堆栈附近(也许在堆栈上?),以便
call
将在32位偏移内。不清楚是否可能使用Rust实现这一点(有一种方法可以指定自定义链接器参数,但我无法确定这些参数应用mov rax, {ABSOLUTE_AOTC_FUNCTION_ADDRESS} jmp rax
将上述方法内联到 JIT 代码中的每个内部调用位置,从而得到 (3) 的反向处理方式。此方法解决了间接引用问题,但会使 JIT 代码变得更大(可能会影响指令缓存和解码效果)。它仍然存在着跳转是间接的并且取决于
mov
操作的问题。在 JIT 页附近的 PROT_READ(只读)页面上放置 AOTC 函数的地址。使所有附近的调用站点成为绝对间接调用(请参见以下代码)。这种方式消除了 (2) 中的第二层间接引用。但是,这条指令的编码非常大(6 个字节),因此其与 (4) 具有相同的问题。此外,现在跳转不再依赖寄存器,而是不必要地依赖内存(只要 JIT 时已知地址),这肯定会影响性能(尽管这个页面可能被缓存?)。
aotc_function_address: .quad 0xDEADBEEF # Then at the call site call qword ptr [rip+aotc_function_address]
调整段寄存器以使其接近AOTC函数,从而可以相对于该段寄存器进行调用。这种调用的编码很长(所以可能有解码管道问题),但除此之外,基本上避免了所有前面的棘手问题。但是,可能相对于非(正如 @prl 所指出的,这需要远程调用,这对性能来说非常糟糕。)cs
段进行调用会表现不佳。或者说这样的 futzing 不明智(例如会干扰 Rust 运行时)。并不是一个解决方案,但我可以使编译器成为32位,完全没有这个问题。这并不是一个好的解决方案,它也将阻止我使用扩展通用寄存器(我利用了全部)。
所有提出的选项都有缺点。简要地说,1和2似乎是没有性能影响的唯一选项,但不清楚是否有非hack的方法实现它们(或者根本没有办法)。3到5与Rust无关,但显然有性能缺陷。
在这个意识流中,我得出了以下修辞问题(不需要明确回答),以证明我缺乏回答这个 SO 线程核心问题的知识。 我将它们打了出来,以便清楚地表明我不是把它们都作为我的问题提出。
对于方法(1),是否可能强制Rust在特定地址(靠近堆)链接某些
extern "C"
函数?我应该如何选择这样的地址(在编译时)?可以安全地假设mmap
返回的任何地址(或由Rust分配的地址)都在与此位置为32位偏移之内吗?对于方法(2),如何找到一个合适的地方放置JIT页面(以便它不会破坏现有的Rust代码)?
还有一些针对 JIT(非 Rust)的具体问题:
对于方法(3),存根会影响性能到足以值得关注吗?间接的
jmp
呢?我知道这有点像链接器存根,只不过我理解的链接器存根至少只在解析一次(因此它们不需要是间接的?)。任何 JIT 是否采用此技术?对于方法(4),如果(3)中的间接调用可以,那么内联调用是否值得?如果JIT通常采用方法(3/4),那么这个选项更好吗?
对于方法(5),跳转对内存的依赖性(考虑到地址在编译时已知)是否不好?这会使其比(3)或(4)更不耐用吗?任何 JIT 是否采用此技术?
对于方法(6),这种 futzing 是否不明智?(针对 Rust)是否有可用的段寄存器(未被运行时或ABI使用)用于此目的?相对于非
cs
段的调用是否与相对于cs
的调用一样有效?
最后(也是最重要的),是否有更好的方法(也许JIT常用的方法),我错过了这些方法吗?
我的Rust问题没有答案,所以我无法实现(1)或(2)。当然,我可以实现和基准测试3-5(也许6,尽管事先知道段寄存器 futzing 会很好),但是考虑到这些方法差异巨大,我希望有现有的文献介绍这一点,而我找不到它,因为我不知道正确的术语来搜索(我也正在进行这些基准测试)。或者,也许深入研究JIT内部的人可以分享他们的经验或他们经常看到的东西?
我知道这个问题:Jumps for a JIT (x86_64)。它与我的不同,因为它在谈论连接基本块(接受的解决方案对于频繁调用的内部函数来说指令太多了)。我还知道Call an absolute pointer in x86 machine code,虽然它讨论了与我的类似的主题,但是不同之处在于,我不假设绝对跳转是必要的(例如方法1-2将避免它们)。