在此golfing answer中,我看到了一个窍门,其中返回值是第二个未传入的参数。
int f(i, j)
{
j = i;
}
int main()
{
return f(3);
}
从gcc's assembly output看来,当代码复制j = i
时,它将结果存储在eax
中,而恰好是返回值。
f:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
movl %eax, -8(%rbp)
nop
popq %rbp
ret
main:
pushq %rbp
movq %rsp, %rbp
movl $3, %edi
movl $0, %eax
call f
popq %rbp
ret
那么,这是否仅仅是因为幸运?这是gcc记录的吗?它仅适用于-O0
,但适用于我尝试过的i
一堆值-m32
,以及一堆不同版本的GCC。
答案 0 :(得分:4)
我认为 gcc -O0
恰好像GNU C statement-expressions这样对待函数体。那是一个实现细节,不是想要让您利用这种优势的东西。
gcc -O0
确保最后计算的赋值表达式的值保留在返回值寄存器中。 (GCC -O0
通常只喜欢在retval寄存器中具有值,但这不仅仅将其作为第一个临时值。)
我已经测试了一下,确实看起来GCC -O0
是跨多个ISA故意这样做的,有时甚至使用额外的mov
指令或等效指令。 IIRC我使表达式更加复杂,因此求值结果在另一个寄存器中结束,但仍将其复制回retval寄存器。
没有任何记载,保证或标准化的内容。
以这种方式“返回”值表示您正在“ GCC -O0”而不是C中进行编程。代码高尔夫球规则的措辞表示,程序必须至少在一个实施。但是我的理解是,它们应该出于正确的理由起作用,而不是因为一些副作用的实现细节。它们在使用clang时中断并不是因为clang不支持某些语言功能,而是因为它们甚至都不是用C编写的。
启用优化功能也很酷;某种程度的UB通常在代码高尔夫中是可以接受的,例如整数环绕或指针投射类型的修剪是人们可能希望明确定义的事情。但这纯粹是滥用了一个编译器的实现细节,而不是语言功能。
我在the relevant answer on Codegolf.SE C golfing tips Q&A下的评论中指出了这一点(错误地声称它在GCC之外有效)。这个答案有4个反对意见(值得更多IMO),但有16个反对意见。因此,社区中的一些成员不同意这是可怕的和愚蠢的。
在ISO C中,执行不落在非void
函数的末尾是未定义的行为,即使调用方不使用结果 >。即使在GNU C中也是如此。在-O0
之外的地方,GCC和clang有时会发出类似ud2
(非法指令)的代码,用于在没有return
的情况下到达函数末尾的执行路径。因此,GCC通常不会定义行为(对于ISO C未定义的事情,允许执行哪些实现。例如gcc -fwrapv
将带符号的溢出定义为2的补码环绕。)
传递第二个参数只会给您一些东西。它不是一个函数arg 并不重要。但是i=i;
即使使用-O0
也会进行优化,因此您确实需要一个单独的变量。也只有i;
可以优化。
有趣的事实:递归f(i){ f(i); }
函数体确实通过EAX反弹i
,然后将其复制到第一个arg-pass寄存器。因此,GCC真的很喜欢EAX。
movl -4(%rbp), %eax
movl %eax, %edi
movl $0, %eax # without a full prototype, pass # of FP args in AL
call f
i++;
不会加载到EAX中;它仅使用内存目标add
而不加载到寄存器中。值得尝试使用gcc -O0 for ARM。