C 试图获得addq,但继续获得leaq

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

所以,我试图熟悉汇编,并尝试对一些代码进行反向工程。我的问题在于试图解码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= (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条说明可以