为什么 PUSHF 和 POPF 这么慢?

逆向工程 二元分析 x86 仪器仪表 二进制
2021-07-07 00:28:30

实验是在32-bit x86Linux 上进行的。

我正在做一些静态二进制检测工作,基本上我试图在每个基本块的开头插入一些指令。

BB23 : push %eax

movl index,%eax
movl $0x80823d0,buf(,%eax,0x4)
add $0x1,%eax
cmp $0x400000,%eax
jle BB_23_stub
movl $0x0,%eax
BB_23_stub:movl %eax,index

pop %eax

注意我需要使用cmp指令,并且为了保证flags可以恢复到原始值,我使用pushf在堆栈上popf存储\加载flags

然后就变成这样了:

 BB_23 :    push %eax
       pushf               
       movl index,%eax
       movl $0x17,buf(,%eax,0x4)
       add $0x1,%eax
       cmp $0x400000,%eax
       jle BB_23_stub
       movl $0x0,%eax
BB_23_stub:movl %eax,index
       popf             
       pop %eax

我测试了有和没有pushf的性能popf(我正在使用gzipbzip)。令我惊讶的是,使用pushfpopf说明后,性能损失甚至可能增加 3 倍!!

但是,没有pushfpopf的压缩结果gzipbzip不正确。

所以这是我的问题:

为什么 pushf 和 popf 这么慢?我是否以正确的方式使用它?

我无法承受 pushf 和 popf 带来的太多性能损失。有什么办法可以避免高开销并保持正确的语义?(保护标志中的值,基本上......)

我够清楚了吗?谁能给我一些帮助?

4个回答

巧妙地(有些人会说难以理解)滥用 x86 功能可以为您做到这一点。loop指令将递减ecx寄存器,如果它不为零则跳转,并且不修改标志。您也可以将其用作jump forward指令,如下所示:

BB23:      push %ecx
           movl index, %ecx
           movl $0x17, buf-4(,%ecx,4)
           loop BB23_stub
           movl $0x400000, %ecx
BB23_stub: movl %ecx, index
           pop %ecx

注意这里ecx是从0x400000运行到1,而不是从0到0x3fffff,所以我必须4从地址中减去buf分析时需要从上到下读取缓冲区。不要忘了初始化index0x400000在你的代码的地方开始。您必须测试loop与删除pushf/popf收益多少相比,分支成本的损失有多大

如果你看一下lib/Target/X86/X86InstrInfo.cppLLVM源代码中,你可以看到,他们更喜欢LAHFSAHF指令PUSHFPOPF速度的原因。这些指令不处理溢出标志,OF因此必须单独处理。

alt_pushf:        seto %al                  ; save OF to AL
                  lahf                      ; save other flags to AH
                  push %eax                 ; push

alt_popf:         pop %eax                  ; pop
                  addb $127, %al            ; restore OF
                  sahf                      ; restore other flags

我不知道这是否会比@GuntramBlohm 的聪明LOOP选择更快,所以它可能值得进行基准测试。

(请注意,如果您希望将来在 64 位代码中使用它,您将需要检查LAHFSAHF指令的存在。)

为不同的方法发布第二个答案,结合cmov使用@Ian Cook 的漂亮的 lahf/sahf 来避免跳过 1 指令分支。

       push   %ecx
       movl   index, %ecx

       push   %eax
       seto   %al            # save OF to AL
       lahf                  # save other flags to AH

       movl   $0x17,  buf(,%ecx,0x4)
       dec    %ecx
       cmovc  buflen, %ecx       # load buflen constant from memory on wraparound

       addb $127, %al            # restore OF
       sahf                      # restore other flags
       pop %eax

       movl   %ecx,index
       pop %ecx

这是 14 个 insns,所有单 uop 单周期延迟(在 Intel 上)。因此,它可能仍然比 LOOP 版本慢,除非如果此代码到处重复,则不会影响分支预测器。

