考虑以下代码:
typedef struct my_struct { int a; int b; int c; } my_struct;
void some_function(my_struct *value)
{
// do something
}
int main(void)
{
for (;;) {
some_function(&(my_struct){1, 2, 3});
}
return 0;
}
我计划始终将a
、b
和设置c
为1
,2
并将 设置为3
。因此,传递给 的对象some_function
将始终相同。在这种情况下,编译器是否会在每次迭代时重新创建并重新销毁该对象?或者它是否会将其转换为类似于以下内容的内容:
typedef struct my_struct { int a; int b; int c; } my_struct;
void some_function(my_struct *value)
{
// do something
}
int main(void)
{
my_struct once_and_for_all = {1, 2, 3};
for (;;) {
some_function(&once_and_for_all);
}
return 0;
}
它不会在循环中执行任何内存分配/释放分配,并且以最佳速度运行。
我更喜欢第一种语法,因为它不太冗长,但我也担心性能。
我使用 GCC。
12
最佳答案
2
在这种情况下,编译器是否会在每次迭代时重新创建并重新销毁对象?
从语义上讲,是的。出现在块作用域中的复合文字具有与最内层包含块相关联的自动存储持续时间。当该块的执行因任何原因终止时,其生命周期就结束了。
但这并没有对实现的底层行为施加太多限制。
您似乎担心循环内部分配和释放的成本,但自动对象的分配和释放通常非常快。通常实际上是免费的。在基于堆栈的机器上,使用的内存(如果有)将位于堆栈中,并且通常会在每次循环迭代中重复使用相同的位置。
您提出的两种替代方案之间的主要区别在于,从语义上讲,前者要求在每次循环迭代时初始化对象,而后者只需初始化一次。只有当编译器确定文字未在循环主体内被修改(包括间接修改)时some_function()
,它才会考虑将初始化从循环中移除,以将您的第一个变体视为第二个变体。
如果您想鼓励编译器做出该假设,那么您可以声明some_function
为…
void some_function(const my_struct *value);
…并将文字定义为…
(const my_struct) { /* ... */ }
。如果由于某种原因,这对您来说在语义上不可行,那么您所希望的优化首先就是不合适的。
我更喜欢第一种语法,因为它不太冗长,但我也担心性能。
这种担心为时过早。两个变体之间的任何性能差异都可能太小而无法衡量。即使您的结构非常大,以至于您实际上可以观察到初始化它的成本,但如果不测量程序的性能并对其进行分析以了解其时间花在哪里,您就不知道该成本(即使发生)是否足以证明微优化是合理的。
一般情况下,使用适当、高效的算法编写干净、清晰的代码。使用可用的语言功能尽可能详细地表达您的意图。启用编译器优化,然后不用担心性能问题,直到您发现实际的性能问题。
2
-
1正如在中看到的,将函数和复合文字更改为 const 不会改变汇编程序的输出。这很奇怪 – 除了 gcc 和 clang 都错过了优化机会之外,我无法想出任何其他解释。(C23 constexpr 也没有改善任何东西)
–
-
@john“启用编译器优化,然后就不用担心性能问题,直到发现实际的性能问题。”–>确实。
–
|
反汇编一下,自己看看。只需先用外部链接声明该函数,然后将函数定义放在代码之外,这样编译器就无法对其做出任何假设:
void some_function (my_struct *value);
然后 x86 的 gcc -O3 给出如下结果:
main:
push rbp
push rbx
sub rsp, 24
mov rbp, QWORD PTR .LC0[rip]
mov rbx, rsp
.L2:
mov rdi, rbx
mov QWORD PTR [rsp], rbp
mov DWORD PTR [rsp+8], 3
call some_function
jmp .L2
.LC0:
.long 1
.long 2
也就是说:堆栈空间被保留一次,但值在循环的每一圈都会复制到该空间中。由于该函数没有const
限定参数,因此编译器不能假定该函数未修改这些值。
但是,将参数更改为 并const my_struct *value
没有改善代码。这似乎是一次错过的优化,因为我无法想到 C 标准中有什么地方会强制编译器在函数参数为 时一遍又一遍地更新文字const
。clang 的行为相同。
使用第二版代码修复了这个问题。然后我们得到:
.L2:
mov rdi, rbx
call some_function
jmp .L2
5
-
1函数的参数不合格这一事实
const
与编译器是否可以假定函数不修改结构无关,因为在 C 标准中,只要const
该对象最初不是用 定义的,它就接受指针并使用结果来修改它指向的对象,这是定义的行为const
。因此,编译器必须假定函数f(const T *)
和函数都f(T *)
可以修改指向的数据,除非它可以看到它们的定义或某些扩展告知它有关函数的性质。
–
-
@EricPostpischil 我知道这一点,但在这种情况下,除了通过指针之外,没有其他方式可以访问对象,
const
而“抛弃 const”将是未定义的行为。此外,声明参数和复合文字const
也不会改变生成的程序集。godbolt.org/z/v7W8qfjsT这是一次错过的优化,就是这样。
– -
不,强制转换
const
不是未定义行为。如果一个指针以 开始T *
并被转换为const T *
(如在调用函数时自动转换),则将其转换回T *
C 2018 6.3.2.3 7 定义,“…否则,当再次转换回来时,结果应与原始指针相等…”,并且 C 标准中没有任何内容说你不能这样做。这是众所周知的,并且在一些使用strchr
和其他一些标准库例程中是众所周知的必要条件。
–
-
@EricPostpischil 无论如何,将复合文字的有效类型更改为
const
限定类型不会改变汇编代码,如发布的链接所示。在这种情况下,指针别名无关紧要,因为根据 6.7.3,“抛弃 const”将是 UB“如果尝试通过使用具有非 const 限定类型的左值来修改使用 const 限定类型定义的对象,则行为未定义。”因此,无法通过引用允许的指针转换/别名形式来解释该行为。
– -
是的,当复合文字定义为时,编译器似乎缺少优化
const
。除此之外,“由于函数没有const
限定参数,因此编译器不能假设值未被函数修改”这句话是不正确的;限定const
并不是编译器无法做出该假设的原因。不应让 C 程序员认为将实参传递给类型的参数const T *
必然会保护它。后面的句子也应该更新。
–
|
–
–
const
。因此,编译器都必须假定函数f(const T *)
和函数f(T *)
都可以修改指向的数据,除非它可以看到它们的定义或某些扩展告知它有关函数的性质。–
f(T *p)
和f(const T *p)
都可用于修改指向的内存p
。根据标准,const
指向类型的限定符仅起到建议作用:如果代码尝试修改 -qualified 左值,则实现必须发出诊断const
。但允许将指针转换为T *
并用作* (T *) p
非const
-qualified 左值,前提是对象最初未定义为const
。编译器必须假设f(const T *p)
可能会更改*p
。–
p
不会修改指向的内存p
(在没有任何其他信息的情况下,例如看到函数定义),以及参数是否已声明T *
或const T *
不会改变该内存。当您发表评论时,您不仅在与 OP 互动;每个用户都可能阅读它,并且该声明会误导他们。–
|