What kind of optimization does const offer in C/C++?
Asked Answered
C

6

78

I know that where possible you should use the const keyword when passing parameters around by reference or by pointer for readability reasons. Is there any optimizations that the compiler can do if I specify that an argument is constant?

There could be a few cases:

Function parameters:

Constant reference:

void foo(const SomeClass& obj)

Constant SomeClass object:

void foo(const SomeClass* pObj)

And constant pointer to SomeClass:

void foo(SomeClass* const pObj)

Variable declarations:

const int i = 1234

Function declarations:

const char* foo()

What kind of compiler optimizations each one offers (if any)?

Coronation answered 14/12, 2014 at 5:27 Comment(11)
const on the target of a pointer or reference does not mean constant. It means that this is a read-only view of the object. Access to the same object by other means may yet change it. Only const on an object definition actually makes it immutable.Housemaster
The main point of using const is not to assist the compiler in optimizations but to protect yourself from mistakes. Actually, I doubt that any compilers rely on const-qualification to optimize the code.Nonet
@BenVoigt I know that it does not make the object constant, but only make it appear constant for the scope of this function. Maybe I didn't use the correct term to express what I mean.Coronation
The short answer is that const makes no difference to optimization; it's to help catch bugs at compile-time.Mound
C and C++ are two different languages with a common heritage. While comments about the non-existent hybrid "C/C++" might provide some insight, focusing on one or the other will be much more meaningful.Kowtko
@Matt: That's not true either. const-qualification of the object dynamic type certainly enhances optimization. However, people tend to assume that constness of the expression static type makes a difference, when it doesn't. Unless the dynamic type is const, const_cast can legally be used to strip const from the expression type and mutate the object.Housemaster
I collated information from various sources( which I updated in my answer)...However, everywhere emphasis was on "const is primarily for human, not for compilers and optimizers".Aleutian
Please make this question about a single language, it seems most of the answers have focussed on C++ so perhaps it would be best to make this question about C++ and make another about C.Caducous
FWIW I've had to study the manuals to numerous specialty processor compilers on how to write code for it's compiler to generate maximum optimization. I've always noticed there was no mention of the value of "const". Personally I'd be willing to accept a dangerous optimization level that assumes const means const as I never cast them mutable or use mutable members anyway - but I've never heard of one.Avoid
Not an optimization, but do remember that a reference to const can extend the lifetime of the referent.Dinnerware
Related (nearly duplicate): https://mcmap.net/q/131472/-does-const-correctness-give-the-compiler-more-room-for-optimization-duplicateLilialiliaceous
A
46

The following is a paraphrase of this article.

Case 1:

When you declare a const in your program,

int const x = 2;

Compiler can optimize away this const by not providing storage for this variable; instead it can be added to the symbol table. So a subsequent read just needs indirection into the symbol table rather than instructions to fetch value from memory.

Note: If you do something like:

const int x = 1;
const int* y = &x;

Then this would force compiler to allocate space for x. So, that degree of optimization is not possible for this case.

In terms of function parameters const means that parameter is not modified in the function. As far as I know, there's no substantial performance gain for using const; rather it's a means to ensure correctness.


Case 2:

"Does declaring the parameter and/or the return value as const help the compiler to generate more optimal code?"

const Y& f( const X& x )
{
    // ... do something with x and find a Y object ...
    return someY;
}

What could the compiler do better? Could it avoid a copy of the parameter or the return value?

No, as argument is already passed by reference.

Could it put a copy of x or someY into read-only memory?

No, as both x and someY live outside its scope and come from and/or are given to the outside world. Even if someY is dynamically allocated on the fly within f() itself, it and its ownership are given up to the caller.

What about possible optimizations of code that appears inside the body of f()? Because of the const, could the compiler somehow improve the code it generates for the body of f()?

