【问题标题】:Does an unused member variable take up memory?未使用的成员变量是否占用内存?
【发布时间】:2019-03-08 10:03:04
【问题描述】:

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

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};

在上面的示例中,成员 'var1' 获取一个值,然后将其显示在控制台中。但是,根本不使用“Var2”。因此,在运行时将其写入内存会浪费资源。编译器是否考虑到这些情况并简单地忽略未使用的变量,或者 Foo 对象总是相同的大小,而不管其成员是否被使用?

【问题讨论】:

  • 这取决于编译器、架构、操作系统和使用的优化。
  • 有大量的低级驱动程序代码专门添加无操作结构成员以进行填充以匹配硬件数据帧大小,并作为获得所需内存对齐的技巧。如果编译器开始优化这些,就会有很多损坏。
  • @Andy 他们并不是真的什么都不做,因为评估了以下数据成员的地址。这意味着这些填充成员的存在确实对程序有可观察的行为。在这里,var2 没有。
  • 如果编译器可以优化它,我会感到惊讶,因为任何寻址这种结构的编译单元都可能链接到使用相同结构的另一个编译单元,并且编译器不知道单独编译是否单位地址成员与否。
  • @geza sizeof(Foo) 不能根据定义减少 - 如果您打印 sizeof(Foo) 它必须产生 8(在常见平台上)。编译器可以在他们认为合理的任何上下文中优化var2 使用的空间(无论是通过new 还是在堆栈上或在函数调用中...),即使没有 LTO 或整个程序优化。在不可能的情况下,他们不会这样做,就像几乎任何其他优化一样。我相信对已接受答案的修改大大降低了被它误导的可能性。

标签: c++ memory struct


【解决方案1】:

黄金 C++“as-if”规则1 指出,如果程序的 observable behavior 不依赖于未使用的数据成员的存在,则允许编译器将其优化掉

未使用的成员变量是否占用内存?

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


现在想到两个问题:

  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 to compile this translation unit,它会输出:

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 does something similar)。

讨论

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

  1. 这个例子太琐碎了,
  2. 结构已完全优化,不算数。

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

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

这是未使用数据成员(此处为std::pair&lt;std::set&lt;int&gt;::iterator, bool&gt;::first)的真实示例。你猜怎么了? It is optimized awaysimpler example with a dummy set,如果那个程序集让你哭了)。

现在是read the excellent answer of Max Langhof 的最佳时机(请为我投票)。它最终解释了为什么结构的概念在编译器输出的汇编级别上没有意义。

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

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

如果 X 是程序可观察行为的一部分2,则不允许编译器将其优化掉。对包含“未使用”数据成员的对象有很多操作,这将对程序产生明显的影响。如果执行了此类操作,或者编译器无法证明没有执行任何操作,则“未使用”数据成员是程序可观察行为的一部分并且无法优化

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

  • 获取某类对象的大小 (sizeof(Foo)),
  • 获取在“未使用”之后声明的数据成员的地址,
  • 使用memcpy之类的函数复制对象,
  • 操纵对象的表示(如memcmp),
  • 将对象限定为volatile

1)

[intro.abstract]/1

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

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

【讨论】:

  • 建议改进答案的评论是archived in chat
  • 即使assert(sizeof(…)…) 实际上也不会约束编译器——它必须提供一个sizeof 允许使用memcpy 之类的代码工作,但这并不意味着编译器在某种程度上需要使用这么多字节,除非它们可能暴露于这样的memcpy,以至于它无法重写以产生正确的值。
  • @Davis 绝对。
【解决方案2】:

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

好的,它也写常量数据段之类的。

基于此,我们已经可以说优化器不会“删除”或“消除”成员,因为它不输出数据结构。它输出代码,可能使用成员,其目标之一是通过消除无意义的使用(即写入/reads) 的成员。


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

