RFR (M): 8184334: Generalizing Atomic with templates

Erik Österlund erik.osterlund at oracle.com
Fri Jul 21 16:08:02 UTC 2017

Hi Andrew,

On 2017-07-21 15:43, Andrew Haley wrote:
> Hi,
> On 20/07/17 14:45, Erik Österlund wrote:
>> It seems like the aliasing problem has multiple levels at which we can
>> debate. I will try to put my thoughts down in a reasonably structured
>> way... let's see how that goes.
>> These are the core questions we are debating (and I will answer them in
>> order):
>> 1) What does the standard say about aliasing rules?
>> 2) What did concrete implementations like GCC implement the aliasing rules?
>> 3) What are the implications of those aliasing rules being violated?
>> Does it matter?
>> 4) Have these changes actually changed anything relating to those
>> aliasing rules?
>> 5) How do we proceed?
>> Regarding #1: We seem to agree that there are various possible
>> interpretations of the standard (wouldn't be the first time...); at
>> least one interpretation that violates treating intptr_t as
>> compatible with A* and at least one interpretation that permits
>> it. I do not know if there is any one true interpretation of the C++
>> standard, so I will assume the possibility that either one of us
>> could be right here, and hence that a compiler writer might write a
>> compiler in which it is either permitted aliasing or not.
>> Regarding #2: While the simplest most contrived example of what we
>> are arguing about being a strict aliasing violation is not caught as
>> a strict aliasing violation using the strictest strict aliasing
>> checks available in GCC, I feel dubious, but will take your word for
>> it that intptr_t and A* are not compatible types. So for the rest of
>> the discussion, I will assume this is true.
> OK.  For the sake of the discussion I'll go along with this.
>> Regarding #3: We already inherently rely on passed in intptr_t or even
>> jlong aliasing A*, possibly today enforced through -fno-strict-aliasing.
>> I do not think the JVM will ever be able to turn such optimizations on.
> I completely agree.  However, if we are using -fno-strict-aliasing,
> then I do not believe that there is any need for the complexity of the
> casting that your patch does.  We should be able to do the operations
> that we need with no type casting at all, with suitable use of
> template functions.
>> Our C++ runtime lives in the border lands between Java and C++ that I
>> will refer to as "shady land".
> Yep.
>> Regarding #4: I have made changes in the connection between the
>> frontend and the backend. But I would like to maintain that the
>> backends retain the same behaviour as they did before. For example,
>> your xchg_ptr for void* casts (C-style) the exchange_value to
>> intptr_t and use the intptr_t variant of xchg_ptr that passes
>> intptr_t (a.k.a. int64_t on your architecture) into
>> _sync_lock_test_and_set. So whatever hypothetical aliasing problem
>> the new Atomic implementation class were still there before these
>> changes. I have not introduced any of those aliasing issues, only
>> improved the mediation between the frontend to the platform
>> dependent backend.
> True.  But this is new code, and IMO should do better, and also IMO
> does not need to be significantly more complex than the already
> complex code we have.  I assume that the complexity is due to some
> compilers I don't know about, though, so I admit that I am at
> something of a disadvantage.  I suspect that none of it is necessary
> for a complete and correct implementation on GCC.  By "the complexity"
> I am referring to a 4k line patch.  I fear that not only is it
> complex, it may also inhibit useful compiler optimizations.
>> Regarding #5: Now as for how to move on with this, I hope we can
>> agree that for code in "shady land", the compiler simply has to
>> treat intptr_t as possible aliases to A* and that if it does not,
>> then we need to tame it to do so. Because the C++ compiler simply
>> does not know the full story of what is going on in our program and
>> can not safely make any meaningful points-to analysis and data flow
>> optimizations crossing dynamically generated JIT-compiled machine
>> code of a different language.
>> I propose three different solutions that we can discuss:
>> Solution A: Do nothing. With this solution we recognize that a) we
>> have actually not introduced any undefined behaviour that was not
>> already there - the backend used intptr_t before and continues to do
>> so, and b) doing so is safe (and inherently has to be safe) due to
>> other levels of safeguarding such as turning off strict aliasing and
>> using volatile.
>> Solution B: If intptr_t does indeed not alias A*, we could find a
>> pointer sized type, let's call it CanonicalPointer, that does indeed
>> alias a generic A*, and pass pointer types from the frontend as
>> CanonicalPointer to the backend. For example char* is explicitly
>> described in the standard as an exception of the rules that is
>> explicitly aliased to a generic A* (hopefully we agree about
>> that).
> We don't.  A char* can point to the bytes of any type, but this rule
> does not mean that an object of type char* can be accessed by an
> lvalue of some other pointer type.  They aren't the same thing at all.
> The questions "what can a char* point to?" and "what can point to a
> char* ?"  are quite different.  An int** can't point to a char*.  The
> only permission we have is that any pointer can be cast to a character
> pointer, and that character pointer points to the first byte of the
> object.

