Will my compiler automatically reuse the return value of this method?

I'm wondering if my compiler will be smart enough to realize that the determinant() of this matrix has already been calculated and will reuse the value that was calculated earlier, or if I need to explicitly handle this to optimize the code to avoid calling determinant() twice? (ie, determinant() is being redundantly called inside of inverse(). m is not being changed between the two calls.)

My matrix code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Matrix2 {
    std::array<real_t, 4> data;
public:

...

    real_t determinant() const {
        return data[0] * data[3] - data[1] * data[2];
    }

    Matrix2 inverse() const {
        real_t det_inv = 1.0 / determinant();
        return Matrix2(data[3] * det_inv, -data[1] * det_inv, -data[2] * det_inv, data[0] * det_inv);
    }
};


Using the matrix later on:

1
2
3
4
5
6
7
Matrix2 m = calc_matrix();
if (abs(m.determinant()) < 1e-6) {
	//Special handling
	return;
}

Matrix2 m_inv = m.inverse();
Last edited on
Hi

Have you considered having a member of the class that the value of determinant() is assigned to? You should make it private and mutable.

Edit: To answer your question: No the compiler will not automatically reuse that value, it is a temporary.

If the determinant is calculated once, call that function in a constructor. Otherwise call as required.
Last edited on
I agree the answer is no, the compiler is not smart enough in general, but for small programs where everything is visible to the compiler all at once, it will sometimes collapse logic down if you have aggressive optimizations on (like -O3).

There's the Compiler Explorer you can use to test out theories about how the compiler will optimize piece of code: https://godbolt.org/

But I also disagree with TheIdeasMan; I seldom see it as a good idea to use mutable. Mostly because it forces you to toss out any semblance of thread safety and the compiler now cannot assume the state of the object is truly constant. I'm not saying to never use it, but I've seen it used as a way to preserve a backwards compatible interface with other components that expect a const object, while implementing caching (but those other components still have to know that things like thread safety are now out the window).

Actually, I think what I wrote was wrong or at least misleading. See kigar's response.
Last edited on
I think probably not, because determinant() and inverse() are two completely separate function calls. This would require the compiler to “save” the result of determinant() in the object instance, so that it can be re-used inside inverse() later on – which would change the memory layout of the object. Maybe it could happen if both, the call to determinant() and inverse(), get completely “inlined” at the place where they are called, so that the result of determinant() can be carried over directly in a temporary (e.g. register). But I think it is unlikely that the compiler is “smart” enough to do this. Anyhow, you won't know for sure until you look at the generated assembly code!

https://learn.microsoft.com/en-us/cpp/build/reference/fa-fa-listing-file?view=msvc-170
https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html#index-S


BTW: I think explicitly caching the determinant, as others have suggested, is the way to go here. Make determinant() “lazily” compute the value (and store it in the cache), if it hasn't been computed yet; otherwise just return the cached value. Using mutable for this kind of “transparent” cache makes perfect sense to me, because even if you have a const reference to the object and therefore you can not change the actual state of the object, a “read-only” operation, such as determinant(), may still need to update the cache.

Using mutable for an internal “cache” variable does not break thread safety – provided that the “cache” variable is not static, and provided that each thread uses its own separate object instance. Meanwhile, sharing the same object instance between concurrent threads (without a proper explicit synchronization) is never “safe”, unless the class' documentation explicitly says so!

Even if we ignore that mutable fields may exist: Having a const reference to a “shared” object does not ensure that the object cannot be modified through other non-const references elsewhere. So, even if thread #1 “reads” an object via a const reference, thread #2 may still modify that same object at the same time via a non-const reference, resulting (potentially) in an inconsistent read result.

Sharing an object instance between concurrent threads (without explicit synchronization) is “safe” only if either: (1) the object is truly immutable, or (2) the class internally/implicitly synchronizes all access (read and write) to the mutable state. Here it is important to note that, in C++, just declaring a reference as const does not magically make the underlying object immutable!

(This is different from, e.g., Rust, where the compiler enforces that there are either n non-mutable references or exactly one mutable reference to an object, but never both at the same time; so if you have a non-mutable reference to an object, you can be sure nonbody else is modifying the object, because nobody else can have a mutable reference while you are holding the non-mutable reference)
Last edited on
I wasn't being very clear, yes you put it in much better terms than I did.

sharing the same object instance between concurrent threads (without a proper explicit synchronization) is never “safe”
If the state doesn't change and both threads are looking at the same underlying memory, then I don't see why it wouldn't be safe. But I agree it's not safe just because the object is "const"; you have to know how it's implemented.

And actually, my internal thinking about this was also wrong, I was thinking that passing a matrix by reference vs const reference would perhaps produce different assembly, but no, they produce the same even with mutable if you aren't actually mutating the data the mutable part depends on.

But anyway, sorry for my side tangent: 'mutable' keyword aside, caching the determinant can be a good choice. I would calculate the determinant at construction and whenever the caller mutates the data[i], if you expect the inverse() function be called a lot more than any mutating functions.
Last edited on
Registered users can post here. Sign in or register to post.