使用英特尔 ADX(使用 CF 或 OF 进行加进进位,以允许并行的两个 dep 链),您可以避免破坏溢出标志。但是它不需要直接的 arg,所以你需要在内存中使用一个常量 (-4)。您需要检测零周围的环绕,并避免cmp. 这个指令集扩展首先在 Broadwell 中得到支持(几乎没有用于台式机,甚至不是所有当前出售的笔记本电脑都有它。)

无论如何,clc / adcx minus_one, %ecx 不是dec %ecx会保存网络指令(一个 clc 来保存 asetoaddb $127保存/恢复溢出标志),这并不多。13 uops 仍然比我的其他答案更多,使用 MMX reg 作为 sub/mask 以避免接触标志。

另一种可能性是使用lea,并使用不影响标志的左移和右移(BMI2(Haswell)指令集SHLX / SHRX将高位清零这完全避免了接触标志:

       push   %ecx
       movl   index, %ecx

       movl   $0x17,  buf(,%ecx,0x4)
       lea    -1(%ecx), %ecx
       push   %eax
       movl   $bit_count, %eax   # 32 - significant bits in buflen
       shlx   %eax, %ecx, %ecx   # shift count has to be in a reg
       shrx   %eax, %ecx, %ecx
       pop    %eax

       movl   %ecx,index
       pop %ecx

好吧,无标志移位仅可用作(英特尔语法)shrx r32a, r/m32, r32b,加载要移位的值,而不是移位计数。并且立即移位计数也不可用,所以我仍然需要 push/pop eax 来获得第二个寄存器。

所以这是 Intel 上的 11 uops,所有单周期延迟。它仍然没有击败 mmx 版本。

如果您index向下计数,并无条件地屏蔽它以处理环绕,而不是有条件的呢?嗯,AND设置所有标志,包括OF(不使用 保存/恢复lahf/safh)。您可以使用 MMX 寄存器,但PAND没有直接形式,因此您需要在内存中拥有常量。

BB23:      push %ecx
           ; movq %mm0, -8(%esp)   ; not safe if a signal handler fires while data is below the stack.
            ;  x86 has no red-zone.  But we can't sub $16, %esp  without clobbering flags
           movq   %mm0, save_mm0
           movd   index, %mm0
           psubd  one, %mm0      ;  mmx has no dec-by-one
           pand   my_mask, %mm0   ; (0x400000-1).  0-max -> untouched.  all-1s after wraparound -> max
           movd   %mm0, %ecx
           movl   $0x17, buf(,%ecx,4)
           ; movq   -8(%esp), %mm0
           movq   save_mm0, %mm0
           movl   %ecx, index
           pop    %ecx

在 Intel 上,这是 10 uops,因此它可能比使用LOOP. 或者只有 8 个,如果您正在检测的代码不使用 MMX,或者不使用 SSE,那么您可以避免保存/恢复向量 reg。跳转会中断来自解码器或 uop 缓存的 uop 流,因此它也适用。

它需要另外 8 个字节的常量。如果它们与索引位于同一缓存行中,那没什么大不了的。它确实需要更多的指令字节。从好的方面来说,它是无分支的,因此将它插入所有位置不会因大量采用的分支而污染分支预测器。(安排分支,以便未采用的情况是常见的情况会更好。保存/恢复标志版本可以使用来自归零内存位置的 cmov,而不是分支。)

在 SnB 和更新版本上, store 的缩放偏移版本可能不会 micro-fuse如果直接数据不算作第三个输入依赖项,那么它仍然可以。否则,将所有内容都扩大 4,包括常数 for psubd,则存储为movl $0x17, buf(%ecx)

我的第一个版本是将 %mm0 保存在堆栈中,但没有推动 MMX regs。这将使它成为 11 个 uop,计算在 之前插入的堆栈引擎同步 uop movq %mm0, -8(%rsp),因为它跟在堆栈指令 ( push) 之后。