当您使编译器与外部世界的交互变得更加复杂/不清楚时(获取/返回更复杂的数据结构,例如std::vector&lt;Foo&gt;,将函数的定义隐藏在不同的编译单元中,禁止/disincentivize inlining 等),编译器无法证明未使用的成员无效的可能性越来越大。

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


为了说明这一点,请考虑this example

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];
}

我们在这里做了一些重要的事情(从byte representation 获取地址、检查和添加字节),但优化器可以发现在这个平台上结果总是相同的:

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

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

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

【讨论】:

  • Mic drop "有关详细信息,请参阅您的编译器手册。" :D
【解决方案3】:

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

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

【讨论】:

  • 但确实如此:godbolt.org/z/UJKguS + 没有编译器会警告未使用的数据成员。
  • @YSC clang++ 会警告未使用的数据成员和变量。
  • @YSC 我认为情况略有不同,它完全优化了结构,直接打印 5
  • @AlanBirtles 我看不出有什么不同。编译器优化了对象中对程序的可观察行为没有影响的所有内容。所以你的第一句话“编译器不太可能优化 awau 一个未使用的成员变量”是错误的。
  • @YSC 在实际代码中实际使用该结构,而不是仅仅为它的副作用而构建它可能更不可能被优化掉
【解决方案4】:

一般来说,你必须假设你得到了你所要求的,例如,“未使用”的成员变量就在那里。

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

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

如果您在不同翻译单元中定义的函数之间有接口,通常编译器什么都不知道。这些接口通常遵循一些预定义的 ABI(如that),这样不同的目标文件可以毫无问题地链接在一起。通常,无论是否使用成员,ABI 都不会产生影响。因此,在这种情况下,第二个成员必须在物理上位于内存中(除非稍后被链接器消除)。

只要你在语言的范围内,你就无法观察到任何消除的发生。如果您拨打sizeof(Foo),您将收到2*sizeof(int)。如果创建Foos 的数组,则Foo 的两个连续对象的起点之间的距离始终为sizeof(Foo) 字节。

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

【讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • +1:只有积极的全程序优化才能在局部结构对象未完全优化的情况下调整数据布局(包括编译时大小和偏移量),. gcc -fwhole-program -O3 *.c 理论上可以做到,但实际上可能不会。 (例如,如果程序对 sizeof() 在此目标上的确切值做出一些假设,并且因为这是一个非常复杂的优化,程序员如果需要的话应该手动进行。)
【解决方案5】:

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

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

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

2019 年 C/C++ 编译器不能从结构中删除 var2,除非整个结构变量都被删除。对于从结构中省略 var2 的有趣案例,答案是:否。

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

【讨论】:

  • 您关于调试信息的段落归结为“如果这会使调试变得更加困难,我们将无法对其进行优化”,这是完全错误的。或者我误读了。你能澄清一下吗?
  • 如果编译器发出有关结构的调试信息,则它不能省略 var2。选项有:(1)如果与结构的物理表示不对应,则不发出调试信息,(2)在调试信息中支持结构成员省略并发出调试信息
  • 也许更一般的说法是指聚合的标量替换(然后删除死存储,)。
【解决方案6】:

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

在gcc中,如果你指定-O,它会开启following optimization flags

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

-fdce 代表Dead Code Elimination

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

这个属性,附加到一个静态存储的变量上,意味着 即使看起来该变量是 未引用。

当应用于 C++ 类模板的静态数据成员时, 属性还意味着如果该类则实例化该成员 自己被实例化了。

【讨论】:

  • 这是针对 static 数据成员的,而不是未使用的每个实例成员(除非整个对象都这样做,否则它们不会被优化掉)。但是,是的,我想这很重要。顺便说一句,消除未使用的静态变量并不是死的代码消除,除非 GCC 改变了这个术语。
猜你喜欢
  • 2012-06-25
  • 2012-04-29
  • 1970-01-01
  • 1970-01-01
  • 2021-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多