Tightening GCCs Memory Model
The middle-ends memory model is that of the C language with additional kludges to support the memory model of C++. See PRs 29286 and 38964.
To summarize
- The C memory model assigns a type to anonymous memory at the time of the first write, fixing it for the rest of the time. See 6.5/6 and /7.
Actually this is not true - anonymous memory changes its effective type on each write that is not through an lvalue of character type. See that same 6.5/6, "... that do not modify the stored value". GCC currently does not even try to avoid exchanging two writes of different types to the same anonymous memory location during scheduling.
- The C++ memory model can dynamically change the type of memory by constructing an object in place. The dynamic type of the memory is fixed until the object is destructed or a different object is constructed in place of it. See 3.8, especially /4, but also 1.8 and 3.10/15.
Unlike for C this also applies to objects with a declared type.
The most extreme consequences one can derive from this are
- You cannot implement malloc in C that re-uses memory after freeing it.
You can, but only using anonymous memory.
- In C++ every store changes the dynamic type of the memory stored to.
There are several consequences for optimizations in the face of type-based alias analysis for C++ and C.
- Type-based disambiguation across dynamic type changes is invalid.
- People are unsure if annotating the C++ new operators with the malloc attribute will break valid programs because of (in-)valid optimizations. See for example the long thread starting
Proposed Changes
The proposal is to change the middle-ends memory model to match that of the most extreme interpretation of the C++ memory model:
Every store changes the dynamic type of the memory stored to.
What are the consequences of this?
- TBAA cannot be used to disambiguate two stores.
- TBAA cannot be used to validate sinking a load across a store (or hoist a store across a load).
All these transformations are usually only performed by scheduling, not by CSE, PRE, load invariant motion or store motion. To carry out these transformations their validity has to be proven using offset-based disambiguation or disambiguation based on points-to analysis.
This should re-assure us that the middle-end will not exploit using the malloc attribute on C++ new operators. Basic arguments that this is safe are
3.7.3.1/2
- [...] If the request succeeds, the value returned shall be a nonnull pointer value (4.10) p0 different from any previously returned value p1, unless that value p1 was subsequently passed to an operator delete.
But then the question is raised on how pointer difference or equivalence relates to the non-aliasing guarantee which the malloc attribute makes.
The most convincing counter-examples are
char pool[N]; size_t next; /* Never mind that the return memory isn't properly aligned here; fixing the implementation is left to the reader. */ void *operator new(size_t s) { void *p = pool + next; next += s; return p; } bool f() { char *c = new char; return (c == pool); }
and
char *pool; void set_pool(char *p) { pool = p; } void *operator new(size_t s) { // return stuff from pool. } bool f() { char *p = new char[1024]; set_pool (p); char *i = new char; return (p == i); }
but they confuse pointer equality arguments with the aliasing guarantees as shown by
int *q = new int; delete q; int *p = new int; delete p; if (q != p) cout << "different";
where we of course may not say that q and p are non-equivalent. Object lifetime is the key here. No examples involving possible invalid optimizations on memory accesses were given.