Lazy statics (was: Feedback / query on jextract for Windows 10)
Brian Goetz
brian.goetz at oracle.com
Wed Feb 3 18:35:11 UTC 2021
Obviously this is ground that has been at least partially trodden, and
past attempts to traverse this territory has not yet yielded a clear
roadmap, hence it's still on the shelf, despite its obvious
attractiveness. Let me try and enumerate some of the loose ends.
We seek laziness because we want to defer a calculation into the future,
where it might not need to happen. This applies both to statics and
cached properties of objects. Computing a static final field now must
happen in <clinit>, which happens before any other useful work does;
computing an instance field in the constructor happens before anyone can
do anything useful with the object. Laziness gives us a chance to avoid
the computation until we need it. It always comes with a tradeoff,
which might be complexity in the programming model, or coordination
overhead (CASes and such.)
Programmers want laziness built into their model because it lets them
express what they want without having to imperatively implement it.
Platform creators like laziness because the author gives us more
information about program invariants, which the platform can then
usefully exploit. The key new information is "write arity." Today,
Java offers us two write-arity options; exactly once (final) or
"anything goes". Laziness expresses a zero-or-one write arity, along
with tacit permission to delay side-effects until the value is needed.
From a programmer perspective, both the instance and static paths are
attractive; we have all done more work than we'd like (and made more
mistakes than we should) implementing lazy initialization in both
instance and static state. But from a platform perspective, the static
case is more compelling, because there's more that the runtime can do to
optimize lazy statics -- it can treat them as trustable statics that are
grist for the inlining mill. There are fewer options here for instances.
Without painting the bikeshed, let me point out that there are two
possible user models for capturing laziness: lazy fields and cached
methods. Lazy fields are the more obvious:
__lazy static final String s = computeABigString();
but we could frame this equivalently as
__cached static String computeABigString() { ... }
There are pros and cons to each, but this is largely a user-model
question about the story we tell. Saying this is a field, vs a cached
method, sends a slightly different message.
Separately, there's a hidden possible requirement we should try to be
explicit about: whether we care about binary compatible migration for
_public_ lazy static fields. If we have today:
public static final String s = ...
and some *other* class accesses the field, there will be `getstatic`
instructions in the wild that expect the field to be there. But, this
would constrain the ability to translate a read of a lazy static as
invoking a method whose body does an LDC, which might be a preferable
translation. I am not expressing an opinion on whether this is a
requirement or not, as much as calling it out as something we could
_choose_ to take on, or not, with full awareness of the tradeoffs.
Reframing as caching makes it more obvious that we have to be clear
about what guarantees we are offering, especially in the instance case;
are we guaranteeing to execute the method no more than once, or is it OK
for multiple threads to race to initialize the cached value? What
visibility guarantees do we make?
Finally, there's exceptions. One of the pain points we have today is
initializing method handles in static finals; we can't say
static final MethodHandle mh = ... lookup ...
because of the exceptions. THis is a mere annoyance with finals, where
we'd catch and rethrow the exception in the <clinit> and the client
would get an ExceptionInInitializerError, but worse with lazies, since
the timing of the exception can be any time and any place.
I'm not writing all this so that we can post our pet solutions now; I'm
trying to capture everything in one place. I'm sure I forgot something;
let's try to get a complete picture of the problem space.
On 2/3/2021 7:46 AM, Maurizio Cimadamore wrote:
> On Mon, 2021-02-01 at 09:31 +0100, Remi Forax wrote:
>> Another intermediary solution is to implement the lazy keyword in the
>> compiler (javac) but only enable it under a flag. So that flag can be
>> used when compiling java.base and jextract generated classes.
> I'd avoid such hacky solutions :-)
> In my experience, people look at those "hidden" flag, and then before
> you know it, you start seeing code in the wild that depends on them
> (few years ago I saw code still using -jsr14).
>
>>
>>
>> For me the non-static lazy is a non problem, Java already considers
>> separately final field and static final field, so lazy can be only
>> allowed on static final fields, but neither on static fields nor on
>> non-static fields.
> I tend to agree - allowing dynamic constants in the constant value
> attribute seems to be a straightforward generalization of what we have
> now.
>
> Exploring the space of non-static lazy final is a possibility, but I
> don't think that exploration should block what would seem like a
> generally useful strategy (being forced to use classfile generation
> just because there's no language support for lazy static constants
> seems like a suboptimal situation).
>
> My intuition also tells me that non-static constant will likely not be
> as interesting; at least in the method handle/var handle case you want
> these things to be static - so that C2 can see their constant-ness -
> so, if a "Constable" instance is likely what we'd like to use the lazy
> support for, then I'd say that tying that to "static" seems a
> relatively forced move (at least given the VM we have today).
>
> Maurizio
>
More information about the panama-dev
mailing list