2

Might be a silly question or something I might have just messed up in my head but here we go...

I saw a code example of someone using getPos() in their own class to retrieve the current position of an object instead of for example using myObj.x and myObj.y. That made me think about the use of public variables or generally using helper methods for such things.

By running the following code there was almost a 33% increase in efficiency(time) by using x = x * x instead of x = getX() * getX() (In the MyClass void update() method).

#include <iostream>
#include <chrono>

class MyClass { public: int getX() { return x; } int x = 10;

    void update() {
        for (int i = 0; i &lt; 1000000; ++i) {
            // x = x * x;
            x = getX() * getX();
        }
    }

};

int main() {

using std::chrono::high_resolution_clock;
using std::chrono::duration;

MyClass test;
for (int i = 0; i &lt; 10; ++i) {
    auto t1 = high_resolution_clock::now();
    test.update(); /* What if we would perform test.x actions here instead? */
    auto t2 = high_resolution_clock::now();

    duration&lt;double, std::milli&gt; ms_double = t2 - t1;
    std::cout &lt;&lt; std::endl;
    std::cout &lt;&lt; ms_double.count() &lt;&lt; &quot;ms\n&quot;;
}

return 0;

}

My question: Is MyClass.x = 10 equivalent to x = 10 (in terms of performance, efficiency, memory allocation etc)? If it is and getX() is a decrease in performance why do use it instead? Is it just a standard? I understand encapsulation but what would the difference be between a constant MyClass.x and a private MyClass.x? We can in both scenarios read the values?

EDIT: I compiled my program with g++ main.cpp (GNU (MinGW)) and no optimizer flags.

1 Answers1

9

The difference is encapsulation and access control

The difference between the two approaches is that getX() allows to hide the internal details of the class, and avoids that the code using it might accidentally interfere (e.g. by changing x's value unintentionally). Of course, this would require to make x private. It also allows you to freely change the internals of the class and still ensure the same behavior.

There is in reality no performance issue here:

  • You will notice a performance difference only if you compile without optimizing options (typically in debugging mode).

  • With optimizing, the compiler will generate almost identical code for both alternatives: getters are generally automatically inlined (i.e. replaced with code equivalent to a direct access to the member variable).
    By the way, your current code would produce over-optimistic results if optimized, since the constant propagation would lead the compiler to find out that x is always 10 and it would optimize away the calculation and the update loop, to just load the precomputed constant result at a nanosecond pace ;-)

Additional information about optimization

Ok, so let's look the optimized assembler on godbolt.org.

I'd simplify and remove the timing for readability, and to trick the optimizer to believe x could change in an unpredictable manner and is still used in the end:

class MyClass {
    public:
        int getX() {
            return x;
        }
        int x = 10;
        void update() {
            for (int i = 0; i < 1000000; ++i) {
                 x = x * x;               // (1)
                //x = getX() * getX();    // (2)
            }
        }
};
extern int f(); 
extern void g(int);
int main() {
    MyClass test;
    test.x = f();    // trick: now it's not constant for the optimizer
    test.update();  
    g(test.getX());  // trick: tell compiler that x is needed 
}

Now, let's look at the alternative generated by alternative 1 (direct access to x) and alternative 2 (use of a getter): the code is completely identical: the compiler inlined the getX() in update() and then inlined the update() in main().

The core of the generated calculation then looks like:

        mov     edi, eax
        mov     eax, 1000000
.L2:
        imul    edi, edi           // edi register = edi * edi
        sub     eax, 1             // decrement counter
        jne     .L2                // loop if counter not 0 

So don't prematurely optimize. Instead prefer writing readable and maintainable code and trust the optimizer to do its job. Only if the performance of optimized code are bad, should you start to worry about manual optimization. And then you'd start with profiling the code because the bottlenecks are not always where we think they are ;-)

Christophe
  • 81,699