Even when you call a const member function, the compiler can't assume that the bits of object x or object someY won't be changed. Further, there are additional problems (unless the compiler performs global optimization): The compiler also may not know for sure that no other code might have a non-const reference that aliases the same object as x and/or someY, and whether any such non-const references to the same object might get used incidentally during the execution of f(); and the compiler may not even know whether the real objects, to which x and someY are merely references, were actually declared const in the first place.


Case 3:

void f( const Z z )
{
    // ...
}

Will there be any optimization in this?

Yes because the compiler knows that z truly is a const object, it could perform some useful optimizations even without global analysis. For example, if the body of f() contains a call like g( &z ), the compiler can be sure that the non-mutable parts of z do not change during the call to g().

Aleutian answered 14/12, 2014 at 5:38 Comment(4)
The optimization is still possible. The language rules prohibit changing x, so its value of 1 can still be substituted anywhere the value is needed. You're right that there needs to be memory to take an address, but accesses to that memory through the name x can be skipped.Housemaster
@BenVoigt: The accesses to that memory address can only be skipped if the compiler can prove that it is indeed an access to the memory address each time the pointer is referenced. For example, consider foo(&y); bar(*y); where foo is defined in another translation unit. Since y itself is not const, the compiler cannot know whether foo changes y, therefore it cannot optimize away *y since at that place it cannot tell where y points to. But since y might still point to x, the memory address for x must exist and contain the value 1.Tatary
@celtschk: Hence the wording of my comment -- "accesses to that memory through the name x can be skipped"Housemaster
The last case is entirely independent of the const declaration: With or without the const, the compiler must (and can, since it sees the function definition!) prove that z is not written to (you can write to a const as well, with a cast!). The important thing for optimization here is the pass-per-value (i.e., per copy) semantics: The compiler has complete knowledge of the object manipulations; it can be sure that the object is not aliased elsewhere. The const has a single function here: It is decoration to indicate intent. It has no relevance whatsoever for code generation.Blindfish
N
16

