-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Offer stronger guarantees under the Java Memory Model for List & Vector (using releaseFence) #6425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Even after this change, we still have a gaping hole in
With this, So the argument for a bona-fide immutable List is still very valid. We might be able to use some tricks to make the variable properly private, and have My kingdom for larval object support in the VM. (See 41m https://www.youtube.com/watch?v=VPF8DLnDgE4) |
Is it possible to reduce the visibility of |
@NthPortal I gave that some thought, but there are a few problems that I see. First, Scala's Second, we don't seal the Pre-JDK9, the Java standard library had VM support to seal "java.**".
User libraries can get a similar effect with Sealed packages in JARs. We haven't used that so far, no doubt there would be compatibility problems if and when we start doing this. I suppose we'll have to face up to those problems anyway, once we ship scala-library.jar as a JPMS module, as that new module system has outlawed split packages. We need to develop a better threat model to find a path forward. Scala has to make a bunch of stuff JV M public, so maybe it isn't worth building a strong defense around |
I think a fence is certainly a valuable start, regardless. |
a2682a4
to
e0f4254
Compare
I've added a commit to show what I propose for scala/scala-dev#408 (conforming the the stricter bytecode verifier in classfile format 53) |
I'm tentatively pushing this to M5. |
956121d
to
a24d33a
Compare
I've published an updated |
a24d33a
to
7ea53e7
Compare
Would be good to move this forward for M5. What needs to be done?
|
@@ -477,6 +482,7 @@ sealed abstract class List[+A] | |||
@SerialVersionUID(4L) | |||
case class :: [+A](override val head: A, private[scala] var next: List[A @uncheckedVariance]) // sound because `next` is used only locally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this comment really true given the reachability of the var in the generated bytecode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, this doesn't state the assumption that people won't mutate this from Java with tl_$eq
. But that's a general problem we have (e.g. trait vals are mutable from Java, too)
@@ -148,6 +149,7 @@ sealed abstract class List[+A] | |||
t = nx | |||
rest = rest.tail | |||
} | |||
ScalaRunTime.releaseFence() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For maintainability, I'd suggest each invocation of releaseFence needs a comment explaining what it intends to publish, so that others in the future can reason about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like "Publish assignments to List.{head,next}"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. So if someone is in here, they know what the current intent is. 👍
@@ -218,6 +219,7 @@ final class Vector[+A] private[immutable] (private[collection] val startIndex: I | |||
s.dirty = dirty | |||
s.gotoPosWritable(focus, idx, focus ^ idx) // if dirty commit changes; go to new pos and prepare for writing | |||
s.display0(idx & 31) = elem.asInstanceOf[AnyRef] | |||
ScalaRunTime.releaseFence() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If any of these are removed or inappropriately moved, is there a corresponding bench which aims to stochastically prove that correct results are obtained?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, we should create a jcstress suite to test drive/defend each of these changes. I'll add that before we merge this unless someone else volunteers to help out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@retronym Awesome, thanks Jason!
src/library/scala/runtime/VM.java
Outdated
import java.lang.invoke.MethodType; | ||
import java.lang.reflect.Field; | ||
|
||
public class VM { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure thing.
Speaking of modifiers, I would have preferred to make this package private, and to have indirected call through ScalaRuntime.releaseFence
. But we don't have a way to force VM inlining of ScalaRuntime.releaseFence
, so I wanted to keep it inlinable by the Scala inliner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@retronym Ok. Perhaps document it as an internal API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it would be better, at least initially, to leave inlining of releaseFence
up to the JDK (my kingdom for @ForceInline
!), and make VM
package protected.
src/library/scala/runtime/VM.java
Outdated
|
||
|
||
static { | ||
RELEASE_FENCE = mkHandle(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a requirement as to what thread must invoke the initialization of this to ensure correct access to internals?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is protected by the static init lock on this class, so whichever thread gets here first can safely initialize. RELEASE_FENCE = ...
will be safely published. Do you see this differently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, that part is fine. But since it will be initialized by the first calling thread, and that thread needs to have correct permissions for thisinitialization, does it make sense to try to "force" the initialization from a specific place which we know to be early?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Permissions" as in a SecurityManager
sense? I hadn't thought of that. Do you have a recommendation based on your Akka experience?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@retronym I don't have any "current" recommendations since I haven't been keeping up with how the module system will impact this situation. Perhaps worth asking on the concurrency-interest ML?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're running on 9, lookup.findStatic(Class.forName("java.lang.invoke.VarHandle"), "releaseFence", MethodType.methodType(Void.TYPE));
will work, so we don't need to worry about the module system preventing the fallback case from working.
I guess we could initialize VM
from Predef
, but which Thread
is going to initialize Predef
? If we do find a problem in the wild, the user will have a workaround of initializing this in their application entry point, and we can revisit.
b395c5c
to
5aa8c14
Compare
I've taken a shot at writing a failing My attempt is included here, you can try it with:
I've tried to create a test to show the issue with a I'd expect that to fail occasionally when /cc @ktoso |
/** | ||
* Forwards to `VarHandle.releaseFence` or `sun.misc.Unsafe.storeFence` on JDK9+ or JDK8 respectively. | ||
*/ | ||
@inline def releaseFence(): Unit = VM.RELEASE_FENCE.invoke() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice 👍 😍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed! 👍
@@ -470,6 +475,7 @@ sealed abstract class List[+A] | |||
|
|||
case class :: [+A](override val head: A, private[scala] var next: List[A @uncheckedVariance]) // sound because `next` is used only locally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is :: meant to be extendable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be final. List
is sealed, but that doesn't prevent non-Scala subclasses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't prevent Scala subclasses either!
$ scala
Welcome to Scala 2.13.0-M4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_40).
Type in expressions for evaluation. Or try :help.
scala> class Oops extends ::(5, Nil)
defined class Oops
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still forget about that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final all the things!
@lrytz @adriaanm I've split the trait setter to a separate PR. I'll do another pass to address @viktorklang's comments on Monday. I wasn't able to make |
@retronym maybe rebasing will restore Scabot to its senses |
c40c26b
to
6541df7
Compare
I took the liberty of rebasing and force-pushing |
@@ -61,6 +62,7 @@ class ListBuffer[A] | |||
// Avoids copying where possible. | |||
override def toList: List[A] = { | |||
aliased = nonEmpty | |||
ScalaRunTime.releaseFence() | |||
first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Echoing @adriaanm's comments from #7028 that belong over in this PR.
Why no fence for the other methods that mutate? I see they're private, but they are called from public methods, I assume?
My rationale is that this is the where the reference to the List
is given out. Only after this point could client code share it with another thread. My assumption is that users are only using the ListBuilder
itself on a single thread, and its only the resulting List
that needs to be safely publishible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to add the fence after each mutation of tail
, at least initially, to make things easier to reason about.
@@ -219,6 +220,7 @@ final class Vector[+A] private[immutable] (private[collection] val startIndex: I | |||
s.dirty = dirty | |||
s.gotoPosWritable(focus, idx, focus ^ idx) // if dirty commit changes; go to new pos and prepare for writing | |||
s.display0(idx & 31) = elem.asInstanceOf[AnyRef] | |||
ScalaRunTime.releaseFence() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Echoing @adriaanm's comments from #7028 that belong over in this PR.
is this to publish the mutation of dirty?
Yes, that's the intent.
Should we also do this in the constructor?
Good catch, I hadn't considered that. I think we get away without one because it starts with the default value. I'll add a comment to that effect. Also happy to add the fence if that seems to subtle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thoughts, I'll add the fence. We need the memory barrier to safely publish the assignments to the other val
fields in Vector, and the presence of a single non-final field means the implicit fence at the end of the constructor is not guaranteed (although it is added in practice for classes with >= 1 final fields)
I added a commit that moves If we agree that this is the right approach, I'll need to update the other PR to target this new method. |
|
||
private static Object findUnsafe(Class<?> unsafeClass) throws IllegalAccessException { | ||
Object found = null; | ||
for (Field field : unsafeClass.getDeclaredFields()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have experience with Unsafe, so I googled a bit. https://dzone.com/articles/understanding-sunmiscunsafe suggest that you could create a new Unsafe instance instead of getting the singleton, as the latter doesn't work on Android. Not sure if that's still relevant.
Apart from that, the use of setAccessible(true)
in List requires basically reflection to be allowed when running any Scala program. Was that the case before? Could security managers be configured in a fine-grained way to allow reflection only for this particular access?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless things changed without me noticing Android is not really Java8+ so Scala nowadays does not work on it in any case, so most likely this should not be a consideration here. Though one may want to check if I'm up to date about that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point re: setAccessible
. A similar issue was reported with our use of that in structural types: scala/bug#2318 (comment)
In that case, we catch the SecurityException
and fall back to doing nothing.
We could do something similar here, I guess, but it would be good to alert the user somehow that the memory barriers aren't supported in that configuration. For instance, we could throw an exception that says "restart with -Dscala.barrier.noop=true to accept this limitation". Not nice :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only affect users on Java 8, of course. On 9+ we just use the public VarHandle
API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not very nice, but I think that's our only option. Silently skipping the fence is certainly not a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the proposal discussed here wasn't implemented in this PR. Should a ticket on http://github.com/scala/scala-dev/ be created to track this?
@retronym can I go and ahead merge this? it looks like there isn't anything here that we can't tweak further before RC1? |
@SethTisue The last commit introduces a semantic merge conflict with #7028. I'll drop it from the PR and resubmit it separately along with some better error handling for platforms where |
Hmmm, on second thought I think its better to fix the semantic merge conflict right away, rather than risk bootstrapping issues with modules or the by letting the sands of scala.runtime._ shift needlessly. |
Use memory barrier after mutating List and Vector internals. The barrier is
the same one that the JDK adds to the end of a constructor of a class with
final fields.
The general pattern is that we have a just-created "larval" object graph,
which undergoes some mutation (e.g. appending elements to the tail of the
list in the builder). We return that larval object to the caller of, say,
ListBuilder.build or List.map, and they could store it to a plain field
which would let another thread observe it. According the the JMM, the other
thread might not see all the mutations.
The release fence will prevent re-ordering of stores that preceed it
(the tail mutation, for example) it with stores that suceeed it (the
caller of
build
storing the resulting list to a field).The runtime support is a bit complicated becausse we need to be cross
compatible with JDK 8 and 9+. We use method handle based reflection
to achieve this. This has zero runtime overhead in the manner used.
I'm putting this PR forward as WIP as part of the discussion in
scala/collection-strawman#50
Fixes scala/bug#7838