[foreign-memaccess] on shared segments

Jorn Vernee jorn.vernee at oracle.com
Tue Oct 15 14:58:01 UTC 2019


Hi,

Some comments;

- MemorySegment's class javadoc mentions confined segments, but not 
shared segments. Maybe there should be a short sentence like "Shared 
segments can not be closed explicitly", with a link back to the 
packag-info javadoc as well.
- In MemorySegment::asConfined javadoc, the language "Obtains a confined 
copy of this memory segment" might be interpreted as the underlying 
memory being copied. Maybe you could use; "Transfers the underlying 
memory to a new MemorySegment instance confined to the given Thread"? I 
think "Transfer" is more descriptive of what's happening (I noticed the 
package javadoc also uses "transfer").
- Similarly for asShared.
- MemorySegment::isAccessible -> typo: "or is the segment is a shared 
segment" -> "or if the segment is a shared segment". The sentence right 
after that is also redundant with the following sentence.

Rest looks good,
Jorn

On 14/10/2019 18:12, Maurizio Cimadamore wrote:
> New revision:
>
> http://cr.openjdk.java.net/~mcimadamore/panama/shared-segments_v4
>
> (although all openjdk websites are experiencing some troubles right now)
>
> This changes the API to:
>
> - add an 'isAccessible()' method which checks 
> "isConfined(Thread.currentThread()) || isShared"
> - changes the spec for asShared and asCOnfined, allowing them to throw 
> exception when an invalid state transition takes place
>
> Maurizio
>
> On 27/09/2019 14:48, Maurizio Cimadamore wrote:
>> Uploaded new revision:
>>
>> http://cr.openjdk.java.net/~mcimadamore/panama/shared-segments_v3/
>>
>> Changes:
>>
>> * Added full fences on asShared, asConfined
>> * Added new predicate for testing confinement
>> * restructured the calls to checkValidState - now basic accessors and 
>> predicates like byteSize, baseAddress, isPinned etc. do NOT check for 
>> confinement
>> * changed the javadoc of asConfined/asShared to reflect the option 
>> (2) - see below
>>
>>
>>  /**
>>      * Obtains a confined copy of this memory segment whose owner 
>> thread is set to the given thread. If the new owner thread
>>      * differs from the current owner thread, as a side-effect, this 
>> segment will be marked as <em>not alive</em>,
>>      * and subsequent operations on this segment will result in 
>> runtime errors.
>>      * @param newOwner the new owner thread.
>>      * @return a confined copy of this segment with given owner thread.
>>      * @throws UnsupportedOperationException if the segment is a 
>> shared segment (see {@link MemorySegment#isShared()}).
>>      */
>>     MemorySegment asConfined(Thread newOwner) throws 
>> UnsupportedOperationException;
>>
>>     /**
>>      * Obtains a shared copy of this memory segment which can be 
>> accessed across multiple threads. If the current segment
>>      * is not already shared (see {@link MemorySegment#isShared()}), 
>> as a side-effect, this segment will be marked as
>>      * <em>not alive</em>, and subsequent operations on this segment 
>> will result in runtime errors.
>>      * The shared copy will also be marked as <em>pinned</em> (see 
>> {@link MemorySegment#isPinned()});
>>      * as such, any attempt to close the returned segment will result 
>> in a runtime error.
>>      * @return a shared copy of this segment.
>>      */
>>     MemorySegment asShared();
>>
>>
>> I think we can't do much better if we go with (2) - that is we _have_ 
>> to say when the invalidating side-effect takes place. I'm still not 
>> 100% that (2) is the way to go; IMHO (2) supports a fiction that 
>> 'asConfined' can be used to assert confinement, rather than to 
>> proactively change it. I think clients should use it sparingly, 
>> especially now that we have full testing capabilities.
>>
>> Maurizio
>>
>> On 26/09/2019 19:10, Maurizio Cimadamore wrote:
>>> Hi,
>>> in a previous document [1] I explored the problem of allowing 
>>> concurrent access to a memory segment in a safe fashion. From that 
>>> exploration, it emerged that there was one type of race that was 
>>> particularly nasty: that is, a race between a thread A attempting to 
>>> close a segment S while a thread B is attempting to access (read or 
>>> write) S.
>>>
>>> The presence of this race makes it really hard to generalize the 
>>> existing memory access API to cases where concurrent/shared access 
>>> is needed. Of course one naive solution would be to synchronize 
>>> every access on the liveness check, but that makes performance 
>>> really poor - which would defeat the point of having such an API in 
>>> the first place.
>>>
>>> Instead, to solve that problem, in the document I posit about a 
>>> solution which uses an explicit acquire/release mechanism - that is 
>>> clients of a shared segment will need to explicitly acquire the 
>>> segment in order to be able to operate on it, and release it when 
>>> done. A shared segment can only be closed when all clients are done 
>>> with the segment - this is what ensures temporal safety. Moreover, 
>>> since each client works on its own 'acquired' copy of the shared 
>>> segment, everything is a constant and the JIT can see through the 
>>> code and optimize it in the same way as it does for confined access. 
>>> That said, we never fully committed to that solution, since the 
>>> resulting API was very complex: for things to work, part of the 
>>> MemorySegment API has to be moved under a new abstraction (in the 
>>> document called MemoryHandle) - more specifically the bits that are 
>>> responsible for creating addresses. While it's possible to devise a 
>>> confined segment that is both a MemorySegment and a MemoryHandle 
>>> (thus giving us back the old API), the general feedback I've 
>>> received is that this solution seems a bit too convoluted.
>>>
>>> When discussing about this problem with Jim, he pointed out a useful 
>>> connection and a possible way out: after all, all these 
>>> acquire/release and reference counting schemes are there to perform 
>>> a job that a JVM knows exactly how to do at speed: determining 
>>> whether an object is still used or not. So, instead of inventing new 
>>> machinery, we could simply piggy back on the mechanisms we already 
>>> have - that is GC and Cleaners.
>>>
>>> The key realization, in the shared case, can be summarized as: 
>>> performance, safety, deterministic deallocation, pick two! Since 
>>> we're not willing to compromise on safety, or on performance, 
>>> letting go of the deterministic de-allocation goal (only for shared 
>>> segments) seems a reasonable conclusion.
>>>
>>> In other words, there are now two kinds of segments: /confined/ 
>>> segment and /shared/ segments. A segment always starts off as 
>>> confined, and has an owning thread. You can update the owning thread 
>>> - effectively nuking the existing segment and obtaining a new 
>>> segment that is confined on a new thread. This allows clients to 
>>> achieve serialized thread-confinement use cases - where multiple 
>>> threads operate on a piece of memory one at a time. Confined 
>>> segments are operated upon as usual: you allocate a segment, you use 
>>> it, you close it (or you use a try with resources to do it all 
>>> automagically).
>>>
>>> If clients want more - e.g. full concurrent access, an API point is 
>>> provided to turn a confined segment into a shared one. Again, what 
>>> happens here is that the existing segment will be nuked, and a new 
>>> shared segment will be created. But, this shared segment _cannot be 
>>> closed_ (e.g. it is pinned, using the existing API terminology). So, 
>>> how are off-heap resources released if we can't close the segment? 
>>> Well, we let the GC take care of it - by registering the segment on 
>>> a Cleaner, and have the cleaner call some cleanup code once the 
>>> segment is no longer referenced (in reality, things are a bit 
>>> different, in the sense that what we really  key on is the _scope_ 
>>> of a segment, which might be shared across multiple views, but the 
>>> essence is the same). In other words, deallocation for shared 
>>> segments works pretty much the same way deallocation of direct 
>>> buffer work.
>>>
>>> With this move, we are able to retain the simplicity of the existing 
>>> API, while also being able to support efficient and safe concurrent 
>>> access.
>>>
>>> A webrev implementing this change is available here:
>>>
>>> http://cr.openjdk.java.net/~mcimadamore/panama/shared-segments_v2/
>>>
>>> Implementation-wise things are, I think, quite straightforward. I 
>>> took sometime to refactor the code, to make the various scope 
>>> subclasses disappear. We now have a single memory segment 
>>> implementation and two scopes: shared and confined. The confined 
>>> scope takes a 'Runnable' cleanup action which is used (i) when 
>>> closing the confined segment or (ii) passed onto the Cleaner by the 
>>> shared scope if the segment is upgraded to 'shared' state. Also, 
>>> since shared segment now can now be picked up by Cleaner when no 
>>> longer referenced, it is crucial that we add in reachability fences 
>>> around Unsafe operations (same way as direct buffer does really). 
>>> This is because sometimes the GC can aggressively collect unused 
>>> objects stored in local variables during method execution. Adding 
>>> these fences doesn't negatively impact performances (in fact, I'm 
>>> told these fences are a no-op in Hotspot).
>>>
>>> I also took some effort to update some of the javadoc which are 
>>> rendered invalid by this change.
>>>
>>> Comments welcome
>>>
>>> Maurizio
>>>
>>> [1] - http://cr.openjdk.java.net/~mcimadamore/panama/confinement.html
>>>


More information about the panama-dev mailing list