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
可以恢复到原始值,我使用pushf
和popf
来存储/加载flags
在堆栈上。 然后变成这个:
q4312078q
我测试了有无
pushf
和popf
的性能(我使用的是gzip
和bzip
)。令我惊讶的是,使用pushf
和popf
指令后,性能损失甚至可以提高3倍!但是,如果没有
pushf
和popf
,则性能下降。 gzip
和bzip
的压缩结果不正确。所以这是我的问题:
为什么pushf和popf这么慢?我是否以正确的方式使用它?
我负担不起pushf和popf引入的过多性能损失。有什么办法可以避免高昂的开销并保持正确的语义? (基本上是保护标志中的值。)
我清楚了吗?有人可以给我些帮助吗?
#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
,则出于速度原因,他们会比LAHF
和SAHF
更喜欢PUSHF
和POPF
指令。这些指令没有处理溢出标志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)。
评论
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,索引。将循环视为递减,如果不为零则跳转。