Does an unused member variable take up memory?
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?
Solution 1:
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:
- When would the observable behavior not depend on a member existence?
- 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:
- this is too trivial an example,
- 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.
Solution 2:
It's important to realize that the code the compiler produces has no actual knowledge of your data structures (because such a thing doesn't exist on assembly level), and neither does the optimizer. The compiler only produces code for each function, not data structures.
Ok, it also writes constant data sections and such.
Based on that, we can already say that the optimizer won't "remove" or "eliminate" members, because it doesn't output data structures. It outputs code, which may or may not use the members, and among its goals is saving memory or cycles by eliminating pointless uses (i.e. writes/reads) of the members.
The gist of it is that "if the compiler can prove within the scope of a function (including functions that were inlined into it) that the unused member makes no difference for how the function operates (and what it returns) then chances are good that the presence of the member causes no overhead".
As you make the interactions of a function with the outside world more complicated/unclear to the compiler (take/return more complex data structures, e.g. a std::vector<Foo>
, hide the definition of a function in a different compilation unit, forbid/disincentivize inlining etc.), it becomes more and more likely that the compiler cannot prove that the unused member has no effect.
There are no hard rules here because it all depends on the optimizations the compiler makes, but as long as you do trivial things (such as shown in YSC's answer) it's very likely that no overhead will be present, whereas doing complicated things (e.g. returning a std::vector<Foo>
from a function too large for inlining) will probably incur the overhead.
To illustrate the point, consider 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];
}
We do non-trivial things here (take addresses, inspect and add bytes from the byte representation) and yet the optimizer can figure out that the result is always the same on this platform:
test(): # @test()
mov eax, 7
ret
Not only did the members of Foo
not occupy any memory, a Foo
didn't even come into existence! If there are other usages that can't be optimized then e.g. sizeof(Foo)
might matter - but only for that segment of code! If all usages could be optimized like this then the existence of e.g. var3
does not influence the generated code. But even if it is used somewhere else, test()
would remain optimized!
In short: Each usage of Foo
is optimized independently. Some may use more memory because of an unneeded member, some may not. Consult your compiler manual for more details.
Solution 3:
The compiler will only optimise away an unused member variable (especially a public one) if it can prove that removing the variable has no side effects and that no part of the program depends on the size of Foo
being the same.
I don't think any current compiler performs such optimisations unless the structure isn't really being used at all. Some compilers may at least warn about unused private variables but not usually for public ones.
Solution 4:
In general, you have to assume that you get what you have asked for, for example, the "unused" member variables are there.
Since in your example both members are public
, the compiler cannot know if some code (particularly from other translation units = other *.cpp files, which are compiled separately and then linked) would access the "unused" member.
The answer of YSC gives a very simple example, where the class type is only used as a variable of automatic storage duration and where no pointer to that variable is taken. There, the compiler can inline all the code and can then eliminate all the dead code.
If you have interfaces between functions defined in different translation units, typically the compiler does not know anything. The interfaces follow typically some predefined ABI (like that) such that different object files can be linked together without any problems. Typically ABIs do not make a difference if a member is used or not. So, in such cases, the second member has to be physically in the memory (unless eliminated out later by the linker).
And as long as you are within the boundaries of the language, you cannot observe that any elimination happens. If you call sizeof(Foo)
, you will get 2*sizeof(int)
. If you create an array of Foo
s, the distance between the beginnings of two consecutive objects of Foo
is always sizeof(Foo)
bytes.
Your type is a standard layout type, which means that you can also access on members based on compile-time computed offsets (cf. the offsetof
macro). Moreover, you can inspect the byte-by-byte representation of the object by copying onto an array of char
using std::memcpy
. In all these cases, the second member can be observed to be there.