实验在32-bit x86 Linux上进行。

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

BB23 : push %eax

movl index,%eax
movl 
 BB_23 :    push %eax
       pushf               
       movl index,%eax
       movl q4312078qx17,buf(,%eax,0x4)
       add q4312078qx1,%eax
       cmp q4312078qx400000,%eax
       jle BB_23_stub
       movl q4312078qx0,%eax
BB_23_stub:movl %eax,index
       popf             
       pop %eax
x80823d0,buf(,%eax,0x4) add q4312078qx1,%eax cmp q4312078qx400000,%eax jle BB_23_stub movl q4312078qx0,%eax BB_23_stub:movl %eax,index pop %eax


请注意,我需要使用cmp指令,并且为了确保flags可以恢复到原始值,我使用pushfpopf来存储/加载flags在堆栈上。

然后变成这个:

q4312078q

我测试了有无pushfpopf的性能(我使用的是gzipbzip)。令我惊讶的是,使用pushfpopf指令后,性能损失甚至可以提高3倍!

但是,如果没有pushfpopf,则性能下降。 gzipbzip的压缩结果不正确。

所以这是我的问题:

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

我负担不起pushf和popf引入的过多性能损失。有什么办法可以避免高昂的开销并保持正确的语义? (基本上是保护标志中的值。)

我清楚了吗?有人可以给我些帮助吗?

评论

1)只是一个想法,没有任何正确性的主张:pushf可能破坏指令流水线,因为它需要所有标志有效,而其他大多数指令都不关心标志。同样,如果遵循需要标志的指令,则流水可能会被popf延迟。 2)我将用inc%eax和$ 0x3fffff,%eax替换您的add-cmp-jle-mov组合,由于避免了分支,应该会加快代码的速度。这不会对您的标志有帮助,但是,我看不到不触摸标志的方法。

哦,用lea eax代替inc%eax,[eax + 1](对不起,Intel语法,我不太喜欢AT&T语法,现在也不知道如何翻译)将避免像inc那样更改标志。现在,如果我能弄清楚如何做并且不更改标志,那么您就可以摆脱那些讨厌的pushf和popf指令...

@GuntramBlohm。辉煌!!非常感谢您的帮助!真的省了我的屁股。

您似乎从索引0开始,递增到0x400000并环绕在那。如果您有能力以其他方式这样做,则可以滥用不会更改标志的循环指令。将索引初始化为0x400000,使用ecx代替eax,并递减并重新初始化为零,使用循环正向,mov $ 0x400000,%ecx,正向:movl%ecx,索引。将循环视为递减,如果不为零则跳转。

#1 楼

巧妙地(有些人会说难以理解)滥用x86功能可以为您做到这一点。 loop指令将递减ecx寄存器,如果非零则跳转,并且不修改标志。您也可以将其用作jump forward指令,如下所示:从4的地址中减去buf,分析时需要从上至下读取缓冲区。不要忘记在代码开始的地方将index初始化为0x400000。与删除loop / pushf收益要多少相比,您必须测试popf中分支成本的损失。

评论


循环确实很慢(7微码,每5个循环1个吞吐量),但是保存/恢复标志的速度甚至更慢。 (pushf = 3 oups,popf = 9)。 (英特尔SnB / Haswell)。

– Peter Cordes
15年7月15日在3:11

是的,听起来循环是您的最佳选择。请记住,与所有其他6条指令的总和相比,一个循环占用了更多的执行资源和uop缓存中的更多空间。希望它仍然足够轻巧,可以满足您的需求。

– Peter Cordes
2015年7月15日14:06



可能更快:按%eax / lahf /使用标志/ sahf /弹出%eax。从/向标志加载/存储AH是当前Intel和AMD CPU上的单线程,单周期等待时间指令。如果您要检测的东西不触及MMX / x87寄存器,则可以使用它们存储索引,也可以在环绕之后屏蔽它。哦,我知道已经有了答案。

– Peter Cordes
15年7月22日在6:46



#2 楼

如果您查看lib/Target/X86/X86InstrInfo.cpp源代码中的LLVM,则出于速度原因,他们会比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 7, %al            ; restore OF
                  sahf                      ; restore other flags


我不知道这是否会比@GuntramBlohm快聪明的LOOP选项,因此可能值得进行基准测试。

#3 楼

发布另一种方法的第二个答案,结合使用cmov来避免跳过1指令分支和@Ian Cook的漂亮的lahf / sahf。 ,所有单uup单周期延迟(在Intel上)。因此,它可能仍然比LOOP版本慢,但如果此代码在所有地方重复都不会影响分支预测变量,则不会影响分支预测器。允许两个dep链并行),则可以避免破坏溢出标志。但这并不需要立即使用arg,因此您需要内存中的常量(-4)。您需要检测到零附近的回绕,并避免cmp。该指令集扩展最初是在Broadwell中支持的(几乎不支持台式机,甚至不是所有目前正在销售的笔记本电脑都具有它。)无论如何,clc / adcx minus_one, %ecx代替dec %ecx可以节省网络指令(一个clc保存一个seto和一个addb 7来保存/恢复溢出标志),这并不多。 13 uops仍然比我的其他答案还多,对子/掩码使用MMX reg以避免接触标志。右移(BMI2(Haswell)指令集的lea)。这样可以避免完全触摸标志:

       push   %ecx
       movl   index, %ecx

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

       movl   
       push   %ecx
       movl   index, %ecx

       movl   q4312078qx17,  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
x17, buf(,%ecx,0x4) dec %ecx cmovc buflen, %ecx # load buflen constant from memory on wraparound addb 7, %al # restore OF sahf # restore other flags pop %eax movl %ecx,index pop %ecx


废话,无标志移位仅作为(Intel语法)SHLX / SHRX可用,加载要移位的值,而不是班次计数。而且也不提供立即移位计数,因此我仍然需要推/弹出eax以获得第二个寄存器。

所以这是Intel上的11微秒,所有单周期延迟。它仍然没有超越mmx版本。

#4 楼

如果您让index向下计数,并无条件地对其进行掩盖处理而不是有条件地处理该怎么办?嗯,AND设置所有标志,包括OF(不会用lahf/safh保存/恢复)。您可以使用MMX寄存器,但是PAND没有立即数形式,因此您需要在内存中具有常量。是10 oups,因此它可能比使用LOOP的版本快。如果您要检测的代码不使用MMX或不使用SSE,则只有8个,所以可以避免保存/恢复向量reg。跳转会中断来自解码器或uop缓存的uops流,因此它也需要这样做。

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

在SnB和在较新的版本中,商店的按比例缩放版本可能无法实现微型保险丝。如果立即数据不算作第三输入依赖项,那么它仍然可以。否则,将所有内容按比例放大4,包括psubd的常量,然后存储为movl movq %mm0, -8(%rsp)x17, buf(%ecx)。这将使它成为11个,计算在push之前插入的堆栈引擎同步uop,因为它遵循堆栈指令(q4312079q)。