Home > Back-end >  Why don't c compilers replace this access to a const class member with its value known at com
Why don't c compilers replace this access to a const class member with its value known at com

Time:12-30

In this code snippet, why don't c compilers just return 1 when compiling test(), but read the value from memory?

struct Test {
  const int x = 1;

  // Do not allow initializing x with a different value
  Test() {}
};
int test(const Test& t) {
  return t.x; 
}

Code on golbolt

Compiler output:

test(Test const&):                         # @test(Test const&)
    mov     eax, dword ptr [rdi]
    ret

I would have expected:

test():                               # @test(Test const&)
    mov     eax, 1
    ret

Is there any standard-compliant way to modify the value of Test::x to contain a different value than 1? Or would the compilers be allowed to do this optimization, but neither gcc nor clang have implemented it?

EDIT: Of course you immediately found my mistake in making this a minimum example, that is allowing aggregate initialization for the struct. I updated the code with an empty default constructor that prevents that. (Old code on godbolt)

CodePudding user response:

I believe it's because you can still construct an instance of Test with other values of x with an initializer list like this:

Test x{2};
cout << test(x);

Demo: https://www.ideone.com/7vlCmX

CodePudding user response:

In your case it means that you have a non modifiable variable which will be set to a given value if not given by any other method. But there are at minimum two other methods like:

struct X {
    const int y = 1;
};
int test(const X& t) {
    return t.y;
}

struct Y: public X
{
    Y():X{9}{}
};

int main()
{
    X x1{3};
    std::cout << test(x1) << std::endl;

    Y y1{};
    std::cout << test(y1) << std::endl;

}

see it working

If you want to say: My type always have the same constant, you simply should write static constexpr int x = 1; which is a totally different semantic. And if you do that, the assembly will be what you expected.

CodePudding user response:

To make this optimization happen you need to tell compiler that x cannot have different value in any case:

struct Test {
    constexpr
    static int x = 1;
};

int test(const Test& t) {
    return t.x;
}

godbolt output

test(Test const&):                         # @test(Test const&)
        mov     eax, 1
        ret

CodePudding user response:

Now you've disallowed using a constructor to create an instance of a Test object with a different x value, but gcc/clang still aren't optimizing.

It may be legal to use char* or memcpy to create an object-representation of a Test object with a different x value. That would make the optimization illegal.

For example,

   Test foo;
   char buf[sizeof(foo)];
   memcpy(buf, &foo, sizeof(foo));
   buf[0] = 3;         // on a little-endian system like x86, this is buf.x = 3;  - the upper bytes stay 0
   memcpy(&foo, buf, sizeof(foo));

The only questionable step is the final memcpy back into foo; this is what creates a Test object with an x value the constructor couldn't produce. In reference contexts in C , const means you can't modify this object through this reference. I don't know how that applies for a const member of a non-const object.

We can see from this example that GCC and clang leave room for non-inline function calls to modify that member of an already-constructed Test object:

void ext(void*);  // might do anything to the pointed-to memory

int test() {
    Test foo;    // construct with x=1
    ext (&foo);
    return foo.x;
}

Godbolt

# GCC11.2 -O3.  clang is basically equivalent.
test():
        sub     rsp, 24             # stack alignment   wasted 16 bytes
        lea     rdi, [rsp 12]
        mov     DWORD PTR [rsp 12], 1      # construct with x=1
        call    ext(void*)
        mov     eax, DWORD PTR [rsp 12]    # reload from memory, not mov eax, 1
        add     rsp, 24
        ret

It may or may not be a missed optimization. Many missed-optimizations are things compilers don't look for because it would be computationally expensive (even an ahead-of-time compiler can't use exponential-time algorithms carelessly on potentially-large functions).

This doesn't seem too expensive to look for, though, just checking if a constructor default has no way to be overridden. Although it seems lowish in value in terms of making faster / smaller code since hopefully most code won't do this.

It's certainly a sub-optimal way to write code, because you're wasting space in each instance of the class holding this constant. So hopefully it doesn't appear often in real code-bases. static constexpr is idiomatic and much better than a const per-instance member object if you intentionally have a per-class constant.

However, constant-propagation can be very valuable, so even if it only happens rarely, it can open up major optimizations in the cases it does.

  • Related