typedef struct foo
{
void (*const t)(struct foo *f);
} foo;
void t(struct foo *f)
{
}
void (*const myt)(struct foo *f) = t;
foo f = {.t = t};
int main(void)
{
f.t(&f);
myt(&f);
return 0;
}
When compiling the above code with x86-64 gcc 13.2 and clang 16.0.0 similar assembly code is generated. Shown below is gcc output.
当用x86-64编译上述代码时,GCC 13.2和clang 16.0.0生成了类似的汇编代码。下面显示的是GCC的输出。
t:
ret
main:
sub rsp, 8
mov edi, OFFSET FLAT:f
call [QWORD PTR f[rip]]
xor eax, eax
add rsp, 8
ret
f:
.quad t
myt:
.quad t
Why do both compilers emit a call to t
function when called through struct foo
but not when called via myt
function pointer? Why are the compilers not able to see that t
function pointer in struct foo
points to an empty function and eliminate the call all together? Since f
is allocated statically and initialized at compile time such that its member t
, a constant pointer that cannot be changed during runtime (without invoking undefined behaviour), points to an empty function t
.
为什么两个编译器在通过struct foo调用时都会发出对t函数的调用,而在通过myt函数指针调用时却不会?为什么编译器不能看到struct Foo中的t函数指针指向一个空函数并一起消除调用?因为f是静态分配的,并且在编译时被初始化,所以它的成员t是一个在运行时不能改变的常量指针(不调用未定义的行为),指向一个空函数t。
更多回答
I guess it fails to see that f.t
is const.
我想它没有看到F.T.是常量。
The same happens for struct { const int i; } s = { 42 }; int foo() { return s.i; }
结构{const int i;}S={42};int foo(){Return S.i;}也是如此
@tstanisl Not entirely sure about C, but in C++ the const
on f.t
or s.i
does not prevent them from evaluating to a different value than what the variables f
or s
were initialized with. (And in C++ this could already happen before main
.) You'd need const
top-level on the variables for that restriction.
@tstanisl不完全确定C,但在C++中,F.T或S.I上的常量不会阻止它们计算为与初始化变量f或S不同的值。(在C++中,这可能已经在Main之前发生了。)您需要了解该限制的变量的顶级常量。
I start to doubt if f.t
is really a const object. It's indeed an const-qualified l-value expression but it does not mean that the memory region is actually const. Some citation from the standard will be very helpful here.
我开始怀疑F.T是否真的是一个常量对象。这确实是一个常量限定的L值表达式,但这并不意味着内存区域实际上是常量。标准中的一些引用在这里会非常有帮助。
I've found this answer to related a question. It looks that whole f
is not const qualified. The const
for t
just prevent using f.t
to change the value of this object. It is a quite murky region of C standard.
我找到了一个相关问题的答案。看起来整个f都不是const限定的。t的const只是防止使用f.t来改变这个对象的值。这是C标准的一个相当模糊的区域。
GCC and Clang both provide mechanisms for arbitrary code to run before main
, so those compilers have to respect this possibility. (Including linking with C++ libraries with static constructors, or GNU C __attribute__((constructor))
.)
GCC和Clang都提供了在Main之前运行任意代码的机制,因此这些编译器必须考虑这种可能性。(包括使用静态构造函数链接C++库,或GNU C__ATTRIBUTE__((构造函数)。
The global variable foo f
is not const
, so GCC and clang assume it could have been modified before execution reaches f.t(&f);
. Changing to const foo f = ...;
lets GCC and clang know that the function-pointer value is a compile-time constant, and inline through it and optimize away the empty function. (Godbolt)
全局变量foo f不是常量,因此GCC和clang假设它可能在执行到达F.t(&f);之前就被修改了。更改为const foo f=...;让GCC和clang知道函数指针值是编译时常量,并通过它内联并优化掉空函数。(Godbolt)
So what could code look like which modifies f.t
?
那么,修改F.T的代码会是什么样子呢?
Obviously it can't do f.t = whatever
because the .t
member is const
. GCC and clang also refuse to compile f = (foo){NULL};
to replace the whole struct's value, complaining about the const
member. (Clang says error: cannot assign to variable 'f' with const-qualified data member 't'. GCC uses the same error as with const foo f
, error: assignment of read-only variable 'f'.)
显然,它不能执行F.T=任何操作,因为.T成员是const。GCC和clang还拒绝编译f=(Foo){NULL};来替换整个结构的值,抱怨常量成员。(clang表示错误:无法向具有常量限定数据成员‘t’的变量‘f’赋值。GCC使用与const foo f相同的错误,错误:只读变量‘f’的赋值。)
But they do allow memcpy
without warnings even from -Wall -Wextra
, vs. they do warn if f
is const
. (I used NULL
as a placeholder just to avoid writing another valid function. If you can change it to NULL, you can change it to a function that would do something when called.)
但他们确实允许没有警告的Memcpy,即使是来自-Wall-WExtra的也是如此,而如果f是常量,他们确实会发出警告。(我使用NULL作为占位符,只是为了避免编写另一个有效函数。如果可以将其更改为空,则可以将其更改为在调用时执行某些操作的函数。)
#include <string.h>
// Code like this could hypothetically run before main in another compilation unit
void startup(void){
// f = (foo){NULL}; // illegal
foo tmp = {NULL};
memcpy(&f, &tmp, sizeof(f)); // legal, it seems, as long as foo f isn't const foo f
// (In which case this warns, and will segfault at run-time)
// actually clang emits no instructions for the memcpy in the UB case where it's trying to write const foo f.
}
I haven't checked the ISO standard, but assuming GCC/clang's (lack of) warnings are consistent with what its optimizer assumes other code could have done, this explains why non-const
f
is not treated as having a known value in this case.
我还没有检查ISO标准,但是假设GCC/clang的(缺少)警告与它的优化器假设的其他代码可以做的一致,这就解释了为什么在这种情况下,非常数f不被视为具有已知值。
Why doesn't static foo f
help?
Making it static foo f
(in a compilation unit that doesn't define a startup()
function to modify it) should allow constant-propagation, since nothing outside the compilation unit can see it, and nothing inside changes the value.
将其设置为静态foo f(在没有定义启动()函数来修改它的编译单元中)应该允许常量传播,因为编译单元外部的任何东西都看不到它,并且内部的任何东西都不会改变它的值。
But even gcc -fwhole-program
and GCC/clang -flto
(disassembling the linked binary) fail to inline f.t(&f)
, even though those options do let them inline myt(&f)
for non-const
non-static
myt
. (Godbolt).
但是,即使是GCC-fall程序和GCC/clang-flto(反汇编链接的二进制)也无法内联F.t(&f),即使这些选项确实允许它们内联MYT(&f)来处理非常数非静态MYT。(龙珠)。
Changing the calls to f.t(0);
and myt(0)
allows GCC and clang to optimize away both calls! (With static foo f
and const myt
, or with -fwhole-program
or -flto
.) I think passing &f
as an arg was defeating escape analysis, even though compilers could in theory have proved that the call target was still always the t()
defined in this compilation unit, which didn't actually let the address escape outside the compilation unit.
将调用更改为F.T(0);和MYT(0)允许GCC和Cang优化这两个调用!(使用静态foo f和const myt,或使用-fall-Program或-flto。)我认为将&f作为arg传递会破坏转义分析,即使编译器在理论上可以证明调用目标始终是这个编译单元中定义的t(),这实际上不会让地址在编译单元之外转义。
// or non-const is the same if we're using -flto or -fwhole-program
void (*const myt)(const struct foo *f) = t;
static foo f = {.t = t};
int main(void)
{
f.t(0); // not f.t(&f) so it's clear to the optimizer the address of f doesn't escape
myt(0);
// f.t(0); myt(&f); // lets clang -flto optimize away both calls, but still not GCC -fwhole-program
}
In C, it's not UB for main
to get called again from inside the program. So if f.t(&f)
changes f.t
, the next execution of main
(e.g. from an __attribute__((destructor))
function in another compilation unit) would be a call to a different function. That's not actually possible here, but it's easy to imagine that a compiler might have got caught in a loop trying to prove that and given up. (f
has its address taken and passed to a function call, by a non-static
function in this compilation unit, which normally means the address escapes. Seeing that it goes to t()
which doesn't change f
would require inlining to happen before it finished escape analysis, but it can't inline through a function pointer unless escape analysis can prove it knows which function is being called.)
在C中,从程序内部再次调用main不是UB。因此,如果F.t(&f)更改F.t,则main的下一次执行(例如,从另一个编译单元中的__ATTRIBUTE__((析构函数))函数)将是对不同函数的调用。这在这里实际上是不可能的,但很容易想象编译器可能在试图证明这一点的循环中被捕获并放弃了。(F通过该编译单元中的非静态函数获取其地址并传递给函数调用,这通常意味着地址转义。看到它转到t(),而t()不改变f,需要在它完成转义分析之前进行内联,但它不能通过函数指针进行内联,除非转义分析可以证明它知道正在调用哪个函数。)
myt
on the other hand is truly const
so is easy to optimize away. Even if we make it not const
or static
, with -flto
or -fwhole-program
it's easy for the compile to see that nothing in this compilation unit (or any compilation unit with LTO) changes it, and &myt
isn't passed as a function arg so obviously doesn't escape. (Without -flto
or const
/static
, constprop through global myt
can't happen either.)
另一方面,MYT是真正的常量,所以很容易优化。即使我们使它不是常量或静态的,使用-flto或-fall-Program,编译器也很容易看到这个编译单元(或任何带有LTO的编译单元)中没有任何东西改变它,而且&myt不会作为函数arg传递,因此显然不会转义。(如果没有-flto或const/static,通过全局MYT的stprop也不会发生。)
Godbolt - with main
doing f.t(0);myt(0);
, *const myt
and static foo f
are sufficient to let both GCC and clang optimize away both calls, even without -flto
or -fwhole-program
. Unless this compilation unit defines a function like startup()
that does a memcpy
to &f
.
Godbolt-Main执行F.T(0);MYT(0);,*const MYT和静态foo f足以让GCC和clang优化这两个调用,即使没有-flto或-fall程序。除非该编译单元定义了一个函数,如startUp(),该函数可以对&f执行一次memcpy。
But with non-static
foo f
, that optimization is once again impossible since it could have been modified even before the first / only execution of main
.
但是对于非静态foo f,这种优化再一次是不可能的,因为它甚至可能在第一次/唯一执行main之前就被修改了。
BTW, my Godbolt links have -fvisibility=hidden
just to make sure symbol-interposition isn't a possibility, which would be another way for a non-static
variable's value to be different from the initializer in the source. But that's already not possible for globals in the main executable, only relevant with -fPIC
to compile code that's safe for a shared library.
顺便说一句,我的Godbolt链接有-fvisibility=hidden,只是为了确保符号插入是不可能的,这将是非静态变量的值与源代码中的初始化器不同的另一种方式。但是,这对于主可执行文件中的全局变量来说已经不可能了,只与-fPIC相关,以编译对共享库安全的代码。
更多回答
"so GCC and clang assume it could have been modified" Except a function pointer of read-only type * const
cannot be modified - doing so would invoke undefined behavior. Adding qualifiers to struct members rather than the whole struct often causes unexpected trouble, in my experience.
“因此GCC和Clang假设它可能已经被修改了”,除非是只读类型的函数指针*const不能被修改--这样做会调用未定义的行为。在我的经验中,向结构成员而不是整个结构添加限定符经常会导致意想不到的麻烦。
Except a function pointer of read-only type * const
cannot be modified - doing so would invoke undefined behavior. As I showed, replacing the object-representation of f
via memcpy
is something that GCC and Clang's optimizers think is allowed when the whole f
isn't const
. Regardless of what the ISO standard says, that's what explains the behaviour of their optimizers. If they define behaviour beyond what ISO C permits, they can't also assume it doesn't happen.
除非是只读类型的函数指针,否则不能修改*const-这样做会调用未定义的行为。正如我所展示的,当整个f不是常量时,GCC和Clang的优化器认为可以通过Memcpy替换f的对象表示形式。不管ISO标准怎么说,这就是他们优化器的行为的解释。如果他们定义的行为超出了ISO C允许的范围,他们也不能假设它没有发生。
The assignment is disallowed in 6.3.2.1p1 as a struct with a const member is not a modifiable lvalue. However, begin non-modifiable l-value does not make an object immutable. Arrays are a good example.
6.3.2.1p1中不允许赋值,因为具有常量成员的结构不是可修改的左值。然而,BEGIN不可修改L-值不会使对象不可变。数组就是一个很好的例子。
@tstanisl Non-const members are modifiable lvalues though, just like array elements, so a struct with a const member is not necessarily immutable. But a const member is a const member. It is an object having a const-qualified declared type (or so the standard makes me believe). It doesn't really matters though for the purpose of this answer because gcc and clang clearly treat the struct as modifiable (they place it in .data rather than .rodata). Whether it is justified standard-wise is a different question.
但是,@tstanisl非常量成员是可修改的左值,就像数组元素一样,因此具有常量成员的结构不一定是不可变的。但是常量成员就是常量成员。它是一个具有常量限定声明类型的对象(至少标准让我相信是这样的)。但对于这个答案来说,这并不重要,因为GCC和Clang显然将该结构视为可修改的(他们将其放置在.data中,而不是.rodata中)。从标准上讲,这是否合理则是另一个问题。
@n.m.couldbeanAI Yes but as such you can only assign a value to it with memcpy, ironically - not by assignment. Which is BS, the language shouldn't work like that. Qualifiers of struct members, the effective type rule, the malloc interface - all of it is very badly designed, leading to nothing but obscure and/or dangerous code - by language design.
@n.m.couldbeanAI是的,但具有讽刺意味的是,您只能使用Memcpy为其赋值-而不是通过赋值。这是胡说八道,语言不应该是这样的。结构成员的限定符、有效的类型规则、Malloc接口-所有这些都设计得非常糟糕,导致语言设计只会导致晦涩和/或危险的代码。
我是一名优秀的程序员,十分优秀!