0

I'm working on presentation that demonstrates various C++ optimizations and stuck with an example where const allows them.

Consider following code:

object.h

class Object {
    int i1;
    int i2;
public:
    Object(int i1_, int i2_) : i1(i1_), i2(i2_) {}

    int getI1() const { return i1; }
    int getI2() const { return i2; }

    std::pair<int, int> calculate() const {
        return std::pair<int, int>(i1 + i2, i1 * i2);
    }
};

constopt.cpp

#include <iostream>
#include "object.h"

int main() {
    Object o(10, 20);

    std::cout << o.getI1() << " + " << o.getI2()  << " = " 
                << o.calculate().first << std::endl
            << o.getI1() << " * " << o.getI2()  << " = " 
                << o.calculate().second << std::endl;

    return 0;
}

When calculate() is inlined, everything fine, G++ directly passes constants (10 and 20) to operator << caching getI1() and getI2() calls:

mov    $0xa,%esi
mov    $0x601080,%edi
callq  0x400740 <_ZNSolsEi@plt>

But when I move calculate() to a separate translation unit, it forces i1 and i2 to be fetched twice (before and after o.calculate().first):

mov    (%rsp),%esi
mov    0x4(%rsp),%r14d
mov    $0x601080,%edi
callq  0x400740 <_ZNSolsEi@plt>

I see no difference, because getI1() doesn't rely on any side effects that can be created by calculate() and Object is intended to be const even when calculate() is in separate translation unit.

Is that G++ is not smart enough or it is not eligible to perform optimizations in such cases? My speculation is that it can cache getI1() and getI2() calls came from that answer: How does const after a function optimize the program?

I use gcc version 4.8.1. I have tried both -Os and -O2.


It seems that const isn't used in GCC optimizer in this case. Instead, it does its own digging (which can't be performed in different translation units), and searches for pure and const functions. Functions can be manually marked with __attribute__((const)). Phase that eliminates extra getI*() calls is called FRE (Full Redundancy Elimination).

Community
  • 1
  • 1
myaut
  • 11,174
  • 2
  • 30
  • 62
  • Possible duplicate of http://stackoverflow.com/questions/9767395 – Moby Disk Apr 02 '15 at 19:01
  • 1
    Compilers do not optimize based on constness. Constness is only there to help the developers. `const_cast` is always there and the compiler has no way to know about it. – sbabbi Apr 02 '15 at 19:04
  • @sbabbi Can you find a reference for that? I'd be really surprised if that's the case – Shade Apr 02 '15 at 19:09
  • 1
    @Shade http://www.gotw.ca/gotw/081.htm – sbabbi Apr 02 '15 at 19:35
  • 1
    @sbabbi: `const_cast` is irrelevant, seeing as mutating a `const` object after `const_cast`ing away the naming expression's `const`ness is UB, and _does_ result in horrendous runtime oddities for precisely this reason. – Lightness Races in Orbit Apr 02 '15 at 20:02
  • 1
    Of course moving the definition of `calculate()` to another file prevents inlining, the compiler has no idea what the function does if it can't see the definition. The definition could say `return std::make_pair(rand(), rand());` – Jonathan Wakely Apr 02 '15 at 20:02
  • Your edit makes it slightly clearer that you're asking about inlining of `getI1()` and `getI2()`, not calculate. That's still because the compiler doesn't know what `calculate()` does, and as Andrey's answer below shows, it could modify the members. – Jonathan Wakely Apr 02 '15 at 20:08
  • @LightningRacisinObrit OP question is about accessing a non-const object via const function. If `Object o` in the OP code was const, then the compiler would have been allowed to perform the optimization, (if it does actually perform the optimization is matter of debate, and I expect that it will not simply because it is a stack allocated object). – sbabbi Apr 02 '15 at 20:45
  • @sbabbi Hmm... `then the compiler would have been allowed to perform the optimization`... Contradiction? And I kindof meant a reference to the standard or something. That article was a very good read though. – Shade Apr 02 '15 at 20:53
  • @Shade, the standard doesn't discuss how compilers should optimise things. – Jonathan Wakely Apr 03 '15 at 15:14

2 Answers2

2

Well, to answer your question exactly, one has to know the paticulars of implementation of the compiler. I don't, so what follows is a mere conjecture.

Object::i1 and Object::i2 aren't declared const. Therefore the compiler, without seeing the definition of Object::calculate, is obliged to presume they might change.

Object::calculate itself being const does not by itself preclude it from making changes into Object::i1 and Object::i2. Consider the following slightly modified example:

class Object {
    int i1;
    int i2;
    int* p1;
public:
    Object(int i1_, int i2_) : i1(i1_), i2(i2_), p1(&i1) {}

    int getI1() const { return i1; }
    int getI2() const { return i2; }

    std::pair<int, int> calculate() const;
};

std::pair<int, int> Object::calculate() const {
    (*p1)++;
    return std::pair<int, int>(i1 + i2, i1 * i2);
}

Moreover, o is not declared const in the first place and therefore Object::calculate is entitled to do something as rude as const_cast and get away with it!

ach
  • 2,314
  • 1
  • 13
  • 23
1

because getI1() doesn't rely on any side effects that can be created by calculate()

It might do, whether it does or not is left unspecified by the C++ standard.

Even though you write getI1() and getI2() to the stream before calling calculate(), in C++ the order of evaluation of function arguments is unspecified. That means the compiler is allowed to make all the calls to calculate() and getI1() and getI2() in any order, before doing any actual writes to std::cout. If it decides to make one or both calls to calculate() before the calls to getI1() or getI2() then it can't assume that the values of i1 and i2 haven't changed.

If you split the expression up then the compiler should be able to see that i1 and i2 cannot change until the calls to calculate():

// this statement uses i1 and i2 before they can possibly be changed
std::cout << o.getI1() << " + " << o.getI2()  << " = ";

// this statement might mutate i1 and i2
std::cout << o.calculate().first << std::endl
        << o.getI1() << " * " << o.getI2()  << " = " 
            << o.calculate().second << std::endl;

and Object is intended to be const even when calculate() is in separate translation unit.

But you didn't actually declare o to be const.

Calling a const member function does not guarantee that members can't be changed, it's more an informal contract between the caller and the function, not a guarantee.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • I though that casting away `const` is UB (which leaves compiler hands untied), and that mutable objects should be `volatile`, but I'd probably mistaken. > But you didn't actually declare o to be const. < Yes, that actually enabled caching `getI1()` values in registers (I moved code to `void print(const Object& o)`)! – myaut Apr 02 '15 at 20:21
  • Casting away `const` is UB if the object is _really_ `const` i.e. declared `const` in the first place. There is no reason to use `volatile` for `mutable` members, they are unrelated. – Jonathan Wakely Apr 02 '15 at 20:32
  • Oh, now we have `const` and _really_ `const` objects, oh, C++! Anyway, thanks for the answer, it is both cleaned my mind and helped with my presentation. – myaut Apr 02 '15 at 20:34
  • It's always been that way. You have const objects, and you have what is called a "const access path" meaning an object is being accessed through a const pointer/reference, or via a const member function. You can access const objects through a const access path, but that doesn't mean they're actually const. Or in other words, seeing the `const` keyword not does imply immutable. – Jonathan Wakely Apr 03 '15 at 15:10