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

Chr*_*888 88 c++ memory struct

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

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};
Run Code Online (Sandbox Code Playgroud)

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

YSC*_*YSC 103

金色的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{}; }
Run Code Online (Sandbox Code Playgroud)

如果我们要求gcc编译此翻译单元,则输出:

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

f2与相同f1,并且没有内存被用来存放实际数据Foo2::var2。(Clang做类似的事情)。

讨论区

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

  1. 这是一个微不足道的例子,
  2. 该结构已完全优化,不算在内。

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

bool insert(std::set<int>& set, int value)
{
    return set.insert(value).second;
}
Run Code Online (Sandbox Code Playgroud)

这是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)就像断言是通过还是失败。


Max*_*hof 60

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

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

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


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

当您使函数与外界的交互对于编译器而言更加复杂/不清楚时(采用/返回更复杂的数据结构,例如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];
}
Run Code Online (Sandbox Code Playgroud)

我们在这里做了一些琐碎的事情(从字节表示中获取地址,检查并添加字节),但是优化器可以确定在该平台上结果始终是相同的:

test(): # @test()
  mov eax, 7
  ret
Run Code Online (Sandbox Code Playgroud)

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

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

  • 麦克风放下_“有关详细信息,请咨询编译器手册。” _:D (5认同)

Ala*_*les 22

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

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

  • @AlanBirtles我看不出有什么不同。编译器优化了对象中的所有内容,这些内容对程序的可观察行为没有影响。因此,您的第一句话“编译器极不可能优化未使用的成员变量”。 (4认同)
  • @YSC我认为情况略有不同,它完全优化了结构,仅直​​接打印5张 (3认同)
  • 在实际代码中使用@YSC,实际上是在使用结构,而不是仅仅出于副作用而构造结构,它可能不太可能被优化 (2认同)

Han*_*999 7

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

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

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

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

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

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

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

ato*_*bol 6

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

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

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

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

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