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 < 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 < 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<double, std::milli> ms_double = t2 - t1;
        std::cout << std::endl;
        std::cout << ms_double.count() << "ms\n";
    }

    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.

5
  • 2
    Which compiler and which optimization options did you use?
    – Doc Brown
    Commented Feb 27, 2022 at 22:43
  • 6
    Such performance differences are generally due to a compilation in debugging mode, without optimization. With optimizing on, especially with the global optimization, The compiler would inline such code and gereate the same code for both
    – Christophe
    Commented Feb 27, 2022 at 22:59
  • I used no optimization flags and compiled it with GNU (MinGW).
    – darclander
    Commented Feb 28, 2022 at 8:28
  • I realize my question might not relate to an actual scenario but wondered if there are moments where for example using a method a large number of times might actually affect performance (due to the cost of a function call).
    – darclander
    Commented Feb 28, 2022 at 8:32
  • Just a quick remark on something Christophe's excellent answer didn't touch on: often, class' own getters are not used from inside the class (you'd just access the member field directly), but sometimes it might be useful to do so - it lets you do things like potentially change the data format of the backing field without affecting the method, or return a default value if the backing field is null, or other such shenanigans. That is, sometimes it's useful to introduce a level of indirection even within the class itself, but this shouldn't be overused. Commented Feb 28, 2022 at 17:45

1 Answer 1

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 ;-)

5
  • This is a great answer! I would say that "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). " this is what I wondered about. The rest of the question really just helps explain it all.
    – darclander
    Commented Feb 28, 2022 at 8:26
  • A sidenote though: I am not very familiar with Compilers or how they work but let us say that we write a recursive function and a for loop (both doing the same thing two different ways) which might be a bit too complex for the compiler to optimize, how would we know which way is better without measuring the time etc? Do we just "trust" the compiler?
    – darclander
    Commented Feb 28, 2022 at 8:27
  • 2
    @darclander This is an interesting remark. Compilers can detect recursion and transform some of it in efficient loops. However in the general case, removing the recursion is a more complex transformation that leads to a different algorithm and not just an optimization of code. Here we can’t expect to much.
    – Christophe
    Commented Feb 28, 2022 at 9:13
  • 1
    thank you for taking your time! I find it very interesting to hear what others say or how the industry works. I will just remain with trying to make readability my main concern instead ;)
    – darclander
    Commented Feb 28, 2022 at 11:20
  • It's a good answer, except the first part about hiding internal state and allowing to change it. Getters obviously don't do any of that. At most you can argue it hides the physical instance variable the state is held in, but not the state itself. Also, you can't easily change the internal state after you publish it. Realistically you can only do isomorphic changes, ones that are 1-to-1 mappable to the previous values. Commented Feb 28, 2022 at 20:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.