Why Const Doesn't Make C Code Faster
theartofmachinery.com
theartofmachinery.com
I found this out the hard way when I implemented OptimumC, the first data flow analysis optimizer for DOS C compilers back in the 1980's.
I had to undo doing optimizations on const.
Anecdote: Optimum C blew away all the other C compilers on a magazine benchmark article because it figured out that the benchmark code did nothing and deleted it. The author didn't ask me about it, he just assumed the compiler was buggy and gave us a bad review. Sigh.
When other compilers did DFA, the benchmarks got changed.
I'd love to hear more about any aspect of this if you have time. (:
We included full library source with the compiler, for free. No reviewer ever noticed that. One day, Borland decided to make source code for their library, sans the floating point stuff, available for an extra charge. This generated headlines in the next compiler roundup article. No mention that complete library source code, including floating point, came with my compiler.
So we decided to split off the library source code into a separate package, and charged for it. This solved the marketing problem, and doubled the money we were making.
This was how I learned about the common practice of upselling. I am a slow learner.
Of course, these days we give away everything for free!
With it being packaged separately, people noticed its existence, and did assign value to it commensurate with its price.
It was attractively priced, and almost everyone buying the compiler also bought the library source.
Did you write a letter complaining? Any response? Do you have a link to the review or copy of it? (Sorry, I just find this history fascinating).
BTW, it goes on. A couple years ago a kind soul gently suggested I implement DFA since clang had invented DFA.
(No, I didn't invent DFA either, but I do believe that Datalight Optimum C was the first DFA compiler for DOS.)
http://www.program-transformation.org/Transform/CCompilerHis...
The Data Flow Analysis code: https://github.com/DigitalMars/Compiler/blob/master/dm/src/d...
I remember this problem being discussed in a CPPCon talk a few years ago, and some ways to "trick" the (modern) compilers into not optimising away the benchmark code - I think it was Chandler Carruth's "Tuning C++" talk: https://youtu.be/nXaxk27zwlk
It seems that interprocedure optimisations in modern compilers make a lot of const-by-reference optimisations apply even when the code is mutable-by-reference in the parameter list but the function body doesn't modify it in practice. This would only work if it could deterministically work out which function is called.
Local constant stack values surely can be completely deterministically verified as such by the optimiser even without the `const` modifier. They could be overwritten without a C assignment to it, via the stack from a buffer overflow of an array next to it, but that's undefined behaviour so a compiler is presumably free to assume it is not modified, eliminate unnecessary register/memory loading code, and let the developers deal with the consequences.
As trailing `const` on member functions outlaws modifications via `this`, it would follow that the same optimisation-even-without-modifier process would apply as to `const` local stack values.
As constancy is a constraint that aids human comprehension, there's a good reason for choosing a keyword just as short as the mutable equivalent, such as Swift's `let` vs `var`; if the more constrained equivalent is equally or more convenient, more constrained and thus easy-to-reason-about code becomes more common.
Only if their address doesn't escape.
However, it can still cheat by rearranging the memory write or even letting linker initialize the address.
An explicitly const-qualified object instead can be replaced with its value even if its address has escaped.
My phrasing was probably a bit ambiguous there.
Ive usually encoutered this most when trying to implement out of line template member functions and forget to include the inline keyword.
Well, isn't it true? It's true also in the inline case. I consider it strange behavior not to emit an explicitly enabled warning for a case that definitely satisfies the warning conditions.
> Static and inline does not trigger this.
Tried -Wunused-function in gcc 6.3.0 and clang 3.8.1-24, with an unused-but-defined function declared "static inline int add(int a, int b)". clang still emits the warning, gcc does not.
If you're already dependent on whatever additional semantics gcc tacked onto the inline keyword you can as well use "__attribute__((unused))" to disable the warning for a particular function, which more clearly communicates the intent to disable the warning.
IMO these special cases just add to the confusion and my criticism of strange behavior, but I stand corrected on the actual behavior of the two compilers.
One thing that I did find out what that even if you inline or force inline a function, that if you have it exported from a DLL or in a class that is exported as a DLL, then the function might still be called non-inline.
For even more aggressiveness, try __attribute__((const)) which tells it that the func accesses NOTHING but the params, not even global vars. Of course this is quite limiting. You can also use this as a trick when YOU know that the only accessed global state is itself constant but gcc does not know that. This can produce substantial savings in number of calls of funcs that, for example, do lookups in large constant data tables, like table-based versions of sin() and cos().
In the example the article gives, adding __attribute__((pure)) to constFunc() does produce the behaviour author wanted and then some
LWN has more: https://lwn.net/Articles/285332/
You can even promise a func is pure without even implementing it in the same translation unit! That produces lots of improvement. See these two godbolt links:
Actually that's the main point, as if it's in the same TU, the compiler can probably figure out a function is pure.
One pain point with pure functions is you can't insert debug logging statements. D has a special case for that - purity for a statement isn't checked if it is prefixed with the `debug` keyword:
pure int square(int x) {
debug printf("called square\n");
return x * x;
}
which is very, very handy.https://d.godbolt.org/z/Rwu4Cq (remove "-d-debug" to see)
Allocators are often pure in D, they probably shouldn't be but that would mean allocating outside the function which would be ugly and probably buggy too
Forgive me I'm just a small brained firmware programmer.
That's not a very useful distinction, and usually one talks about "purity" at a conceptual level. The language provides a layer of abstraction in which things can be considered pure (or not), even if the underlying implementation e.g. makes optimizations that require mutation. The language is just a guarantee that you can ignore these aspects of its implementation.
A function that allocates memory isn't pure, except perhaps iff it frees that memory either itself or by compiler magic. Then perhaps it is.
I.e. it's for conditionally inserting debugging code where you don't care about purity, you just want to find the bug.
And it doesn’t do anything unintended if you’re lying to it by accessing the global state that you know to be constant?
If you're actually not accessing any global state at all (including making no memory allocation), shouldn't the compiler figure that out anyway? That doesn't seem like something that would be very hard to check for.
void* __attribute__((const)) getGlobals(void) {
void* ret;
asm (
" ldr %0, [r9] \n"
" ldr %0, [%0, %1] \n"
: "=r"(ret)
: "I"(MY_MODULE_ID)
:
);
return ret;
}For const to help, the object itself needs to be defined const. Just taking a `const object * ` (or `const object&` in C++) doesn't help you determine the constness of the underlying object, and that usually accounts for the majority of const usage by volume.
Limiting the scope to of actual const definitions, it can help a lot, but only in cases where the compiler couldn't move that anyways. So local variable const definitions rarely help, because the compiler can often already prove they are const by inspection (but they can help if the variable escapes).
They are useful especially for global variables (and moral equivalents, like static class members in C++), since the compiler cannot prove by examining only the current TU whether the variable is unmodified (and it is a hard problem even if the whole program can be inspected), so const is a useful promise there.
const int* const
instead of const int*Maybe the compiler will happily let you modify the dereferenced const int* (undefined behavior), wouldn’t try it now, but that’s not what the signature promises.
Edit: Thought about it more and read some other comments. Now it makes sense.
Indirection in C and C++ is a mess, but at least C has the "restrict" keyword. Best to program with value types whenever you can, and use pointers and references when you must.
My understanding is that compilers can always assume that there is no other thread involved, which (part of) why C11 atomics are necessary. Is that not the case?
It is allowed to concurrently read from const regions. But not concurrently read and write, and definitely not concurrently write.
void foo(const int* const p1, int* const p2) {
int a = *p1;
*p2 += 42;
int b = *p1;
return a == b; // b = a + 42
}
int main() {
int x = 0;
return foo(&x, &x);
}The second we have indirection (function pointers), or a different source file without whole-program-optimization, or a library, these assumptions break down.
Further, the author assumes that the compiler can't benefit from the knowledge that something is const because the const can be cached away. This isn't true, e.g. per ISO9899 6.7.3.5:
If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined. If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.
The real reason to use "const" is to show intent.
See "Const and Rigid Parameters" in this wonderful article about the Doom 3 source code: https://kotaku.com/the-exceptional-beauty-of-doom-3s-source-...
Your choice of data structure or algorithm would…
This means, I won't bother using "inline" anymore, but I will consider how many subfunctions in total I'd call.
Consider that `x` may point to a global variable which `something` may modify. That is why the reload is necessary.
edit: I just realized that the reload would be necessary even without an intervening function call. https://godbolt.org/z/QhVzlV
Consider that `x` may point to errno; then printf would modify it!
"volatile" basically tells the compiler "Hey, wait, this might be modified by DMA, another thread, or another process. Assume nothing."
Even excluding casting evils, calling a method with a const pointer only means that the method isn't supposed to change the value. It does not mean that the caller isn't going to change things (particularly from another thread).
The languages are simply too permissible when it comes to what a pointer means and how it can be used.
For const to be optimizable, you'd need to take Rust's approach and make language level guarantees that "You can't do bad things with this". C and C++ missed that boat.
This is incorrect.
Actually, you promise not to change things from another thread, DMA, interrupt handler, signal, etc, with any non-volatile reference passed, let alone a const! The compiler loads things into registers and has no way to know if memory in a passed reference changes underneath the hood-- it freely generates code that assumes that the things it has pointers do, do not change. It can freely make optimizations that lead to incorrect computation, infinite loops, segmentation faults if this is not obeyed. If you've ever head about how "double check locking" is an antipattern, this is a big part of why.
e.g. from ISO 9899:
Alternatively, an implementation might perform various optimizations within each translation unit, such that the actual semantics would agree with the abstract semantics only when making function calls across translation unit boundaries. In such an implementation, at the time of each function entry and function return where the calling function and the called function are in different translation units, the values of all externally linked objects and of all objects accessible via pointers therein would agree with the abstract semantics. Furthermore, at the time of each such function entry the values of the parameters of the called function and of all objects accessible via pointers therein would agree with the abstract semantics. In this type of implementation, objects referred to by interrupt service routines activated by the signal function would require explicit specification of volatile storage, as well as other implementation-defined restrictions.
This is the model used by pretty much every C compiler you'll encounter. When you e.g. acquire a lock, you call something in a different linkage unit so multithreaded stuff behaves properly.
For const, the guarantees go further: you promise it won't change elsewhere (relevant standards text quoted in my other comment).
The C specification describes semantics in terms of an abstract machine which is actually quite different from real hardware (most notably in terms of how memory works!). It then goes on to say that the compiler may choose to implement it radically differently, so long as the observable semantics (I/O calls and volatile accesses) are preserved. I'd have to double check whether it was C or C++ that said that the compiler is free to assume that infinite loops do not exist.
> Actually, you promise not to change things from another thread, DMA, interrupt handler, signal, etc, with any non-volatile reference passed, let alone a const!
This is not the case. The C specification requires that you use volatile to indicate that code outside of the C execution model may access the memory location. Of your list, only DMA and interrupt handlers are outside the execution model; signal handlers and threads are both considered inside the memory model. The only way for a signal handler to communicate with code outside the signal handler is with volatile sig_atomic_t; volatile int does not cut it. To communicate between threads, you need to ensure proper synchronization. This may involve the use of locks, fences, or atomics with appropriate orderings chosen.
> The compiler loads things into registers and has no way to know if memory in a passed reference changes underneath the hood
To be pedantic, the compiler relies on undefined behavior here. It is undefined behavior if you cause the value to be changed in a way that violates these rules, so the compiler has absolutely no restrictions on what may happen in such executions.
> If you've ever head about how "double check locking" is an antipattern, this is a big part of why.
This has absolutely nothing to do with why double-checked locking is incorrect. Double-checked locking is problematic in large part because of hardware reordering of loads and stores. In general, you need a store barrier to guarantee that all of the modifications the first thread changed has been made visible to other processors followed by a load barrier to guarantee that all prior modifications from other processors have been made visible to the second thread. Volatile does absolutely nothing to provide these barriers (except if you use MSVC, which documents that they treat volatile variables as equivalent to acquire/release semantics on x86 because regular loads and stores on x86 have those semantics anyways--this is nonportable behavior). The double-checked locking pattern does not provide a load barrier in the second thread, which means the ordering semantics are not guaranteed. If you use atomic loads and stores when implementing double-checked locking, you do get the necessary semantics for correctness.
You say that it is only thing outside of the execution model that require volatile, but you explain that a signal handler requires volatile and to be the signal-atomic type. ;) In practice, volatile with lock-free types (that now stdatomic has a way to query whether types are lock-free) are correctly used all the time to pass data between threads.
A volatile declaration may be used to describe an object corresponding to a... an object accessed by an asynchronously interrupting function.
Like, execution in other thread contexts.
Or, from the C99 Rationale:
The translator may assume, for an unqualified lvalue, that it may read or write the referenced object, that the value of this object cannot be changed except by explicitly programmed actions in the current thread of control, but that other lvalue expressions could reference the same object.
vs volatile:
No cacheing through this lvalue: each operation in the abstract semantics must be performed (that is, no cacheing assumptions may be made, since the location is not guaranteed to contain any previous value). In the absence of this qualifier, the contents of the designated location may be assumed to be unchanged except for possible aliasing.
> Double-checked locking is problematic in large part because of hardware reordering of loads and stores.
This was unsafe even on in-order uniprocessors, because the compiler was free to reorder loads and non-aliased stores.
No, I'm saying that the statement that volatile is only necessary and sufficient for outside the execution model. For signal handlers, it is necessary but not sufficient; for threads, it is neither necessary nor sufficient.
> Or, from the C99 Rationale:
C99 does not consider threading at all. C11 and C++11 do, and any compiler written in the past decade is going to be obeying the rules for the C++11 memory model (which C11 adopted as its memory model). The committees explicitly considered whether it would make sense to imbue volatile with any special threading semantics, and they explicitly rejected doing so.
A volatile read or write is not guaranteed to be converted into a single hardware load or store, even for lock-free types. There are situations where the compiler will narrow or widen the load/store, or even insert extraneous loads and stores to the value.
> This was unsafe even on in-order uniprocessors, because the compiler was free to reorder loads and non-aliased stores.
The compiler is free to reorder non-volatile loads and stores with volatile loads and stores. Only reordering volatile loads and stores with respect to other volatile loads and stores is prohibited. Volatile is not sufficient to make double-checked locking safe.
You seem to not be reading what I'm saying and also to be arguing with things I never asserted. e.g.
> Volatile is not sufficient to make double-checked locking safe.
OK, but I didn't say volatile makes double-checked locking safe... I said that the compiler's ability to reorder non-qualified variable accesses is "a big part of why" double-checked locking is an antipattern. My statement:
> Actually, you promise not to change things from another thread..., let alone a const! The compiler loads things into registers and has no way to know if memory in a passed reference changes underneath the hood... It can freely make optimizations that lead to incorrect computation, infinite loops, segmentation faults if this is not obeyed. If you've ever head about how "double check locking" is an antipattern, this is a big part of why.
It is possible to cast away const and immutable in D, but these are only allowed in system code, presumably where the programmer actually does know what he's doing.
To do optimizations, compilers really need to know that nobody will mutate a value; just knowing that a particular function won't mutate a value isn't that helpful.
const on a methond says "I won't mutate the state of this class (except for members explicitly marked as mutable)
A const reference says I won't mutate this (unless I const_cast away the constness first)
But a const variable actually is a promise to C++, saying "This object will never mutate after initialization ever forever I promise for real this time " (except in the destructor, then it's OK).
Confusingly, all together this means you're only allowed to cast away constness if the object wasn't const to start with.
___
Some of the above may be wrong, not a language expert.
In short, placement new (or some scenarios involving unions) technically cause undefined behavior if you try to use the object after the call to placement new, since the pre-existing object there has had its lifetime expire. std::launder lets you use a pre-existing pointer to the memory at the same location to access the data there.
I may be misunderstanding, but this seems to directly contradict what the linked paper says:
> Note that std::launder() does not “white wash” the pointer for any further usage.
> The obvious question is, why don’t we simply fix the current memory model so that using data where placement new was called for implicitly always does launder?
no, it is consistent. Launder does not white wash its parameter, only its return pointer and any pointer returned by it. Same for placement new.
I save the data of whether or not it's a copy into a flag variable, so my program knows when and when not to delete a const char* .
The alternative would be of course to create a two variables, one char* and one const char* , but that would double the memory needed, but only one type of variable would be used at a time.
class string {
union {
const char *referenced;
char *copied;
} data;
bool copied;
public:
string(const char *source, booly copy) {
if(copy) {
data.copied = strdup(source);
copied = true;
} else {
data.referenced = source;
copied = false;
}
}
operator const char *() const {
return copied ? data.copied : data.referenced;
}
~string() {
if(copied) {
free(data.copied);
}
}
};
No const_cast and no wasted memory and it's clear that you are storing two semantically different types.I would expect the conversion operator to optimize down to a noop since in all cases the returned address is equal to this.
I want the pointer (actually, that to what it's pointing to) to be referenced as const in all cases, except when I create a copy or delete that copy, in which case I then explicitly recast them as not-const. That's just two tiny exceptions in code I have to write anyways.
Such as it is, I would only consider a union of them if I'd actually need const/non-const 50/50 of the time, but my current use case is more like 98/2.
Apart from that, I'm guessing you're from a C++ background, as there are no classes in C? (and also, if you use C++, it's recommended to use "delete" instead of "free", since the latter is a C function)
EDIT: Actually, it's only one exception/re-cast, when I delete it, as no compiler would complain about saving a char* in a const char* data type.
how could it make that assumption ? there is nothing that prevents you to implement the function in fortran or even in raw assembly
Now, the tricky part is that const objects (i.e. objects declared as const) enjoy these optimization benefits, but the compiler usually cannot prove that a reference or pointer to const actually refers/points to a const object. Taking a const pointer to a non-const object, casting away the constness again and then modifying the object is totally legal.
Since the former case (objects declared const) is much rarer than the latter case (working on const object pointers/references) the compiler cannot often optimize based on const.
template<class T>
struct deep_const { const T value; };
template<class T>
const deep_const& freeze(const T&) { /* magic */ }
template<cass T>
const T & thaw(deep_const<T> const&) { /* more magic */ }
magic and more magic left as an exercise for the reader.edit: I do not think either GCC or clang take advantage of that though, so currently it is just a curiosity.
edit: missed actual const keywords /facepalm
[0] The Design and Evolution of C++
Correct, but only if the original (referenced) object is not const itself. Casting away constness to modify an object that was declared const is 100% UB.
Obviously there are stupid things that can be avoided from the start but in general I prefer clean code where people write for readability and simplicity and not speed.
Didn't do any comparison studies of performance, but the productivity boost from not having to think about it was nice, and I suspect any performance difference is margin-of-error stuff like the sqlite results in this article.
Compiling took over 2 hours. The header was generated automatically, but after I added a macro that replaced those const uints with #defines, compile time dropped to 30 minutes, which was quickly voted the biggest productivity enhancement in that project.
More relevant to the topic, in javascript programming I recently switched to using const instead of let wherever possible. Most variables don't actually vary, so let's make that explicit. That can prevent some unexpected surprises (though not all, as objects and arrays in constants are not immutable).
In contrast, a Standard ML compiler can enforce this law because the language itself insists that all bindings are immutable.
void constByArg(const int *x)
{
printf("%d\n", *x);
constFunc(x);
printf("%d\n", *x);
}
Here, the object referenced by pointer x is non-local, and so is constFunc.The object could be modified in legal ways nothing to do with constFunc stripping away the qualifier.
Also, if the object is not defined const, then constFunc is allowed to do that, too.
Then the next, correct, example with a const protected local shows that two instructions are shaved off. That's a wortwhile saving that could be leveraged to get faster code.
If you're passing local variables into helper functions which are not supposed to change them, you can shave off some cycles with const.
> I mean, I removed const from the entire program
Maybe sqLite doesn't use const specifically with a view toward optimization. To see an overall performance impact, there would have to be some case in a "hot spot" of the program, where const is used with some local variables being passed into functions. (Or whatever other case we can ferret out where const happens to help.)
There are some widely applicable optimizations which scoop up all the "low hanging fruit" improvements, but after that, optimization is a game of eking out small gains with specialized cases.
A recent optimization in clang (not sure if it's in clang-9 or clang-10) will remove memsets to variables declared const, which usually come from assigning through a pointer that's had const (of the pointed to type) casted away. The MIPS Linux kernel won't boot when built with clang due to the above (I sent a patch 2 weeks ago)
constFoo(x);
constBar(x);
where it couldn't make that assumption if one function wasn't const.However, const-qualifying your pointer arguments where the pointed-to object isn't changed is what allows you to make more liberal use of const-qualified declarations, and as also demonstrated by the article, those in turn do allow some optimisations.
Additionally, there is some safety on the table: if your C program makes use of pointers to structures full of function pointers to implement polymorphism, making those pointers const-qualified allows your underlying structures full of function pointers to be declared const, which in turn allows them to be stored in hardware-enforced read-only memory.
As a side note, I've often thought that block-scope variables declared const and whose address is never taken should be automatically made static.
// x is just a read-only pointer to something that may or may not be a constant
void constFunc(const int *x)
Is that correct? I read that as “x is a pointer to a const int”If you want “x is a read-only pointer to an int”, you would need
void constFunc(int * const x)
The article also doesn’t mention the case “x is a read-only pointer to a constant int”. To state that, you would use void constFunc(const int * const x)
(https://stackoverflow.com/a/1143272)Consider:
int foo = 42;
const int *x = &foo;
foo = 43;He only talks about const args, where the compiler does not try yet to check for casts which violate the constness guarantees. (DFA, Escape Analysis). And thus misses all important optimizations. You cannot rely on that never being implemented in the future. With LTO they already do I think.
* (poor choice, I know)
Compilers are smart. Even Java's JIT will generate SIMD instructions pretty well
Const variables can also be mapped read-only. This gives you hardware protection against modifications, and also uses less RAM if the const variable is in a shared library (multiple processes can share the same mappings).
I can only imagine what he thought about aggressively optimizing C-family compilers in his later years.
Haven't profiled it but would make more sense to have the pointer also be const instead of having a non const pointer as input?
Const pointer is a statement about the internal variables of a function definition, usually not of any interest outside the function itself and therefore rarely used.
...and in fact not even part of the name mangling (for the exact reason you mentioned): https://godbolt.org/z/1pjecq
Some years ago i'd agree, but experience has shown that C/C++ compiler writers would rather win benchmark games than keep working code working. So it'd be nice if there was a better reason than "well, a lot of code would break if they did that".
I may have a non-const object that I pass to a function that takes it by const pointer: that doesn't magically make it const! The object can still be modified, e.g., by an opaque function in the caller which modifies the object not necessarily by casting away cost, but because the object isn't const at all (e.g., it may have a non-const reference to the object).
Const definitely helps optimizations, but only when the object is actually cost. References or pointers to const objects can't tell you that.
The main problem with const is that it's ugly, it's viral and it does not add any value to the code.
And I know all the supposedly good things about this, but I never found any real usage in practice.
Understanding which object parameters to a function are inputs and which ones are modified is key information about the behavior of a function. It's omission requires devs to divine the intent from the name and hope that every previous dev was a good citizen w.r.t keeping function names accurate to intent.
Finding and replace const with empty is the moral equivalent of replacing all the types of parameters with void*. After all, types are viral and ugly too.
I really hope that intern found a better place to work than your company.
But first, when you're an intern, you have to follow the local codestyle, even if you think it's not ideal.
Second, I think there are much better rules than spamming const to attain the same objective, but you might need more experience to really appreciate that.
Like?
> you might need more experience to really appreciate that.
Practically a picture perfect appeal to authority.
I've been programming in C for 25+ years. const is clutter.
Yes, types can be trivially casted away too. That's not a reason to use void* everywhere.