未使用的数据成员是否占用内存



初始化数据成员而不引用/使用它是否会在运行时进一步占用内存,还是编译器只是忽略该成员?

struct Foo {
int var1;
int var2;
Foo() : var1{5} {
std::cout << var1;
}
};

在上面的示例中,成员var1获取一个值,该值随后显示在控制台中。 然而,var2根本没有使用。因此,在运行时将其写入内存将浪费资源。

编译器是否将这些情况纳入帐户并简单地忽略未使用的数据成员,或者无论是否使用其成员,Foo对象的大小是否始终相同?

黄金C++"as-if"规则1指出,如果程序的可观察行为不依赖于未使用的数据成员存在,则允许编译器对其进行优化

未使用的成员变量会占用内存吗?

否(如果它"真的"未使用)。


现在想到两个问题:

  1. 何时可观察的行为不依赖于成员的存在?
  2. 这种情况在现实生活中会发生吗?

让我们从一个例子开始。

#include <iostream>
struct Foo1
{ int var1 = 5;           Foo1() { std::cout << var1; } };
struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };
void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }

如果我们要求 gcc 编译这个翻译单元,它会输出:

f1():
mov     esi, 5
mov     edi, OFFSET FLAT:_ZSt4cout
jmp     std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
jmp     f1()

f2f1相同,并且没有内存用于保存实际的Foo2::var2。(Clang做了类似的事情)。

讨论

有些人可能会说这是不同的,原因有两个:

  1. 这个例子太微不足道了,
  2. 结构完全优化,不算数。

好吧,一个好的程序是简单事物的智能和复杂组合,而不是复杂事物的简单并置。在现实生活中,你使用简单的结构编写大量简单的函数,而不是编译器优化的。例如:

bool insert(std::set<int>& set, int value)
{
return set.insert(value).second;
}

这是数据成员(此处为std::pair<std::set<int>::iterator, bool>::first)未使用的真实示例。你猜怎么着?它被优化了(如果该程序集让你哭泣,则使用虚拟集的更简单示例)。

现在是阅读Max Langhof的出色答案的最佳时机(请为我投票)。它解释了为什么最终,结构的概念在编译器输出的程序集级别没有意义。

"但是,如果我做X,那么未使用的成员被优化掉的事实是一个问题!">

有许多评论认为这个答案一定是错误的,因为某些操作(如assert(sizeof(Foo2) == 2*sizeof(int)))会破坏某些东西。

如果 X 是程序2可观察行为的一部分,则不允许编译器优化事物。在包含"未使用"数据成员的对象上有很多操作,这些操作会对程序产生可观察的影响。如果执行了这样的操作,或者编译器无法证明没有执行任何操作,则该"未使用"的数据成员是程序可观察行为的一部分,无法优化。

影响可观察行为的操作包括但不限于:

  • 取一种对象的大小(sizeof(Foo)),
  • 获取在"未使用"数据成员之后声明的数据成员的地址,
  • 使用类似memcpy的函数复制对象,
  • 操作对象的表示(如memcmp),
  • 将对象限定为易失性对象,
  • 等等

1)

[intro.abstract]/1

本文档中的语义描述定义了参数化的不确定抽象机器。本文档对符合性实现的结构没有要求。特别是,它们不需要复制或模拟抽象机器的结构。相反,需要符合要求的实现来模拟(仅)抽象机器的可观察行为,如下所述。

2)就像断言通过或失败一样。

重要的是要认识到编译器生成的代码对数据结构没有实际的了解(因为这样的东西在程序集级别不存在),优化器也不存在。编译器只为每个函数生成代码,而不生成数据结构

好的,它还写入常量数据部分等。

基于此,我们已经可以说优化器不会"删除"或"消除"成员,因为它不输出数据结构。它输出代码,这些代码可能会也可能不会使用成员,其目标之一是通过消除成员的无意义使用(即写入/读取)来节省内存或周期。


它的要点是"如果编译器可以在函数(包括内联到其中的函数)范围内证明未使用的成员对函数的运行方式(以及它返回的内容)没有影响,那么成员的存在很可能不会导致开销"。

随着你使函数与外部世界的交互对编译器来说更加复杂/不清楚(获取/返回更复杂的数据结构,例如std::vector<Foo>,在不同的编译单元中隐藏函数的定义,禁止/抑制内联等),编译器越来越有可能无法证明未使用的成员没有效果。

