Unsafe vs MemorySegments / Bounds checking...
Maurizio Cimadamore
maurizio.cimadamore at oracle.com
Thu Oct 31 18:25:17 UTC 2024
On 31/10/2024 18:08, Johannes Lichtenberger wrote:
>
> So, one of the "tricks" either way is to have a MemorySegment using
> the entire address space -- size is Long.MAX_VALUE
> (MemorySegment.NULL.reinterpret(Long.MAX_VALUE)). But it seems
> unintuitive to have to allocate memory in another step via an Arena
> for instance, take the address and copy data to this address in a
> subsequent step to the "ALL" MemorySegment. I almost lost track, but
> at least the VarHandle trick should not be needed in the future?
>
I'd suggest to leave such "tricks" until you have evidence you need them :-)
If your code was using heap arrays before (as I seem to recall) it was
_already_ performing bound checks. So memory segment access should
eliminate bound checks pretty much in the same way they are eliminated
for arrays. The remaining cost in your case would be the arena liveness
check (which can typically be hoisted out of loops).
Regarding MAX_VALUE:
The way I've seen this used is not to copy stuff around - but rather, to
access memory using a segment that has a "big enough" bound (in the hope
that the bound check will be eliminated).
There are typically two ways this trick can be exploited:
1. one is to create a singleton segment which starts at the zero address
(NULL) and ends at Long.MAX_VALUE. This allows clients to use this
segment to access memory at any virtually offset (yes, there's negative
addresses too, but typically the sign bit on addresses is used to
separate address spaces of kernel and application, so either one or the
other will likely be used). This approach makes the bound check somewhat
lighter (but does not totally eliminate it, because... sign), and
eliminates the liveness check (at least in principle, this is related to
[1]).
2. the other is to resize a segment "on the fly". E.g. just before you
access it, you reinterpet it to the desired size (or MAX_VALUE), and
then you access the reinterpreted segment. This approach works on any
address (both positive and negative). Also, since you are
resizing-then-accessing, a nice property is that the bound check is now
fully eliminated.
On paper, (2) is better and more general than (1). However, (2) relies
on the ability of the JIT to inline all the access path. If that doesn't
happen, you start seeing memory segment allocations (as reinterpret will
create a new segment object). If everything is inlined, all allocation
disappears (thanks to escape analysis).
Another difference between (1) and (2) is that (1) performs a restricted
operation once (when the "everything" segment is created). (2) needs a
reinterpret _on every access_. Now the runtime checks for restricted
methods are heavily optimized, but it is possible for something to go
wrong (e.g. if you are out of inlining budget), and then you'll start
paying for a lot of things.
But... as stated above, using tricks such as these to avoid bound checks
should really be used as a last resort. You are completely giving up
safety in doing so (as the warnings about restricted methods, which will
eventually turn to errors, will remind you). While it might be an ok
approach where performance considerations are paramount, we absolutely
do not recommend going down this rabbit hole.
Cheers
Maurizio
[1] - https://github.com/openjdk/jdk/pull/21810
More information about the panama-dev
mailing list