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


91

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

struct Foo {
    int var1;
    int var2;

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

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


25
这取决于编译器,体系结构,操作系统和使用的优化。
猫头鹰

16
那里有大量的低级驱动程序代码,它们专门添加了空操作结构成员以进行填充以匹配硬件数据帧大小,并作为黑客获得所需的内存对齐。如果编译器开始对其进行优化,将会有很多破损。
安迪·布朗

2
@Andy实际上并没有做任何事情,因为对以下数据成员的地址进行了评估。这意味着这些填充成员的存在确实在程序上具有可观察到的行为。在这里,var2不是。
YSC

4
如果编译器可以优化它,因为处理这种结构的任何编译单元都可能使用相同的结构链接到另一个编译单元,并且编译器不知道单独的编译单元是否寻址该成员,我会感到惊讶。
加里克

2
@geza sizeof(Foo)不能按定义减少-如果打印,sizeof(Foo)则必须屈服8(在常见平台上)。即使在没有LTO或整个程序优化的情况下,编译器也可以在他们认为合理的任何情况下优化占用的空间var2(无论是通过new堆栈,在堆栈上还是在函数调用中……)。在不可能的地方,他们不会像其他任何优化那样这样做。我认为,对已接受答案的编辑会大大减少被它误导的可能性。
Max Langhof

Answers:


106

金色的C ++“假设”规则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()

f2与相同f1,并且没有内存被用来存放实际的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),
  • 将对象限定为volatile

1)

[intro.abstract]/1

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

2)就像断言是通过还是失败。


聊天中已记录了改善答案的评论。
科迪·格雷

1
即使assert(sizeof(…)…)实际上并没有限制编译器,它也必须提供一个sizeof允许代码使用类似功能的代码memcpy,但这并不意味着编译器就需要使用那么多字节,除非它们可能暴露于这样的memcpy情况下。 “T重写反正产生正确的值。
戴维斯·鲱鱼

@戴维斯绝对。
YSC

63

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

好的,它还会写入常量数据段等。

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


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

当您使函数与外界的交互对于编译器而言更加复杂/不清楚时(采用/返回更复杂的数据结构,例如a 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都是独立优化的。由于不需要成员,有些可能会使用更多的内存,有些可能不会。有关更多详细信息,请查阅您的编译器手册。


6
麦克风滴“请咨询您的编译器手册以获取更多详细信息”。:D
YSC

22

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

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


1
但是确实如此:godbolt.org/z/UJKguS +没有编译器会警告未使用的数据成员。
YSC

@YSC clang ++会警告未使用的数据成员和变量。
Maxim Egorushkin

3
@YSC我认为情况略有不同,它完全优化了结构,仅直​​接打印5张照片
Alan Birtles

4
@AlanBirtles我看不出有什么不同。编译器优化了对象的所有内容,这些内容对程序的可观察行为没有影响。因此,您的第一句话“编译器极不可能优化未使用的成员变量”。
YSC

2
在实际代码中使用@YSC,实际上是在使用该结构,而不是仅仅出于副作用而构造该结构,可能更不可能对其进行优化
Alan Birtles

7

通常,您必须假定您已获得所需的内容,例如,“未使用的”成员变量在那里。

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

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

如果在不同转换单元中定义的函数之间具有接口,则通常编译器什么都不知道。接口遵循通常一些预定义的ABI(如),使得不同的目标文件可以链接在一起,而没有任何问题。通常,无论是否使用成员,ABI都不起作用。因此,在这种情况下,第二个成员必须物理上位于内存中(除非以后由链接程序删除)。

而且,只要您在语言的范围之内,就无法观察到任何消除现象的发生。如果您打电话sizeof(Foo),您会得到2*sizeof(int)。如果创建的数组Foo,则两个连续对象的开始之间的距离Foo始终为sizeof(Foo)字节。

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


评论不作进一步讨论;此对话已转移至聊天
科迪·格雷

2
+1:对于局部结构对象未完全优化的情况,只有积极地进行整个程序的优化才能调整数据布局(包括编译时大小和偏移量)。 gcc -fwhole-program -O3 *.c理论上可以做到,但实际上可能不会。(例如,如果程序sizeof()对目标的确切值进行了一些假设,并且因为这是一个非常复杂的优化,程序员可以根据需要手动执行。)
Peter Cordes

6

该问题的其他答案提供的示例var2都是基于一种优化技术的:恒定传播,以及随后整个结构的省略(不是just的省略var2)。这是简单的情况,优化编译器可以实现它。

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

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

var2除非忽略整个struct变量,否则2019年C / C ++编译器无法从struct中退出。对于var2从结构中删除的有趣情况,答案是:否。

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


1
您关于调试信息的段落可以归结为“如果使调试更加困难,我们将无法对其进行优化”,这是完全错误的。或者我读错了。你能澄清一下吗?
Max Langhof

如果编译器发出有关该结构的调试信息,则它将无法忽略var2。选项包括:(1)如果不对应于该结构的物理表示,则不发出调试信息,(2)在调试信息中支持结构成员
省略

也许更笼统的是参考骨料的标量替换(然后清除死存储)。
戴维斯·鲱鱼

4

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

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

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

-fdce代表消除死代码

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

该属性附加到具有静态存储的变量中,这意味着即使看起来未引用该变量,也必须发出该变量。

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


这是针对静态数据成员的,不是针对未使用的按实例的成员的(除非整个对象都这样做,否则它们不会被优化)。但是,是的,我想这确实很重要。顺便说一句,消除未使用的静态变量并不是消除死代码,除非GCC修改了该术语。
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.