这里没有硬性规定,因为这完全取决于编译器所做的优化,但只要你做一些琐碎的事情(如 YSC 的答案所示),很可能不会出现开销,而做复杂的事情(例如,从太大而无法内联的函数返回std::vector<Foo>)可能会产生开销。


为了说明这一点,请考虑以下示例:

struct Foo {
int var1 = 3;
int var2 = 4;
int var3 = 5;
};
int test()
{
Foo foo;
std::array<char, sizeof(Foo)> arr;
std::memcpy(&arr, &foo, sizeof(Foo));
return arr[0] + arr[4];
}

我们在这里做了一些不平凡的事情(获取地址,检查并从字节表示中添加字节),但优化器可以发现结果在这个平台上总是相同的:

test(): # @test()
mov eax, 7
ret

Foo的成员不仅没有占据任何记忆,甚至没有Foo出现!如果还有其他无法优化的用法,例如sizeof(Foo)可能很重要 - 但仅适用于那段代码!如果所有用法都可以像这样优化,那么存在例如var3不会影响生成的代码。但即使它在其他地方使用,test()也会保持优化!

简而言之:Foo的每种用法都是独立优化的。有些可能会因为不需要的成员而使用更多内存,有些可能不会。有关更多详细信息,请参阅编译器手册。

编译器只会优化一个未使用的成员变量(尤其是公共变量),如果它可以证明删除变量没有副作用,并且程序的任何部分都不依赖于Foo的大小是相同的。

我不认为任何当前的编译器都会执行这样的优化,除非结构根本没有真正被使用。一些编译器可能至少会警告未使用的私有变量,但通常不会警告公共变量。

通常,您必须假设您得到了所请求的内容,例如,"未使用"的成员变量就在那里。

由于在您的示例中,两个成员都是public的,编译器无法知道某些代码(特别是来自其他翻译单元 = 其他 *.cpp 文件,这些文件单独编译然后链接)是否会访问"未使用"的成员。

YSC的答案给出了一个非常简单的例子,其中类类型仅用作自动存储持续时间的变量,并且没有指向该变量的指针。在那里,编译器可以内联所有代码,然后可以消除所有死代码。

如果在不同翻译单元中定义的函数之间具有接口,则编译器通常什么都不知道。这些接口通常遵循一些预定义的 ABI(例如),以便可以将不同的对象文件链接在一起而不会出现问题。通常,ABI 在使用与否时不会产生任何影响。因此,在这种情况下,第二个成员必须在内存中物理上(除非稍后被链接器消除)。

只要你在語言的範圍內,你就無法觀察到任何消除發生。如果你打电话给sizeof(Foo),你会得到2*sizeof(int)。如果创建Foo的数组,则两个连续的Foo对象的开头之间的距离始终为sizeof(Foo)个字节。

您的类型是标准布局类型,这意味着您还可以根据编译时计算的偏移量访问成员(参见offsetof宏)。此外,您可以通过使用std::memcpy复制到char数组来检查对象的逐字节表示形式。在所有这些情况下,可以观察到第二个成员在那里。

elidevar2这个问题的其他答案提供的示例基于单一优化技术:常量传播,以及随后整个结构的省略(不是仅仅var2的省略)。这是一个简单的情况,优化编译器确实实现了它。

对于非托管 C/C++ 代码,答案是编译器通常不会var2。据我所知,调试信息中不支持这样的 C/C++ 结构转换,如果结构可以在调试器中作为变量访问,则var2无法省略。据我所知,没有当前的C/C++编译器可以根据var2的省略来专门化函数,因此如果结构传递给非内联函数或从非内联函数返回,则var2不能省略。

对于带有 JIT 编译器的托管语言(如 C#/Java),编译器可能能够安全地避开var2,因为它可以精确跟踪它是否正在使用以及它是否转义为非托管代码。托管语言中结构的物理大小可以不同于向程序员报告的大小。

2019 年 C/C++ 编译器无法从结构中省略var2,除非省略整个结构变量。对于从结构中省略var2的有趣情况,答案是:不。

未来的一些C/C++编译器将能够从结构中消除var2,并且围绕编译器构建的生态系统将需要适应编译器生成的处理省略信息。

这取决于编译器及其优化级别。

在 gcc 中,如果指定-O,它将打开以下优化标志:

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdce代表 死代码消除。

您可以使用__attribute__((used))来防止 gcc 使用静态存储消除未使用的变量:

此属性附加到具有静态存储的变量,这意味着 即使变量看起来是 未引用。

当应用于C++类模板的静态数据成员时, 属性还意味着如果类 本身是实例化的。

最新更新