scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.88k stars 1.06k forks source link

Runtime code implementing lazy val should not use sun.misc.Unsafe on Java 9+ (JEP-471) #9013

Open smarter opened 4 years ago

smarter commented 4 years ago

Currently, any usage of a lazy val requires getitng an instance of sun.misc.Unsafe at runtime: https://github.com/lampepfl/dotty/blob/43e4bfa5598e9cdbebb1dc56ed25a319d5aa8fbe/library/src/dotty/runtime/LazyVals.scala#L7 which is problematic for various reasons (e.g. usage of a security manager, using Graal Native (https://github.com/lampepfl/dotty/issues/13985)).

On Java 8, there's no good alternative, but on Java 9+ we should be able to replace that using VarHandle.

We should be able to use the same trick used in scala.runtime.Statics to check once at runtime if VarHandle is available to stay compatible with Java 8: https://github.com/scala/scala/blob/a8a726118d06c90b5506f907b1524457c0d401a7/src/library/scala/runtime/Statics.java#L158-L173 (EDIT: actually I don't think this is good enough: using VarHandle would require adding static fields in any class that has a lazy val, so we need to know at compile-time the version of Java we support (via -release/-Xtarget)

stewSquared commented 1 year ago

Here's some prototype target code @smarter paired with me to write:

```java /* * Decompiled with CFR 0.152. * * Could not load the following classes: * scala.runtime.LazyVals$ */ import scala.runtime.LazyVals$; import java.lang.invoke.VarHandle; import java.lang.invoke.MethodHandles; /* * Illegal identifiers - consider using --renameillegalidents true */ public class Decompiled { // public static final long OFFSET$0 = LazyVals$.MODULE$.getOffsetStatic(LazyVal.package.Foo.1.class.getDeclaredField("bitmap$1")); // get a varhandle instead of an offset private static final VarHandle handle; static { try { MethodHandles.Lookup l = MethodHandles.lookup(); handle = l.findVarHandle(Decompiled.class, "bitmap$1", long.class); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } } public long bitmap$1; public int value$lzy1; static final long LAZY_VAL_MASK = 3; static final long BITS_PER_LAZY_VAL = 2; public long casHelper(long current, long newState, int ord) { long mask = ~(LAZY_VAL_MASK << ord * BITS_PER_LAZY_VAL); return (current & mask) | (newState << (ord * BITS_PER_LAZY_VAL)); } private boolean CAS(Object t, long current, long newState, int ord) { return handle.compareAndSet(t, current, casHelper(current, newState, ord)); } public static void setFlag(Object t, int v, int ord) { boolean retry = true; while (retry) { long cur = handle.getVolatile(t); if (LazyVals$.MODULE$.STATE(cur, ord) == 1) retry = !CAS(t, cur, v, ord); else { // cur == 2, somebody is waiting on monitor if (CAS(t, cur, v, ord)) { Class clazz = LazyVals$.class; var privateMethod = clazz.getDeclaredMethod("getMonitor", Object.class, int.class); privateMethod.setAccessible(true); Object monitor = privateMethod.invokew(LazyVals$.MODULE$, t, ord); synchronized (monitor) { monitor.notifyAll(); } retry = false; } } } } public static void wait4Notification(Object t, long cur, int ord) { boolean retry = true; while (retry) { long current = handle.getVolatile(t); int state = LazyVals$.MODULE$.STATE(current, ord); if (state == 1) CAS(t, current, 2, ord); else if (state == 2) { Class clazz = LazyVals$.class; var privateMethod = clazz.getDeclaredMethod("getMonitor", Object.class, int.class); privateMethod.setAccessible(true); Object monitor = privateMethod.invokew(LazyVals$.MODULE$, t, ord); synchronized (monitor) { if (LazyVal$.MODULE$.STATE(handle.getVolatile(t), ord) == 2) // make sure notification did not happen yet. monitor.wait(); } } else { retry = false; } } } public int value() { long l; long l2; while ((l2 = LazyVals$.MODULE$.STATE(l = handle.getVolatile(this), 0)) != 3L) { if (l2 == 0L) { if (!handle.compareAndSet(this, l, casHelper(l, 1, 0))) continue; try { int n; this.value$lzy1 = n = 13; handle. setFlag((Object)this, 3, 0); return n; } catch (Throwable throwable) { setFlag((Object)this, 0, 0); throw throwable; } } wait4Notification((Object)this, l, 0); } return this.value$lzy1; } } ```
reid-spencer commented 8 months ago

This occurs for me in another context: using jlink to minimize the JVM foot print for a command line tool. My tool only uses the java.base module, so I want to eliminate 600MB of JDK 21 down to a 95MB JDK to deliver with my application. Since my use of threading exceeds the threading capabilities of Scala Native, I can't use that. The same problem occurs with GraalVM (see #13985). So, I'm blocked from minimizing my tool's footprint. :(

This occurs with Scala 3.3.3.

reid-spencer commented 8 months ago

Also: in response to this from the issue description:

actually I don't think this is good enough: using VarHandle would require adding static fields in any class that has a lazy val, so we need to know at compile-time the version of Java we support (via -release/-Xtarget)

I'd be happy if Scala declared a minimum JVM version to the version that supports adding static fields in any class. Didn't the JVM just add its own bytecode manipulation API? I'm okay with JDK 21 as the minimum for Scala, and I'm sure others are not.

alexklibisz commented 6 months ago

Noting that also comes up when using the new given keyword. implicit val and implicit def do not invoke LazyVals and Unsafe. given does invoke LazyVals and Unsafe.

alexklibisz commented 6 months ago

OOC, why is this not a problem with lazy vals in 2.12 and 2.13? Is there something fundamentally different about lazy vals in 3.x?

SethTisue commented 6 months ago

@alexklibisz yes, see https://github.com/scala/scala3/pull/15296 (and linked history stretching at least as far back as https://github.com/scala/scala3/pull/6979)

Gedochao commented 4 months ago

Note: JEP-471 deprecates memory access methods in sun.misc.Unsafe for removal. https://openjdk.org/jeps/471 (thanks to @smarter for raising this)

bishabosha commented 4 months ago

so I guess you can use some static methodhandles that resolve to the right thing at runtime?

konsoletyper commented 4 days ago

BTW, Kotlin uses AtomicReferenceFieldUpdater. Oracle did some work to improve its performance.

And for me personally most important that this ABI depends on the idea that object is located somewhere in flat memory with fields available by some offsets. For some JVM implementations this not necessarily true. So it would be a good step forward if at least at ABI level there was something more high-level, at least similar to interface provided by AtomicReferenceFieldUpdater. I think OpenJDK is smart enough to optimize any overhead here.