C 试图获得addq,但继续获得leaq
所以,我试图熟悉汇编,并尝试对一些代码进行反向工程。我的问题在于试图解码addq,我理解它执行Source+Destination=Destination 我使用的假设是参数x、y和z在寄存器%rdi、%rsi和%rdx中传递。返回值存储在%rax中C 试图获得addq,但继续获得leaq,c,gcc,assembly,x86-64,compiler-optimization,C,Gcc,Assembly,X86 64,Compiler Optimization,所以,我试图熟悉汇编,并尝试对一些代码进行反向工程。我的问题在于试图解码addq,我理解它执行Source+Destination=Destination 我使用的假设是参数x、y和z在寄存器%rdi、%rsi和%rdx中传递。返回值存储在%rax中 long someFunc(long x, long y, long z){ 1. long temp=(x-z)*x; 2. long temp2= (temp<<63)>>63; 3. long temp3= (t
long someFunc(long x, long y, long z){
1. long temp=(x-z)*x;
2. long temp2= (temp<<63)>>63;
3. long temp3= (temp2 ^ x);
4. long answer=y+temp3;
5. return answer;
}
long someFunc(长x、长y、长z){
1.长温=(x-z)*x;
2.长temp2=(temp63;
3.长temp3=(temp2^x);
4.长答案=y+temp3;
5.返回答案;
}
到目前为止,第4行以上的内容正是我想要的。然而,第4行给了我
leaq(%rsi,%rdi),%rax
而不是addq%rsi,%rax
。我不确定这是否是我做错了什么,但我在寻找一些见解。这些指令并不等同。对于LEA,rax
是一个纯输出。对于你希望的添加,它是rax+=rsi
,因此编译器必须mov%rdi,%首先是rax。这样效率较低,所以它不会这样做
lea
是编译器实现dst=src1+src2
,保存mov
指令的一种完全正常的方式。一般来说,不要期望C运算符编译成以它们命名的指令。尤其是小的左移和加或乘3、5或9,因为它们是优化的主要目标与LEA的关联。例如,LEA(%rsi,%rsi,2),%rax
实现了result=y*3
。有关更多信息,请参阅。如果以后需要两个输入,LEA还可用于避免破坏这两个输入
假设您的意思是t3
是与temp3
相同的变量,那么clang确实按照您期望的方式编译,更好地完成寄存器分配,因此它可以使用更短、更高效的add
指令,而无需任何额外的mov
指令,而不需要lea
Clang选择比GCC做得更好的寄存器分配,因此它可以只使用add
,而不需要对最后一条指令使用lea
。()这节省了代码大小(因为采用了索引寻址模式)在大多数CPU上,add
的吞吐量略高于LEA,比如4/时钟而不是2/时钟
Clang还优化了到和l$1的移位,%eax
/negq%rax
以创建该算术右移=位广播的0或-1
结果。它还优化了前几个步骤的32位操作数大小,因为移位除了temp1
的低位外,其余都会丢弃
# side by side comparison, like the Godbolt diff pane
clang: | gcc:
movl %edi, %eax movq %rdi, %rax
subl %edx, %eax subq %rdx, %rdi
imull %edi, %eax imulq %rax, %rdi # temp1
andl $1, %eax salq $63, %rdi
negq %rax sarq $63, %rdi # temp2
xorq %rdi, %rax xorq %rax, %rdi # temp3
addq %rsi, %rax leaq (%rdi,%rsi), %rax # answer
retq ret
请注意,clang选择了imul%edi,%eax
(转换为RAX),但GCC选择了乘法转换为RDI。这就是寄存器分配的差异,导致GCC在末尾需要lea
,而不是add
当编译器做出这样糟糕的选择时,有时甚至会在一个小函数的末尾遇到一条额外的mov
指令,如果最后一个操作不是像加法一样,可以用lea
作为非破坏性的操作和复制来完成。这些是遗漏的优化错误;您可以在GCC的bugzil上报告它们洛杉矶
其他错过的优化
GCC和clang可以通过使用和
而不是imul
进行优化,仅当两个输入都是奇数时才设置低位
此外,由于只有sub
输出的低位才重要,所以XOR(不带进位的加法)或偶数加法(奇数+-偶数=奇数。偶数+-偶数=偶数。奇数+-奇数=奇数)将允许lea
而不是mov/sub
作为第一条指令
lea (%rdi,%rsi), %eax
and %edi, %eax # low bit matches (x-z)*x
andl $1, %eax # keep only the low bit
negq %rax # temp2
让我们为x
和z
的低位创建一个真值表,看看如果我们想进行更多/不同的优化,这会如何变化:
# truth table for low bit: input to shifts that broadcasts this to all bits
x&1 | z&1 | x-z = x^z | x*(x-z) = x & (x-z)
0 0 0 0
0 1 1 0
1 0 1 1
1 1 0 0
x & (~z) = BMI1 andn
所以temp2=(x^z)&x&1?-1:0
。但是temp2=-((x&~z)&1)
。
我们可以将其重新排列为-((x&1)和~z)
,这样我们就可以并行地从而不是z
和以及$1,x
开始,以获得更好的ILP。或者,如果z
可能首先准备好,我们可以对其进行操作,并缩短关键路径,从x
到答案,代价是z
或者对于执行(~z)&x
的指令,我们可以在一条指令中执行此操作。(加上另一条指令以隔离低位)
我认为这个函数对于每个可能的输入都具有相同的行为,因此编译器可以从您的源代码中发出它。这是您希望编译器发出的一种可能性:
# hand-optimized
# long someFunc(long x, long y, long z)
someFunc:
not %edx # ~z
and $1, %edx
and %edi, %edx # x&1 & ~z = low bit of temp1
neg %rdx # temp2 = 0 or -1
xor %rdi, %rdx # temp3 = x or ~x
lea (%rsi, %rdx), %rax # answer = y + temp3
ret
因此仍然没有ILP,除非z
在x
和/或y
之前准备好。使用额外的mov
指令,我们可以与not z
并行执行x&1
也许你可以用test
/setz
或cmov
做些什么,但是IDK如果那能打败lea/and(temp1)+和/neg(temp2)+xor+add
我还没有研究优化最终的xor和add,但请注意,temp3
基本上是一个条件而不是x
。您可以通过同时计算这两种方法并使用cmov在它们之间进行选择来提高延迟,而以牺牲吞吐量为代价。可能通过涉及2的补码标识-x-1=~x
。也许可以通过执行x+y
来改善ILP/延迟,然后根据x和z条件进行修正?因为我们不能使用LEA进行减法,所以最好不要进行减法,而是进行加法
# return y + x or y + (~x) according to the condition on x and z
someFunc:
lea (%rsi, %rdi), %rax # y + x
andn %edi, %edx, %ecx # ecx = x & (~z)
not %rdi # ~x
add %rsi, %rdi # y + (~x)
test $1, %cl
cmovnz %rdi, %rax # select between y+x and y+~x
retq
这有更多的ILP,但需要BMI1和N
仍然只有6条(单uop)指令。Broadwell和更高版本有单uop CMOV;在早期的Intel上是2个uop
另一个功能可以是使用BMI和N
的5个UOP
在此版本中,前3条说明可以