I'm not sure if I was clear enough, pardon me if I was not, but the 
loophole I was referring to is section 3.10 where it says:

If a program attempts to access the stored value of an object through
a glvalue of other than one of the following types the behavior is

— the dynamic type of the object,

— a cv-qualified version of the dynamic type of the object,

— a type similar (as defined in 4.4) to the dynamic type of the object,

— a type that is the signed or unsigned type corresponding to the
   dynamic type of the object,

— a type that is the signed or unsigned type corresponding to a
   cv-qualified version of the dynamic type of the object,

— an aggregate or union type that includes one of the aforementioned
   types among its elements or non- static data members (including,
   recursively, an element or non-static data member of a subaggregate
   or contained union),

— a type that is a (possibly cv-qualified) base class type of the
   dynamic type of the object,

— a char or unsigned char type.

I will make a store load pair of A* example split into three relevant cases:
1) A non-Atomic store is observed by an Atomic load
2) An Atomic store is observed by a non-Atomic load
3) An Atomic store is observed by an Atomic load

Case 1: A non-Atomic store is observed by an Atomic load
A non-Atomic store (possibly initialization code) stores an A* that is 
observed as char* and subsequently reinterpret_casted to A*. I claim 
that A* and char* are compatible types in this context.
It is not undefined behaviour w.r.t. 3.10 if the type behind the pointer 
in a pointer load is a char, regardless of the dynamic type it actually 
points at. The load then has to assume conservatively that it does not 
know what those bytes are referring to - it might be anything (including 
the dynamic type behind the stored pointer: A). In other words, char 
aliases all dynamic types that the pointer could point at. It seems like 
we both agree this case is fine.

Case 2: An Atomic store is observed by a non-Atomic load
Conversely, if the Atomic API was to store something as a char* that 
actually has a dynamic type of A*, and a normal load observes this as an 
A*, that is also fine as the dynamic type is still A* despite being 
stored as char*. In other words, the load is invariant of what type the 
store had, w.r.t. 3.10, if its type matches the dynamic type. And it does.
It seems to me like this is the case where you made a point that despite 
being stored as char*, that does not mean that a non-Atomic load of A* 
will understand the aliasing. And my point is that this is still not 
undefined behavior w.r.t. 3.10 as the non-Atomic load is true to the 
dynamic type. I hope we agree here.

But then apart from the standard there is the problem of aliasing 
optimizations of specific compilers, like -fstrict-aliasing on GCC. 
Let's remember that in C++03, "strict aliasing" is not a thing. And a 
concrete points-to analysis might not know what the true dynamic type of 
a pointer is, so depending on how conservative the data flow analysis, 
points-to analysis and type-based aliasing analysis is, an aggressive 
compiler might make further assumptions than what the standard outlines 
as undefined behaviour, and as a result mess up the program. But then 
again, we turn such optimizations off, and they are arguably not 
supported by the standard. If we were to support that some day, perhaps 
CanonicalPointer would be better off as the union of A* and char*. But 
then as I mentioned, that is not enough because C++ programs can send, 
via JNI, a pointer as a jlong into Java that will try to perform a CAS 
through var handles and unsafe, and ultimately end up in a possible 
runtime call to Atomic performing that CAS with Atomic::cmpxchg of 
jlong. Therefore, we must inherently be able to deal with the dynamic 
type being destroyed in "shady land" long before it reaches Atomic. It 
will seemingly just never work reliably with -fstrict-aliasing, as far 
as I can tell.

Case 3: An Atomic store is observed by an Atomic load
This seems trivially compatible as the store and load pair are 
communicating through the same type, char* that are compatible and aliased.

> With regard to a solution, I feel there may be a much simpler answer.
> But in order to sketch out that simpler answer, I need to know exactly
> what problem is being solved.  I presume that there is a need to turn
> a call to a generalized function such as
>     template <typename T, typename U>
>     inline static void store(T store_value, volatile U* dest);
> into a call to one of a number of specialized functions such as
>     template <>
>     inline void Atomic::specialized_store<int64_t>(int64_t store_value, volatile int64_t* dest) {
>       _Atomic_move_long(&store_value, dest);
>     }

Yes that's about it. Without messing things up of course w.r.t T and U 
having different signedness, size and type.

> and also perform some suitable magic for floating-point types.  But do
> we ever use atomic floats for anything?  Will we ever?

That is a good point. I am not certain. But the current API supports it, 
so it seemed like a good idea to continue supporting it.


More information about the hotspot-runtime-dev mailing list