[foreign-memaccess] on confinement
Jorn Vernee
jbvernee at xs4all.nl
Wed Jun 5 17:34:30 UTC 2019
Comments inline...
Maurizio Cimadamore schreef op 2019-06-05 19:16:
> On 05/06/2019 17:48, Jorn Vernee wrote:
>>> Thoughts?
>>
>> I don't think we can safely do our confinement check `scope.owner !=
>> Thread.currentThread()` if scope.owner is mutable, without some form
>> of synchronization. I really think the confinement thread should be
>> determined at (sub)segment creation time, and then be immutable
>> afterwards.
>
> Yes, that's a piece that's missing.
>
> But it seems like we landed in pretty similar places indeed, down to
> your proposed AtomicInteger (I just use an int in my implementation,
> but that's the same check I believe to the one you were proposing).
>
> I like your take on overlapping regions - since they are all de facto
> pinned in a shared context, there's no safety concern; it's up to the
> user to 'make it right'.
>
> One thing that still stands though: we need some kind of cleaner to go
> after GCed segments, to keep the reference count in check.
>
> This is actually a bigger topic: what to do with things that go out of
> scope - even a simple segment going out of scope could mean a memory
> leak (e.g. nobody calling Unsafe::freeMemory).
>
> Here we could:
>
> 1) do nothing - just let things leak - it's user responsibility to
> clean things up
>
> 2) detect when things are GCed and call 'close' forcibly
>
> This is an hard choice; for root segments we'd probably like (1),
> whereas for subsegments if you do (1) you end up closing memory for
> other related segments too (which might still be alive!!) - at least
> in the case where the segment is not shared. At the same time, since a
> subsegment can keep hold of the root segment, the root segment can
> never get GCed if at least one of its subsegment is reachable.
>
> At the moment I'm more for (1), since otherwise it would be pretty
> hard for the user to understand what's going on.
I think (1) is much better because:
* We want to have prompt cleanup of memory.
* We want to share offheap memory with native code later on, for which
the GC can't see the references to the resource.
Though, using GC + Cleaner as a fallback for view segments to keep the
reference count in check seems fine as well, since that should never
clean up the memory resource.
> (again, this problem is made more acute by segments, but it's there
> with scopes too)
>
> Maurizio
I fiddled a little with your example as well to make the owner thread
field immutable: http://cr.openjdk.java.net/~jvernee/panama/Test.java
Jorn
>
>
>
>>
>> Jorn
>>
>> Maurizio Cimadamore schreef op 2019-06-05 17:40:
>>> Thanks Jorn,
>>> I went pretty much through the same reasoning and realized that:
>>>
>>> a) confinement must be the default
>>>
>>> b) handing off ownership must be an opt-in
>>>
>>> b2) similarly, racy shared segments which synchronize on the liveness
>>> bit can be an equally appealing opt-in
>>>
>>> c) tracking region overlapping is uber-expensive; it's much better to
>>> define primitives which allow 'splitting a region' in non overlapping
>>> segments by construction (e.g. resize is not the way to get what we
>>> want here); let's call this 'split'
>>>
>>> c2) the bits returned by 'split' are _pinned_
>>>
>>> d) we need a way to 'merge' the bits back into the parent.
>>>
>>> What I came up with this this [1], which I think kind of implements
>>> your principle for ShareableSegments (note this is an example, not a
>>> full blown Panama patch).
>>>
>>> Thoughts?
>>>
>>> Maurizio
>>>
>>> [1] -
>>> http://cr.openjdk.java.net/~mcimadamore/panama/TestScopedSegmentMerge.java
>>>
>>> On 05/06/2019 14:59, Jorn Vernee wrote:
>>>> One other thing I realized; closing the root segment through a view
>>>> segment (like proposed before) is only possible when the root
>>>> segment and _all_ view segments are confined to the same thread. At
>>>> least if we want to avoid synchronization on access when checking
>>>> liveliness. I think this gets us the following set of rules for
>>>> non-shared segments:
>>>>
>>>> 1. Terminal operations are always thread confined (safety feature to
>>>> prevent VM crashes when resource is freed by another thread).
>>>> 2. Always confined to the same thread (avoid mutable fields,
>>>> complexity in implementation).
>>>> 3. We can close the root segment through a view segment.
>>>> 4. We can not share a view segment with a different thread (would
>>>> break rule 1. when combined with 3.).
>>>> 5. No need for the user to keep a reference to the root segment,
>>>> since we can close it through a view segment.
>>>> 6. No need for subsegment tracking.
>>>>
>>>> Also, shareability should be an opt-in, but it seems that supporting
>>>> lazy transition into a shared state (with asConfined()) creates too
>>>> much complexity for the simple single-threaded case, so I think it
>>>> should be an opt-in at segment creation time. That way we can keep
>>>> the 'default' single threaded implementation fast and simple.
>>>>
>>>> ---
>>>>
>>>> We could still go with a separate ShareableSegment type, which does
>>>> allow sharing of view segments with other threads, but does not
>>>> allow closing the root segment through a view segment. To avoid
>>>> mutable confinement thread fields we can require the confinement
>>>> thread to be specified when creating the view segment. A strawman:
>>>>
>>>> interface ShareableSegment extends MemorySegment {
>>>> MemorySegment resize(Thread confinementThread, long offset,
>>>> long length); // support 'divide et impera'.
>>>> default MemorySegment resize(long offset, long length) {
>>>> return resize(Thread.currentThread(), offset, length);
>>>> }
>>>>
>>>> void merge(MemorySegment subsegment); // could do
>>>> automatically with GC + Cleaner as well
>>>> // need some synchronization if resize and merge can be
>>>> called by other threads then the root's confinement thread
>>>>
>>>> // ... factory methods
>>>> }
>>>>
>>>> Which gets us the following rules for shareable segments:
>>>>
>>>> 1. Terminal operations are always thread confined (safety feature to
>>>> prevent VM crashes when resource is freed by another thread).
>>>> 2. Always confined to the same thread (avoid mutable fields,
>>>> complexity in implementation).
>>>> 3. View segments can be confined to different threads than the root
>>>> segment.
>>>> 4. We can not close the root segment through a view segment (would
>>>> break rule 1 when combined with 3).
>>>> 5. The user must keep a reference to the root segment at all times
>>>> to be able to close it and avoid resource leaks.
>>>> 6. Need to track subsegments in order to know whether the root
>>>> segment can be closed safely.
>>>>
>>>> ---
>>>>
>>>> Also, overlap of subsegments will break confinement in the sense
>>>> that multiple threads can write/read to/from the same region, but
>>>> since subsegments owned by multiple threads can not free/release the
>>>> underlying resource, I don't think overlapping subsegments could
>>>> crash the VM. So, maybe it's good enough to tell the user to make
>>>> sure that subsegments owned by different thread's don't interfere
>>>> which each other, but we don't enforce that in the implementation?
>>>>
>>>> If we go that route I believe we can make the subsegment tracking
>>>> for ShareableSegment a simple AtomicLong reference count. Where the
>>>> liveliness flag in a subsegment is a reference to the root segment,
>>>> that is nulled out when merging, and also used to make sure that
>>>> merge is called with an actual subsegment.
>>>>
>>>> Jorn
>>>>
>>>> Maurizio Cimadamore schreef op 2019-06-05 02:16:
>>>>> On 04/06/2019 17:03, Maurizio Cimadamore wrote:
>>>>>> Note: I'm not saying this will be trivial to implement correctly -
>>>>>> but what I like about this is that the programming model will look
>>>>>> relatively clean in comparison to something like (1). Essentially
>>>>>> you can slice and dice all you want, and, as long as you are
>>>>>> asking reasonable questions, things will work with decent
>>>>>> performances.
>>>>>
>>>>> Quick update; I've been doing some experiment on this - it doesn't
>>>>> look pretty for now.
>>>>>
>>>>> Some of the issues we have to take into account:
>>>>>
>>>>> * as discussed, we want the master region to somehow keep track
>>>>> (via
>>>>> its mutable 'scope-like' object) of the sub-regions
>>>>>
>>>>> * if we share the same scope for all subregions (which we probably
>>>>> want to avoid too much allocation on resize) then we need to have a
>>>>> way for the sub-region to perform an efficient confinement check -
>>>>> one
>>>>> trick I used was to give each sub region an unique index, and then
>>>>> use
>>>>> the index to access a subregion 'ownership' array
>>>>>
>>>>> * we need to take into account regions being GCed - otherwise the
>>>>> lists kept into the master region will (potentially) grow w/o
>>>>> bounds
>>>>>
>>>>> * we need to take into account synchronization when adding/removing
>>>>> sub-regions - this is probably not a big concern given that these
>>>>> operations occur during a 'resize' or when a region is being GC, so
>>>>> the memory access itself can still be fast
>>>>>
>>>>> * since we can transfer ownership, the owner thread is not a final
>>>>> constant anymore... this will probably affect performances
>>>>> considerably
>>>>>
>>>>> * I haven't even started to look at rejecting overlapping sub
>>>>> regions
>>>>> with different owners...
>>>>>
>>>>> Needless to say, the resulting implementation is very finicky, and
>>>>> I'm
>>>>> worried about the overall performance model of this approach.
>>>>>
>>>>> Also, I don't think that what I'm seeing is an artifact of lumping
>>>>> MemoryScope and MemorySegment together - yes, in principle having a
>>>>> separate scope (with a notion of confinement in it) helps in the
>>>>> sense
>>>>> that resizing a segment becomes an orthogonal concern. But then you
>>>>> are back in a world where you can't give a different thread owner
>>>>> to
>>>>> different sub-region, and the only way around that restriction is
>>>>> to
>>>>> use memory copy (e.g. create a new segment and copy contents of the
>>>>> old one to the new).
>>>>>
>>>>> If that cross-subregion policy is what we realistically want to
>>>>> enforce, then I don't think it's worth doing a lot of heroics here
>>>>> -
>>>>> we can simply say that a segment is confined to a thread, there's
>>>>> no
>>>>> ownership transfer operation, but the same effects can be achieved
>>>>> through memory copy. This doesn't seem quite a rich a story as the
>>>>> one
>>>>> we were looking at - but if we were ok with Scope being in charge
>>>>> of
>>>>> thread confinement, this would have been the only story possible.
>>>>>
>>>>> So, the question becomes: do we really need a way to transfer
>>>>> ownership of a segment from thread A to thread B ? And if so, what
>>>>> granularity should be used? I think these are the possible answers:
>>>>>
>>>>> a) ownership transfer not supported - region copy should be used as
>>>>> a workaround
>>>>> b) ownership transfer supported; all subregion are constrained to
>>>>> have
>>>>> same owner as the root; when ownership changes, all subregions
>>>>> change
>>>>> ownership too
>>>>> c) ownership transfer supported; subregion ownership can set
>>>>> independently of the root
>>>>>
>>>>> I realized that, in the email I've sent this morning I picked the
>>>>> most
>>>>> difficult point in the design space (c) - that is, support
>>>>> ownership
>>>>> transfers at the subregion granularity. This seems useful to
>>>>> implement
>>>>> divide and conquer algorithms, but at the same time, I realized,
>>>>> this
>>>>> was simply not possible with the scope-based solution we had before
>>>>> (since all subregions had same scope there - hence same
>>>>> confinement).
>>>>>
>>>>> In other words, all the implementation strategies we've seen so far
>>>>> are capable of handling either (a) or (b) [as for (b) I'm not sure
>>>>> about the potential JIT cost in making thread owner non-final]. The
>>>>> implementation story for (c) is far more convoluted (**), and I'm
>>>>> very
>>>>> skeptical that, even if we can pull that off, it will perform in a
>>>>> way
>>>>> that will be deemed acceptable.
>>>>>
>>>>> Is (c) simply asking for too much? And, if so, is (b) something
>>>>> that
>>>>> could be useful still?
>>>>>
>>>>> Maurizio
>>>>>
>>>>> (**) Honestly, the overlapping region check seems the straw that
>>>>> breaks the camel's back - to implement the check it's sadly
>>>>> unavoidable to keep all subregions which share the same root in the
>>>>> same place - which then poses aforementioned problems with respect
>>>>> to
>>>>> such subregions being GCed, and need for synchronization when
>>>>> maintaining all the ancillary lists. And, this overlapping region
>>>>> check is needed in both the approached (1) and (2) that I have
>>>>> outlined earlier in [1], I believe.
>>>>>
>>>>> [1] -
>>>>> https://mail.openjdk.java.net/pipermail/panama-dev/2019-June/005674.html
More information about the panama-dev
mailing list