RFR (M): 8184334: Generalizing Atomic with templates

Erik Österlund erik.osterlund at oracle.com
Thu Jul 20 13:45:33 UTC 2017


Hi Andrew,

On 2017-07-19 18:21, Andrew Haley wrote:
> On 18/07/17 16:26, Erik Österlund wrote:
>>
>> On 2017-07-18 16:41, Andrew Haley wrote:
>>> On 18/07/17 14:46, Erik Österlund wrote:
>>>> On 2017-07-18 11:57, Andrew Haley wrote:
>>>>> On 18/07/17 10:38, Erik Österlund wrote:
>>> It is more general than that.  If you access the stored value of an
>>> object through a glvalue of other than one of the allowed types, then
>>> your *whole program is undefined*.
>> And I think it is in that list of allowed types. Specifically, it is my
>> interpretation of the standard that intptr_t is "a type that is the
>> signed or unsigned type corresponding to the dynamic type of the object"
>> (cf. 5.2.10) if the dynamic type is e.g. void*.
>>
>>>> and that therefore it is fine to load something (that was stored as
>>>> A*) as intptr_t and subsequently cast it to A* before the A itself
>>>> is being accessed. Am I wrong?
>>> No, that is correct.  The question is whether intptr_t is a type
>>> compatible with a pointer type, and I don't think you will find any
>>> language to the effect that it is.
>> I think 5.2.10 describing reinterpret_cast seems to suggest that they
>> are compatible by saying "A pointer converted to an integer of
>> sufficient size (if any such exists on the implementation) and back to
>> the same pointer type will have its original value". That is precisely
>> what is done.
> This is the core of our disagreement.
>
> In a sense, it doesn't matter which of us is right; what really
> matters is the opinion of the compiler.  GCC does not put intptr_t and
> pointer types into the same alias set, and even if this did turn out
> to be a bug in GCC it wouldn't help us because we often use
> unsupported old versions of GCC to build OpenJDK, so it wouldn't get
> fixed anyway.
>
> So what are we to do?  Do you believe me when I say that GCC does
> treat A* and intptr_t to be compatible types?  If not, what would it
> take to convince you?

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.

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. 
Our C++ runtime lives in the border lands between Java and C++ that I 
will refer to as "shady land". C++ standards are relatively successful 
at describing well-behaved semantics for a program where C++ code 
interacts with other C++ code. Unfortunately, we are working in "shady 
land" where C++ code also interacts with JIT-compiled machine code from 
Java land which not surprisingly has poorly defined meaning in the 
context of C++ standards. For example, we take pointers from C/C++, send 
them as jlong via JNI into Java that may pass it around and ultimately 
store it in some Java long field of an object (with a store or perhaps 
an Unsafe CompareAndSetLong call that may or may not be implemented via 
an intrinsic), and ultimately be passed in to C++ code that loads that 
long as jlong, and casts it into a pointer and starts poking around in 
values behind that pointer (e.g. Unsafe_SetMemory0(), 
Copy::conjoint_jlongs_atomic(), jni_NewDirectByteBuffer(), etc). In 
fact, the type for "native pointers" in Java is long, a.k.a. jlong, and 
I am fairly certain that the code base is sufficiently full of this that 
pointer sized integers and pointers *have* to be aliased by the compiler 
or we are in deep trouble. I do not think we can ever allow them not to 
be aliased, especially not in "shady land". So if compilers ever started 
assuming that there is no way anything loaded as an intptr_t, or jlong 
for that matter, could point at any C++ types (despite being casted to 
do so), and perform aggressive optimizations utilizing points-to 
analysis, relying on those assumptions, then that would be so 
fundamentally broken that we could arguably not recover. This goes 
without appreciating just how much other code in hotspot already relies 
on this.
While it is possible to try to isolate code in "shady land" from the 
rest of the C++ code, it is precisely in "shady land" that Atomic and 
OrderAccess live. They are first class citizens of "shady land". They 
poke around in memory that is being concurrently accessed either by 
other C++ code or by JIT-compiled Java code (possibly performing atomics 
with jlongs that are in fact native pointers passed in from JNI). Here 
the aliasing must be as conservative as possible.
My conclusion is that if intptr_t/jlong is not considered compatible 
with A* by the compiler, the compiler has to be tamed to think so 
anyway, or the JVM will break horribly. Especially for Atomic and 
OrderAccess that lives in "shady land".

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.

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). I 
have put together a small example of how xchg might look with this model 
here:
http://cr.openjdk.java.net/~eosterlund/8184334/webrev.01_alias_pointer_A/
The frontend canonicalizes types and sends them to the backend. The 
canonical type of any pointer is char*, which explicitly aliases to all 
other pointers. Passing pointers through jlong (which is actually done) 
is possibly still not safe in this variant.

Solution C: Go even further (yet with less code) and continue sending 
canonical integer types into the backend, and leave it to the backend to 
decide what measures need to be taken to trick the compiler into not 
performing dangerous optimizations (in the hypothetical scenario that we 
turn on strict aliasing). For example on AArch64 linux treating int64_t 
as char* that explicitly aliases all pointers (3.10), relying on the 
roundtrip guarantees of reinterpret_cast guarantees (5.2.10) between 
pointers and intptr_t. A code example, again for xchg, can be found here:
http://cr.openjdk.java.net/~eosterlund/8184334/webrev.01_alias_pointer_B/
This solution also allows atomically loading a jlong and subsequently 
casting it to a pointer (because its backing load access used char*), 
which I argue inherently needs to be allowed.

My preference is solution A for now (as no new violations are actually 
introduced with these changes, conversely the part of the pipeline from 
the C++ runtime to the backend in "shady land" has been purged from 
evil), and possibly looking into C later, on a platform by platform 
basis, to further strengthen aliasing in "shady land".

I am curious to hear your thoughts about this.

Thanks,
/Erik

>> Conversely, I do not find any language suggesting that intptr_t would
>> not be compatible with pointer types.
>>
>>> Pointer types are distinct from one another and from all integer
>>> types.  People sometimes get confused by this: the fact that you can
>>> cast from A* to e.g. void*, doesn't tell you that you can cast from
>>> A** to void** and use the result: they are different types.
>> Agreed. But I do not see any problem with reinterpret_cast from A* to
>> intptr_t back to A* and using the A from that result. That is explicitly
>> allowed as per 5.2.10 as previously described.
> I don't think that's what you're doing.  I think you're casting from
> A** to intptr_t*, dereferencing the intptr_t*, and casting the result
> to A*.
>
>> That looks like the correct (and indeed expected) output. iptr should be
>> the same as &b, and iptr->_x should be 0, and &a should be different
>> from &b and iptr. I tried this experiment locally too with similar
>> correct result. Am I missing something here?
> No, I was; I misinterpreted the result, and it's a red herring.
> Sorry.
>
> I believe this patch to be dangerous.  We might well get away with it,
> given sufficient larding of volatile, but it's still undefined
> behaviour.  We have far too much UB in HotSpot already, and we should
> not be adding any more.
>
> And I still believe that this patch is more complex than is needed,
> and with many (if not all) targets we could do something much simpler,
> and which does not need to cast pointer types.  It could also map
> cleanly onto C++11.
>



More information about the hotspot-runtime-dev mailing list