我尝试使用Clang和GCC编译此代码:
struct s { int _[50]; };
void (*pF)(const struct s), (*pF1)(struct s), (*pF2)(struct s *);
main()
{
struct s a;
pF2(&a);
pF(a), pF1(a);
}
结果是一样的。虽然不允许调用pF
来修改其唯一参数,但会复制对象a
以进行第二次pF1
调用。这是为什么?
这是装配输出(来自GCC):
; main
push rbx
sub rsp, 0D0h
mov rbx, rsp
mov rdi, rsp
call cs:pF2
;create argument for pF1 call (as there the argument is modified)
;and copy the local a into it
;although it seems not needed because the local isn't futher read anyway
sub rsp, 0D0h
mov rsi, rbx
mov ecx, 19h
mov rdi, rsp
;
rep movsq
call cs:pF
;copy the local a into the argument created once again
;though the argument cannot be modified by the function pointed by pF
mov rdi, rsp
mov rsi, rbx
mov ecx, 19h
rep movsq
;
call cs:pF1
add rsp, 1A0h
xor eax, eax
pop rbx
retn
优化程序可以看到pF
指向的函数无法修改其参数(因为它声明为const
),因此省略了上次复制操作?另外,最近我看到,由于变量a
未在代码中进一步读取,因此可以将其存储用于函数参数。
相同的代码可以写成:
; main
push rbx
sub rsp, 0D0h
mov rdi, rsp
call cs:pF2
call cs:pF
call cs:pF1
add rsp, 0D0h
xor eax, eax
pop rbx
retn
我正在使用-O3
标志进行编译。我错过了什么吗?
即使我没有调用UB(因为函数指针默认为NULL
),它也是一样的,而我将它们初始化为:
#include <stdio.h>
struct s { int _[50]; };
extern void f2(struct s *a);
void (*pF)(const struct s), (*pF1)(struct s), (*pF2)(struct s *) = f2;
extern void f1(struct s a)
{
a._[2] = 90;
}
extern void f(const struct s a)
{
for(size_t i = 0; i < sizeof(a._)/sizeof(a._[0]); ++i)
printf("%d\n", a._[i]);
}
extern void f2(struct s *a)
{
a->_[6] = 90;
pF1 = f1, pF = f;
}
答案 0 :(得分:3)
我不相信这种优化是合法的。您忽略的是具有const参数的函数类型与具有非const参数的函数类型兼容,因此可以将变量其参数的函数分配给指针pF
。
以下是一个示例程序:
struct s {
int x;
};
/* Black hole so that DCE doesn't eat everything */
void observe(void *);
void (*pF)(const struct s);
void test(struct s arg) {
arg.x = 0;
observe(&arg);
}
void assignment(void) {
pF = test;
}
底线是参数的const注释为编译器提供了关于参数存储是否被被调用者变异的可靠信息。执行此优化似乎要求ABI要求参数存储不需要变异(或者某种整个程序分析,但不要紧,)。
答案 1 :(得分:2)
我认为该功能仍然需要制作一个副本(请参阅最终我认为最佳的允许版本)。其余的(或多或少可以理解)优化失败。
SysV x86-64 ABI并不保证函数不会修改其stack-args。它没有提及const
的任何内容。不能保证任何它不保证的东西。它只是说通过值传递的大对象进入堆栈;被调用函数返回时的状态。被调用者“拥有”它的args,即使它们被声明为const
。另请参阅x86 wiki,但ABI doc本身是wiki中唯一真正相关的链接。
类似地,窄整数类型可以在高位中的垃圾寄存器中作为args或返回值。 ABI没有明确说出任何方式,因此不能保证高位被清零。这实际上是gcc的作用:它假设在接收值时存在高垃圾,并且在传递值时会留下高垃圾。对于xmm regs中的float / double也是如此。我最近向一位ABI维护者证实了这一点,同时调查了clang生成的一些不安全的代码。因此,我确信正确的解释是您不应该假设ABI未明确保证的任何内容。
gcc没有这样做,但我相信这样的被调用函数实际上不会复制是合法的:
void modifyconstarg(const struct s x) {
// x.arr[10] = 10; // This is a compile-time error
struct s xtmp = x; // gcc/clang: make a full copy before this
xtmp.arr[11]=11;
pFconstval(xtmp); // gcc/clang: make a full copy here
}
相反,只需存储到其arg和jmp pFconstval
。
我的猜测是,这是一个错过的优化,而不是gcc和clang在对标准的解释中保守。
gcc和clang似乎并没有很好地优化远远超过寄存器的对象的副本。首先没有复制它们的源代码甚至比编译器可以做的最好的工作要好(例如通过const *
或C ++ const-reference),因为我不认为你的建议优化是合法的。
奇怪的是:使用-march=haswell
(或任何其他Intel CPU),gcc会发出对memcpy而不是rep movsq
内联代码的函数调用。我不明白。即使使用-ffreestanding
/ -nostdlib
IDK,如果其他人一直认为rdi
是指向内存的指针,即它是由不可见的引用传递的。我花了很长时间才完全确定按值调用函数根本不接受寄存器中的任何参数。我一直认为rep movsq
离开rdi
指向高拷贝是很奇怪的。
struct s { int _[50]; };
//void (*pFconstval)(const struct s), (*pFval)(struct s), (*pFref)(struct s *);
void pFref(struct s *); void pFconstval(const struct s), pFval(struct s);
void func(void) {
struct s a;
pFref(&a);
pFconstval(a); pFval(a);
}
void modifyconstarg(const struct s x) {
// x.arr[10] = 10; // This is a compile-time error
struct s xtmp = x; // full copy here
xtmp.arr[11]=11;
pFconstval(xtmp); // full copy here
}
void modifyarg(struct s x) {
x.arr[10] = 10;
pFconstval(x);
}
gcc的modifyarg
输出很有趣:
lea rdi, [rsp+8]
mov DWORD PTR [rsp+48], 10
mov ecx, 25
mov rsi, rdi ; src=dest
rep movsq ; in-place "copy"
jmp pFconstval
即使您不修改x
,也会执行复制。 Clang在尾部调用jmp
之前将实际副本发送到其他位置。
据我了解ABI:
sub rsp, 416
mov rdi, rsp
call pFref ; or call [pF2] if using function pointers. Is your disassembly in MASM syntax?
lea rdi, [rsp+208] ; aligned by 16 for hopefully better rep movsq perf
; and so the stack is aligned by 16 at each location
mov rsi, rsp
mov ecx, 25
rep movsq
call pFconstval ; clobbering the low copy
add rsp, 208
call pFval ; clobbering the remaining high copy
add rsp, 208
ret
BTW,gcc对rbx
的使用很愚蠢。它节省了四个代码字节:push
/ pop
:2个字节。 mov rbx, rsp
:3B。 2x mov rsi, rbx
:2x3B。总计= 12B
用2x lea rsi, [rsp+208]
替换所有内容:2x 8B。总计= 16B。
由于还使用mov rdi, rsp
,因此无法避免额外的堆栈引擎同步uop。 4B代码不值得花费3 uops。在我的版本中,它只复制一次(并且只需要一个LEA),这也是代码字节的损失。