Before giving any answer, I want to emphasize that the reason to use or not use const really ought to be for program correctness and for clarity for other developers more so than for compiler optimizations; that is, making a parameter const documents that the method will not modify that parameter, and making a member function const documents that that member will not modify the object of which it is a member (at least not in a way that logically changes the output from any other const member function). Doing this, for example, allows developers to avoid making unnecessary copies of objects (because they don't have to worry that the original will be destroyed or modified) or to avoid unnecessary thread synchronization (e.g. by knowing that all threads merely read and do not mutate the object in question).

In terms of optimizations a compiler could make, at least in theory, albeit in an optimization mode that allows it to make certain non-standard assumptions that could break standard C++ code, consider:

for (int i = 0; i < obj.length(); ++i) {
   f(obj);
}

Suppose the length function is marked as const but is actually an expensive operation (let's say it actually operates in O(n) time instead of O(1) time). If the function f takes its parameter by const reference, then the compiler could potentially optimize this loop to:

int cached_length = obj.length();
for (int i = 0; i < cached_length; ++i) {
   f(obj);
}

... because the fact that the function f does not modify the parameter guarantees that the length function should return the same values each time given that the object has not changed. However, if f is declared to take the parameter by a mutable reference, then length would need to be recomputed on each iteration of the loop, as f could have modified the object in a way to produce a change in the value.

As pointed out in the comments, this is assuming a number of additional caveats and would only be possible when invoking the compiler in a non-standard mode that allows it to make additional assumptions (such as that const methods are strictly a function of their inputs and that optimizations can assume that code will never use const_cast to convert a const reference parameter to a mutable reference).

Nejd answered 14/12, 2014 at 5:38 Comment(20)
This depends on whether the dynamic type of obj is known to be const, whether f takes its parameter by copy or reference, and whether the body of f is visible here. It does NOT depend on whether f taking a reference parameter is const-qualified or not.Housemaster
Your new edit (saying const reference instead of "const parameter") is much clearer. It's clearly wrong now. The transformation you mentioned is possible only if obj is created const, or the compiler can see inside the length() member function.Housemaster
I do not understand why you call your variable cached_lenght, you are making the allusion that reading a constant will make a hit and accesing normal variable will do miss on the cache?Abubekr
@BenVoigt can you expound on that? If the "length" function is marked "const", why would it be incorrect for a compiler to make this optimization? (Also please answer not referencing the way a particular compiler happens to optimize).Nejd
@cursillosonline it's cached in the sense that it is computed once before the loop rather than computed on each iteration of the loop.Nejd
@MichaelAaronSafyan: A const function can still return a different result on each call. This optimization actually requires that the function is marked idempotent or the compiler can deduce that by inspection of the function body. The parameter type doesn't help.Housemaster
@MichaelAaronSafyan const doesn't enable the optimization in your example. if the function definition is visible (or via LTO), then an optimizer has the ability to determine whether or not a function is pure. if so, it can pull out that invariant. functions in C++ are often impure. when a function with no visible definition is called, the compiler must not assume that the function is pure. note that this is all independent of const qualification.Keneth
@BenVoigt, thanks for the clarification. I've added some other assumptions in there. Is that closer to being strictly precise? You're right, a function marked "const" could -- in theory -- return a random value or something that changes (though, really, it shouldn't be marked const in such a case), though I do think you are ignoring the fact that one can instruct a compiler be significantly more aggressive in its optimizations (and make additional assumptions), which is useful when a code base is more stringent in their use of these keywords.Nejd
@MichaelAaronSafyan: Unless obj actually is a const object, even f(const ObjType&) can modify it. Either through const_cast, or via an alias (the latter can often be ruled out for local objects, but the former cannot)Housemaster
@BenVoigt but you seem to ignore that a compiler can be instructed to be more aggressive (e.g. to assume that const_cast isn't used when applying optimizations), which is actually quite reasonable in an organization that strictly prohibits its usage.Nejd
@MichaelAaronSafyan: Such a compiler mode is no longer the C++ language.Housemaster
@BenVoigt that's getting a little too academic for me. Practically speaking, compilers for the C++ language often have tweaks or flags that allow one to impose non-standard alterations or extensions to the language. I think it's more useful to discuss how C++ is actually used in the broader context rather than what is strictly within the C++ standard (unless the question is specifically about the text of the standard, itself, or explicitly says that we are talking about a strict, pedantic implementation).Nejd
@MichaelAaronSafyan: If you're claiming that this is a non-standard "extension" in common use, you'd better give an example. To the best of my knowledge no major compiler actually has an option for converting const_cast to undefined behavior.Housemaster
@BenVoigt I don't know if this one is actually in use or not, but it really would not surprise me if there were a company that had a proprietary fork of GCC or some other compiler that did this, especially since this is a pretty straight-forward thing to do. Google had feedback directed optimization (FDO) in a fork of GCC for a long time before upstreaming it, so it wouldn't surprise me if a company out there implemented this in their own fork of GCC.Nejd
@MichaelAaronSafyan "though, really, it shouldn't be marked const in such a case" -- Why not? The only meaning that const conveys on a method is that it will not alter the object on which it is invoked when it is called. It does not mean that the return value will be the same each time it is called even if the object isn't changed. const on a method is a promise not to change the object; it doesn't imply idempotence.Built
@cdhowie, there is some disagreement as to when to use const. I'm on the side that says that const is more about concept than implementation; a function should be const if and only if it does not modify state in a way that changes the observable output / effects of this or other const functions without an interleaving non-const function. If the outputs change for the same state/inputs, then it is not logically constant.Nejd
@MichaelAaronSafyan Ultimately it has nothing to do with what we think const implies on a member function nor when we think it should be used. The standard is the authority, and the as-if rule governs all optimization. If the compiler can prove that the const method is idempotent then it can elide calls in a block of code where the object isn't modified. But if it can't prove this and it does the optimization anyway, the as-if rule is broken and it's not really a C++ compiler, just a compiler that can parse C++ code but that interprets it incorrectly.Built
It is unclear in your answer whether obj is an object allocated in scope or not; this makes a difference with regard to aliasing. If obj is potentially aliased, then there is no telling that f cannot access a mutable reference to obj even without using const_cast making any attempt to cache the result of .length() invalid. Your answer, thus, is very misleading for constness is only part of the issue.Sabo
C++ const is a different concept from being a "pure" function whose return value only depends on the args, and has no side effects on any other (global) state. You can easily come up with examples where const makes sense for a non-pure member function. e.g. an object that has some parameters for an API call or system call. You could have a .doit() member function that doesn't mutate this object, just uses those parameters to make a system or library call. If you try to use C++ const as a synonym for GNU C __attribute__((pure)) or __attribute__((const)), you'll have a Bad Time.Waldenburg
LTO fixes a lot of the non-optimizable conditions you have listed.Stretchy
K
7

Function parameters:

const is not significant for referenced memory. It's like tying a hand behind the optimizer's back.

Suppose you call another function (e.g. void bar()) in foo which has no visible definition. The optimizer will have a restriction because it has no way of knowing whether or not bar has modified the function parameter passed to foo (e.g. via access to global memory). Potential to modify memory externally and aliasing introduce significant restrictions for optimizers in this area.

Although you did not ask, const values for function parameters does allow optimizations because the optimizer is guaranteed a const object. Of course, the cost to copy that parameter may be much higher than the optimizer's benefits.

See: http://www.gotw.ca/gotw/081.htm


Variable declarations: const int i = 1234

This depends on where it is declared, when it is created, and the type. This category is largely where const optimizations exist. It is undefined to modify a const object or known constant, so the compiler is allowed to make some optimizations; it assumes you do not invoke undefined behavior and that introduces some guarantees.

const int A(10);
foo(A);
// compiler can assume A's not been modified by foo

Obviously, an optimizer can also identify variables which do not change:

for (int i(0), n(10); i < n; ++i) { // << n is not const
 std::cout << i << ' ';
}

Function declarations: const char* foo()

Not significant. The referenced memory may be modified externally. If the referenced variable returned by foo is visible, then an optimizer could make an optimization, but that has nothing to do with the presence/absence of const on the function's return type.

Again, a const value or object is different:

extern const char foo[];

Keneth answered 14/12, 2014 at 6:28 Comment(0)
W
5

The exact effects of const differ for each context where it is used. If const is used while declaring an variable, it is physically const and potently resides in read-only memory.

const int x = 123;

Trying to cast the const-ness away is undefined behavour:

Even though const_cast may remove constness or volatility from any pointer or reference, using the resulting pointer or reference to write to an object that was declared const or to access an object that was declared volatile invokes undefined behavior. cppreference/const_cast

So in this case, the compiler may assume that the value of x is always 123. This opens some optimization potential (constants propagation)

For functions it's a different matter. Suppose:

void doFancyStuff(const MyObject& o);

our function doFancyStuff may do any of the following things with o.

  1. not modify the object.
  2. cast the constness away, then modify the object
  3. modify an mutable data member of MyObject

Note that if you call our function with an instance of MyObject that was declared as const, you'll invoke undefined behavior with #2.

Guru question: will the following invoke undefined behavior?

const int x = 1;
auto lam = [x]() mutable {const_cast<int&>(x) = 2;};
lam();
Wrath answered 14/12, 2014 at 8:39 Comment(3)
You're probably right that modifying the mutable data member of an const object is defines behavior. I'll edit that part. But i think you got my guru question wrong. An by-value lambda capture preserves the const-ness of the captures. So internally inside the lambda we have an variable x which is declared as const int. Modifying that one is undefined behavior.Wrath
@Deduplicator I wish it was that way, but it isn't. See youtube.com/watch?v=48kP_Ssg2eY from 16:40.Wrath
Careful, the const semantics of lambdas not explicitly marked as mutable have changed.Housemaster
H
4

SomeClass* const pObj creates a constant object of pointer type. There exists no safe method of changing such an object, so the compiler can, for example, cache it into a register with only one memory read, even if its address is taken.

The others don't enable any optimizations specifically, although the const qualifier on the type will affect overload resolution and possibly result in different and faster functions being selected.

Housemaster answered 14/12, 2014 at 5:34 Comment(4)
Can you elaborate more on overload part? Maybe an example?Coronation
@UnTraDe: I'm trying to think of a case in the Standard library where the const version of a function does something radically different, and I can't. User code and other libraries might do such a thing, however.Housemaster
@BenVoigt, I remember the old days when implementors of the C++ standard library experimented with reference-counted std::strings. Calling, e. g., begin() on a non-const string caused its detachment; that is, if the std::string object shared the string with another std::string object, it was copied at this moment and marked as non-reference-countable. Calling begin() on a const string did not change its internal state.Nonet
@Andrey: Yes, that the type of improvement via overload selection I'm talking about. But it isn't standard-compliant for std::string to do that, and I'm not aware of any other class in the Standard which has such a behavior.Housemaster
I
2

const can be a major boost to optimizations because the compiler assumes that a const object never changes its value once initialized. Specifically:

Example 1 - Const enables constant folding

/* const */ float base = 2;

float f(float x) {
    return powf(base, x);
}

Depending on whether we use const or not, we get two different assembly outputs (clang -O3 -march=x86-64-v4):

Without const
f:
        vmovaps xmm1, xmm0
        vmovss  xmm0, dword ptr [rip + base]
        jmp     powf@PLT
With const
f:
        jmp     exp2f@PLT

With const, the compiler can assume that base is always 2, so powf(base, x) is the same as exp2f(x). Exponentiating with a base of two is significantly faster.

Example 2 - Const prevents clobbering

void clobber(const int*);

int f(void) {
    /* const */ int result = 0;
    clobber(&result);
    return result;
}
Without const
f:
        [...]
        mov     eax, dword ptr [rsp + 4]
        pop     rcx
        ret

The lack of const means that clobber(&result) could have altered the value of result. The fact that clobber accepts a const int* doesn't change that because it could just cast the const away and modify the value. This forces the compiler to get the returned result from the stack.

With const
f:
        [...]
        xor     eax, eax
        pop     rcx
        ret

Here, const means that clobber cannot modify result. To do that, it would need to modify a const object and doing so is undefined behavior. Instead of getting the result from the stack, the compiler assumes that it's still zero and emits xor eax, eax, which puts 0 into eax.

Note that adding [[unsequenced]] to clobber would also prevent clobbering of result in the non-const case. See also: What are the [[reproducible]] and [[unsequenced]] attributes in C23, and when should I use them?

Ichabod answered 11/1, 2024 at 11:8 Comment(3)
Note that compilers aren't perfect about non-clobbering optimizations. With a compile-time-constant initializer, it's just a case of constant propagation. But with const int result = runtime_var, compilers won't optimize as much as they're allowed in some cases. godbolt.org/z/3T97va6h1 shows int f(int x) which does return result - x; after calling clobber(&result). Neither var can have changed, but GCC/clang/MSVC save both and subtract anyway.Waldenburg
@PeterCordes that's pretty surprising. The compiler is really pessimistic here, even when both x and result are const. Is it just that this optimization is extremely difficult? Is it spill code and a lack of registers? I really wouldn't have expected a missed optimization there.Ichabod
Yeah, I didn't expect that missed optimization either; I only noticed it yesterday when answering Does a restrict-qualified pointer parameter of a function allow optimization of the caller functions? where I was hoping to demo a const object allowing the optimization that a const * restrict pointer didn't. But GCC, Clang, and MSVC all miss this optimization. It's not a register-allocation problem; return 0 doesn't need either input. godbolt.org/z/endzbP3Ke shows optimization with a visible but noinline callee.Waldenburg

© 2022 - 2025 — McMap. All rights reserved.