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

2021-12-21 00:00:00 memory struct c++

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

struct Foo {int var1;int var2;Foo() { var1 = 5;std::cout <<变量 1;}};

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

解决方案

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

<块引用>

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

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

<小时>

现在想到两个问题:

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

让我们从一个例子开始.

示例

#include 结构 Foo1{ int var1 = 5;Foo1() { std::cout <<变量 1;} };结构 Foo2{ int var1 = 5;int var2;Foo2() { std::cout <<变量 1;} };void f1() { (void) Foo1{};}void f2() { (void) Foo2{};}

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

f1():mov esi, 5mov edi,偏移平面:_ZSt4coutjmp std::basic_ostream>::operator<<(int)f2():jmp f1()

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

讨论

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

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

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

bool insert(std::set& set, int value){返回 set.insert(value).second;}

这是一个未使用的数据成员(此处为 std::pair::iterator, bool>::first)的真实示例.你猜怎么着?已优化 (带有虚拟集的更简单示例,如果该程序集让您哭了.

现在是阅读 Max Langhof 的精彩回答的最佳时机(请为我点赞).它解释了为什么最终结构的概念在编译器输出的汇编级别没有意义.

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

有很多评论认为这个答案一定是错误的,因为某些操作(例如 assert(sizeof(Foo2) == 2*sizeof(int)))会破坏某些内容.>

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

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

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

1)

<块引用>

[intro.abstract]/1

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

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

Does initializing a member variable and not referencing/using it further take up RAM during runtime, or does the compiler simply ignore that variable?

struct Foo {
    int var1;
    int var2;

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

In the example above, the member 'var1' gets a value which is then displayed in the console. 'Var2', however, is not used at all. Therefore writing it to memory during runtime would be a waste of resources. Does the compiler take these kinds of situations into an account and simply ignore unused variables, or is the Foo object always the same size, regardless of whether its members are used?

解决方案

The golden C++ "as-if" rule1 states that, if the observable behavior of a program doesn't depend on an unused data-member existence, the compiler is allowed to optimized it away.

Does an unused member variable take up memory?

No (if it is "really" unused).


Now comes two questions in mind:

  1. When would the observable behavior not depend on a member existence?
  2. Does that kind of situations occurs in real life programs?

Let's start with an example.

Example

#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{}; }

If we ask gcc to compile this translation unit, it outputs:

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

f2 is the same as f1, and no memory is ever used to hold an actual Foo2::var2. (Clang does something similar).

Discussion

Some may say this is different for two reasons:

  1. this is too trivial an example,
  2. the struct is entirely optimized, it doesn't count.

Well, a good program is a smart and complex assembly of simple things rather than a simple juxtaposition of complex things. In real life, you write tons of simple functions using simple structures than the compiler optimizes away. For instance:

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

This is a genuine example of a data-member (here, std::pair<std::set<int>::iterator, bool>::first) being unused. Guess what? It is optimized away (simpler example with a dummy set if that assembly makes you cry).

Now would be the perfect time to read the excellent answer of Max Langhof (upvote it for me please). It explains why, in the end, the concept of structure doesn't make sense at the assembly level the compiler outputs.

"But, if I do X, the fact that the unused member is optimized away is a problem!"

There have been a number of comments arguing this answer must be wrong because some operation (like assert(sizeof(Foo2) == 2*sizeof(int))) would break something.

If X is part of the observable behavior of the program2, the compiler is not allowed to optimized things away. There are a lot of operations on an object containing an "unused" data-member which would have an observable effect on the program. If such an operation is performed or if the compiler cannot prove none is performed, that "unused" data-member is part of the observable behavior of the program and cannot be optimized away.

Operations that affect the observable behavior include, but are not limited to:

  • taking the size of a type of object (sizeof(Foo)),
  • taking the address of a data member declared after the "unused" one,
  • copying the object with a function like memcpy,
  • manipulating the representation of the object (like with memcmp),
  • qualifying an object as volatile,
  • etc.

1)

[intro.abstract]/1

The semantic descriptions in this document define a parameterized nondeterministic abstract machine. This document places no requirement on the structure of conforming implementations. In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming implementations are required to emulate (only) the observable behavior of the abstract machine as explained below.

2) Like an assert passing or failing is.

相关文章