[问题以粗体显示]
当汇编程序生成二进制编码时,需要决定是使每个分支变长还是短,如果可能的话,短路更好。汇编程序的这一部分称为分支位移优化(BDO)算法。一种典型的方法是汇编程序使 all 分支编码变短(如果它们小于某个阈值),然后迭代地增加任何分支跳转到未达到的long。当然,这可以导致其他分支转换为跳远。因此,汇编程序必须不断通过跳转列表,直到不再需要升迁。这种二次时间方法对我来说似乎是一个最优算法,但据推测BDO是NP完全的,这种方法实际上并不是最优的。
兰德尔海德提供了一个反例: .386
.model flat, syscall
00000000 .code
00000000 _HLAMain proc
00000000 E9 00000016 jmpLbl: jmp [near ptr] target
00000005 = 00000005 jmpSize = $-jmpLbl
00000005 00000016 [ byte 32 - jmpSize*2
dup
(0)
00
]
0000001B target:
0000001B _HLAMain endp
end
通过在括号中添加部分" [near ptr]"并且强制进行5字节编码,二进制实际上最终会变短,因为分配的数组比跳跃大小的两倍小。因此,通过缩短跳转编码,最终代码实际上更长。
这对我来说似乎是一个非常病态的情况,并不是真正相关的,因为分支编码仍然较小,它只是程序的非分支部分的这种奇怪的副作用,导致二进制变大。由于分支编码本身仍然较小,我并不是真的认为这是一个有效的反例,从小开始#34;算法
我可以将start-small算法视为最佳BDO算法,还是存在逼真情况,其中不为所有分支提供最小编码大小?
答案 0 :(得分:5)
这里有一个证据表明,在评论中没有哈罗德提到的异常跳跃时,"从小开始#34;算法是最优:
首先,让我们确定"从小开始#34;总是产生一个可行的解决方案 - 也就是说,它不包含太长跳转的任何短编码。该算法基本上等于反复询问问题"它是否可行?"并且如果没有那么延长一些跳转编码,那么如果它终止,那么它产生的解决方案必须是可行的。由于每次迭代都会延长一些跳跃,并且跳转不会超过一次,因此该算法最终必须在最多nJump次迭代后终止,因此解决方案必须可行。
现在假设相反该算法可以产生次优解X.设Y是一些最优解。我们可以将解决方案表示为延长的跳转指令的子集。我们知道| X \ Y | > = 1 - 也就是说,在X中至少有1个指令延长,而不是在Y中 - 因为否则X将是Y的子集,并且因为Y是假设的最优且X已知是可行的是,它将遵循X = Y,这意味着X本身就是一个最优解,这与我们关于X的原始假设相矛盾。
从X \ Y中的说明中,选择i为"首先加长"算法,并且让Z是Y(和X)的子集,包括在此之前算法已经延长的所有指令。因为"从小开始#34;算法决定延长i的编码,一定是在那个时间点(即在延长Z中的所有指令之后)的情况下,我的跳跃位移对于短编码来说太大了。 (请注意,虽然Z中的某些长度可能会使我的跳跃位移超过临界点,但这绝不是必要的 - 也许我的位移从一开始就高于阈值。我们所有人可以知道,而且我们需要知道的是,当Z处理完毕时,我的跳跃位移高于阈值。)但是现在回顾最优解Y,并注意 none在Y中的其他长度 - 即在Y \ Z中 - 能够减少我的跳跃位移,因此,因为我的位移高于阈值但是它的编码是不延长Y,Y甚至不可行!一个不可行的解决方案不能是最优的,所以在Y中存在这样一个非延长的指令i会与Y是最优的假设相矛盾 - 这意味着我不能存在这样的假设。
答案 1 :(得分:4)
j_random_hacker的论点,Start Small对于没有填充声音合理的简化情况是最佳的。但是,在优化尺寸功能之外,它不是很有用。 真正的asm 确实拥有ALIGN
指令,而 会产生差异。
这是我可以构建一个最简单的例子,其中Start Small没有给出最佳结果(使用NASM和YASM测试)。 使用jz near .target0
强制执行长编码,提前移动another_function:
32个字节并减少func
内的填充。
func:
.target0: ; anywhere nearby
jz .target0 ; (B0) short encoding is easily possible
.target1:
times 10 vpermilps xmm14, xmm15, [rdi+12345]
; A long B0 doesn't push this past a 32B boundary, so short or long B0 doesn't matter
ALIGN 32
.loop:
times 12 xor r15d,r15d
jz .target1 ; (B1) short encoding only possible if B0 is long
times 18 xor r15d,r15d
ret ; A long B1 does push this just past a 32B boundary.
ALIGN 32
another_function:
xor eax,eax
ret
如果B0很短,那么B1必须很长才能达到目标1。
如果B0很长,它会将target1拉近B1,从而允许短编码到达。
因此,B0和B1中的一个最多可以有一个短编码,但重要的是哪一个。短B0意味着3个字节的对齐填充,不会节省代码大小。允许短B1 的长B0 可以节省总代码大小。在我的例子中,我已经说明了可能发生的最简单的方法:通过在B1超过下一个对齐的边界之后推动代码的结尾。它也可能影响其他分支,例如需要对分支进行长编码.loop
。
开始 - 小结果:B0短,B1长。 (它们的初始首次通过状态。)Start-Small不会尝试延长B0并缩短B1以查看它是否减少了总填充,或者只是执行填充(理想情况下按行程计数加权)。
.loop
之前的4字节NOP,another_func
之前的31个字节的NOP,因此它从0x400160
开始,而不是我们从0x400140
开始使用{ {1}}导致B1的短编码。
请注意,B0本身的长编码不是实现B1 的短编码的唯一方法。对jz near .target0
之前的任何指令进行长度超过必要的编码也可以解决问题。 (例如,4B位移或立即,而不是1B。或不必要或重复的前缀。)
不幸的是,我所知道的汇编程序不支持这种填充方式;只有.target1
。 What methods can be used to efficiently extend instruction length on modern x86?
通常情况下,在循环开始时甚至没有跳过长nop
,因此更多的填充可能会对性能造成更大的影响(如果需要多个NOP,或者代码运行在像Atom或Silvermont这样的CPU很慢,有很多前缀,因为汇编程序没有为Silvermont进行调整而得到了使用。
请注意,编译器输出很少在函数之间跳转(通常只用于尾调用优化)。 x86没有NOP
的短编码。手写的asm可以做任何想做的事,但意大利面条代码(希望?)仍然不常见。
我认为,对于大多数asm源文件,BDO问题可能会被分解为多个独立的子问题,通常每个函数都是一个单独的问题。这意味着即使非多项式复杂度算法也是可行的。
帮助解决问题的一些快捷方式将有助于:例如检测何时需要长编码,即使所有中间分支都使用短编码。当连接它们的唯一东西是两个远程函数之间的尾调用时,这将允许打破子问题之间的依赖关系。
我不知道在哪里开始制作算法来寻找全局最优解决方案。如果我们愿意考虑扩展其他指令以移动分支目标,那么搜索空间非常大。但是,我认为我们只需考虑跨越对齐填充的分支。
可能的情况是:
如果我们将一些微体系结构优化知识嵌入汇编程序中,那么做得更好可能会更容易:例如:总是尝试让分支目标在16B insn fetch块的开头附近开始,并且最后肯定不对。 Intel uop缓存行只能从一个32B块内缓存uop,因此32B边界对于uop缓存很重要。 L1 I $行大小为64B,页面大小为4kiB。 (汇编程序不知道哪些代码很热,哪些代码很冷。热代码跨越两页可能比稍大一点的代码大。)
在指令解码组的开头有一个多uop指令也比在其他任何地方使用它更好,对于Intel和AMD。 (对于具有uop缓存的Intel CPU,情况会更糟)。确定CPU在大多数时间内将通过代码的路径,以及指令解码边界的位置,可能远远超出汇编程序可以管理的范围。