C++ Can constant class data be optimized out of class by compiler?
Asked Answered
H

3

3

I know that constant variables outside classes can be optimized directly into function calls by the compiler, but is it legal for the compiler to do the same for constant class variables?

If there is a class declared like this:

class A {
public:
const int constVar;
    //other, modifiable variables

A(int val): constVar(val) {
         //code to initialize modifiable variables

}
};

and I create an instance of A and call a function like this:

A obj(-2);
int absoluteVal = std::abs(A.constVar);

is the compiler allowed to do this instead and make the class be sizeof(int) smaller?:

A obj();
int absoluteVal = std::abs(-2);
Hijoung answered 31/7, 2017 at 14:5 Comment(8)
You can check what your compiler does with various flags at godbolt.orgLavonna
Don't think the optimzer is allowed to change the sizeof(A).Babb
@RickAstley Perhaps not, but it may inline const class members. You can inline without eliminating the original function or object.Lavonna
Do not mix compile-time constant with constant!Organza
Possible duplicate of What exactly is the "as-if" rule?Enforcement
and the answer is yes; I've seen entire classes be optimised away with trivial examples, and presumably this can become distinctly non-trivial as the optimisation level rises and things like LTO are factored in. You should be able to test this for yourself using simple examples, as I did.Enforcement
Can you confirm that you are interested in the general rule of whether optimizer could remove a const from the memory layout of a class (which could be more complex than your simplified example) and not in specific code generation related to your simplified example ?Flofloat
@Flofloat I tried to convey that with the comments inside the class.Hijoung
U
9

The compiler is free to emit any code that preserves the "observable behavior" of the program (there is an exception with copy constructor which can be elided even if it has observable behavior, but it doesn't apply here). This is called the as if rule

struct X { int x; };

auto foo()
{
  X x{24};

  return x.x;
}

any decent compiler will optimize the above to this:

foo():                                # @foo()
        mov     eax, 24
        ret

As you can see, it doesn't have anything to do with constness (well, almost), just with observable behavior. You can try and play with the code adding in complexity and see how smart is the compiler in figuring out it can remove the code related to the class instance. Hint: it is very smart.


Is not clear to me what you mean by this:

is the compiler allowed to do this instead and make the class be sizeof(int) smaller?:

I can tell you that: for a type X and an object x of such type sizeof(x) is always = sizeof(X) regardless of instantiations of the class. In other words the size of the class is determined when the class is defined, and as such it is not influenced by possible instantiations or lack of. The size of a class is the sum of all the sizes of its non-static data members plus padding. The padding is implementation defined and can be usually be somewhat controlled (e.g. packed structs). So no, the size of a class can never be smaller than the sum of the sizes all it's non-static non-reference data members.

Untaught answered 31/7, 2017 at 14:20 Comment(3)
Constness still has something to do with it. The optimizer has to prove that removing code won't have observable behaviour. And it is often much easier to prove that a const object won't change value, than non-const. In this example, the proof is quite easy without constness of course.Marlborough
@user2079303 I think you'll find this interesting: Understanding Compiler Optimization - Chandler Carruth - Opening Keynote Meeting C++ 2015 - and I think you'll find that he disagrees with you; const is basically useless to the optimizer.Subchloride
While you are completely right on everything you say, you don't really answer the general question of whether the compiler is allowed to shorten the object. Ok, here, constant propagation + dead code elimination + absence of other observable behavior allows the instantiation to be optimized away. But what if the object would really be used somewhere (e.g. if the object would be written to a file). FOr example here godbolt.org/g/FZ1WiV you see that the object is instantiated and the constant initialized despite being optimized away in the computation.Flofloat
S
2

It would be perfectly legal for the compiler to generate

int absoluteVal = 2;

If abs has no side effects. It all hinges on "observable behaviour" (the as-if rule). If you cannot tell from the outside that the compiler made some transformation, then it is legal for the compiler to make that transformation.

Subchloride answered 31/7, 2017 at 14:10 Comment(2)
Yes, indeed. But OP asks also if the compiler would be allowed to make the object shorterFlofloat
@Flofloat making the object smaller would be pretty obviously observable. So, as far as I know, the answer would be no.Subchloride
F
2

Code optimizing and object memory layout don't obey the same rules

The C++ standard states the following about the memory layout of the objects:

1.8/2: Objects can contain other objects, called subobjects. A subobject can be a member subobject, a base class subobject, or an array element. (...)

9.2/13: Nonstatic data members of a (non-union) class with the same access control are allocated so that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified. Implementation alignment requirements might cause two adjacent members not to be allocated immediately after each other; so might requirements for space for managing virtual functions and virtual base classes.

This guarantees that non static const members (which are data members, even if they are const) are contained within the object. So the compiler is not allowed to shorten the size of an object.

However, the compiler is authorized to perform code optimization such as constant propagation and dead code elimination, reordering, etc. as long as the observable behavior is not altered:

1.9/5: A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input. (...)

So if your const member is not volatile nor atomic<>, the compiler can very well generate

A obj();              // size not touched.  And const member will be initialized if needed
int absoluteVal = 2;  // constant propagation + inlining (the object is not even accessed)

Additional information

Here an example where the object can't be optimized away :

A obj(-2);                                 // object is constructed
int absoluteVal = std::abs(obj.constVar);  // will be optimized a way into = 2 
std::cout<<absoluteVal<<std::endl;
size_t lo = sizeof(obj);
std::cout<<lo<<std::endl; 
std::cout.write((char*)&obj, lo);         // obj is written to a stream 
                                          // and output of content at &obj adress is observable behavior

You can see online on the optimizer results : despite the computation of absoluteVal being optimized away, the object is instantiated in its full length and its constant is initialized:

    ...
    mov     esi, 2                      ; this is absoluteVal calculation
    mov     DWORD PTR [rsp+12], -2      ; the const in [rsp+12] object is nevertheless initialized
    ...
    lea     rsi, [rsp+12]               ; the address of the object 
    mov     edx, 4                      ; and its length 
    ...                                 ; are used to call cout.write()
    call    std::basic_ostream<char, std::char_traits<char> >::write(char const*, long) 

This is because the observable behavior of writing this trivially copyable object to a stream requires every byte of the object to fit to the expectations.

Flofloat answered 31/7, 2017 at 14:31 Comment(2)
Shorten the size, no, but it can get rid of obj altogether... as bolov showed, the instantiation of the class can be elided completely if it is not used for anything (i.e. anything else, after constant propagation)Enforcement
@Enforcement yes, but this is a different thing : the elision of the instantiation is result of dead code elimination after constant propagation. This works in this specific example, because the object is not used for any other purpose. But the instantiation would not be eliminated if the address of the object would be used in an std::ostream::write() because the object would be needed to ensure the observable behavior. It would not be eliminated either it the constructor would do some observable behaviors such as side effects instead of just initializing the const.Flofloat

© 2022 - 2024 — McMap. All rights reserved.