8000 Offer stronger guarantees under the Java Memory Model for List & Vector (using releaseFence) by retronym · Pull Request #6425 · scala/scala · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
merged 5 commits into from
Aug 18, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
J 8000 ump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Combine VM with Statics, and rely on JIT inlining of releaseFence
  • Loading branch information
retronym committed Aug 16, 2018
commit 2c6d90cacce308c68e6c25d92d47f985e79eda5d
18 changes: 9 additions & 9 deletions src/library/scala/collection/immutable/List.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import java.io.{ObjectInputStream, ObjectOutputStream}
import scala.annotation.unchecked.uncheckedVariance
import scala.annotation.tailrec
import mutable.{Builder, ListBuffer}
import scala.runtime.ScalaRunTime
import scala.runtime.Statics.releaseFence

/** A class for immutable linked lists representing ordered collections
* of elements of type `A`.
Expand Down Expand Up @@ -148,7 +148,7 @@ sealed abstract class List[+A]
t = nx
rest = rest.tail
}
ScalaRunTime.releaseFence()
releaseFence()
h
}

Expand Down Expand Up @@ -218,7 +218,7 @@ sealed abstract class List[+A]
t = nx
rest = rest.tail
}
ScalaRunTime.releaseFence()
8000 releaseFence()
h
}
}
Expand All @@ -245,7 +245,7 @@ sealed abstract class List[+A]
}
rest = rest.tail
} while (rest ne Nil)
{ScalaRunTime.releaseFence(); h}
{releaseFence(); h}
}
}
final override def flatMap[B](f: A => IterableOnce[B]): List[B] = {
Expand All @@ -269,7 +269,7 @@ sealed abstract class List[+A]
}
rest = rest.tail
}
if (!found) Nil else {ScalaRunTime.releaseFence(); h}
if (!found) Nil else {releaseFence(); h}
}
}

Expand Down Expand Up @@ -453,7 +453,7 @@ sealed abstract class List[+A]
}
}
val result = loop(null, null, this, this)
ScalaRunTime.releaseFence()
releaseFence()
result
}

Expand Down Expand Up @@ -537,7 +537,7 @@ sealed abstract class List[+A]
}

val result = noneIn(this)
ScalaRunTime.releaseFence()
releaseFence()
result
}

Expand Down Expand Up @@ -565,11 +565,11 @@ sealed abstract class List[+A]

}

// Internal code that mutates `next` _must_ call `ScalaRunTime.releaseFence()` if either immediately, or
// Internal code that mutates `next` _must_ call `Statics.releaseFence()` if either immediately, or
// before a newly-allocated, thread-local :: instance is aliased (e.g. in ListBuffer.toList)
final case class :: [+A](override val head: A, private[scala] var next: List[A @uncheckedVariance]) // sound because `next` is used only locally
extends List[A] {
ScalaRunTime.releaseFence()
releaseFence()
override def isEmpty: Boolean = false
override def headOption: Some[A] = Some(head)
override def tail: List[A] = next
Expand Down
18 changes: 9 additions & 9 deletions src/library/scala/collection/immutable/Vector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.io.{ObjectInputStream, ObjectOutputStream}

import scala.collection.mutable.{Builder, ReusableBuilder}
import scala.annotation.unchecked.uncheckedVariance
import scala.runtime.ScalaRunTime
import scala.runtime.Statics.releaseFence

/** $factoryInfo
* @define Coll `Vector`
Expand Down Expand Up @@ -71,12 +71,12 @@ final class Vector[+A] private[immutable] (private[collection] val startIndex: I

override def iterableFactory: SeqFactory[Vector] = Vector

// Code paths that mutates `dirty` _must_ call `ScalaRunTime.releaseFence()` before returning from
// Code paths that mutates `dirty` _must_ call `Statics.releaseFence()` before returning from
// the public method.
private[immutable] var dirty = false
// While most JDKs would implicit add this fence because of >= 1 final field, the spec only mandates
// it if all fields are final, so let's add this in explicitly.
ScalaRunTime.releaseFence()
releaseFence()

def length: Int = endIndex - startIndex

Expand Down Expand Up @@ -225,7 +225,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()
releaseFence()
s
}

Expand Down Expand Up @@ -319,7 +319,7 @@ final class Vector[+A] private[immutable] (private[collection] val startIndex: I
s.display0 = elems
s
}
ScalaRunTime.releaseFence()
releaseFence()
result
}

Expand Down Expand Up @@ -383,7 +383,7 @@ final class Vector[+A] private[immutable] (private[collection] val startIndex: I
s.display0 = elems
s
}
ScalaRunTime.releaseFence()
releaseFence()
result
}

Expand Down Expand Up @@ -521,7 +521,7 @@ final class Vector[+A] private[immutable] (private[collection] val startIndex: I
s.gotoPosWritable(focus, blockIndex, focus ^ blockIndex)
s.preClean(d)
s.cleanLeftEdge(cutIndex - shift)
ScalaRunTime.releaseFence()
releaseFence()
s
}

Expand All @@ -537,7 +537,7 @@ final class Vector[+A] private[immutable] (private[collection] val startIndex: I
s.gotoPosWritable(focus, blockIndex, focus ^ blockIndex)
s.preClean(d)
s.cleanRightEdge(cutIndex - shift)
ScalaRunTime.releaseFence()
releaseFence()
s
}

Expand Down Expand Up @@ -630,7 +630,7 @@ final class VectorBuilder[A]() extends ReusableBuilder[A, Vector[A]] with Vector
val s = new Vector[A](0, size, 0) // should focus front or back?
s.initFrom(this)
if (depth > 1) s.gotoPos(0, size - 1) // we're currently focused to size - 1, not size!
ScalaRunTime.releaseFence()
releaseFence()
s
}

Expand Down
4 changes: 2 additions & 2 deletions src/library/scala/collection/mutable/ListBuffer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import scala.annotation.tailrec
import scala.collection.immutable.{List, Nil, ::}
import scala.annotation.tailrec
import java.lang.{IllegalArgumentException, IndexOutOfBoundsException}
import scala.runtime.ScalaRunTime
import scala.runtime.Statics.releaseFence

/** A `Buffer` implementation backed by a list. It provides constant time
* prepend and append. Most other operations are linear.
Expand Down Expand Up @@ -65,7 +65,7 @@ class ListBuffer[A]
// We've accumulated a number of mutations to `List.tail` by this stage.
// Make sure they are visible to threads that the client of this ListBuffer might be about
// to share this List with.
ScalaRunTime.releaseFence()
releaseFence()
first
Copy link
Member Author
@retronym retronym Aug 16, 2018

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.

Copy link
Member Author

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.

}

Expand Down
5 changes: 0 additions & 5 deletions src/library/scala/runtime/ScalaRunTime.scala
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,4 @@ object ScalaRunTime {
def wrapShortArray(xs: Array[Short]): ArraySeq[Short] = if (xs ne null) new ArraySeq.ofShort(xs) else null
def wrapBooleanArray(xs: Array[Boolean]): ArraySeq[Boolean] = if (xs ne null) new ArraySeq.ofBoolean(xs) else null
def wrapUnitArray(xs: Array[Unit]): ArraySeq[Unit] = if (xs ne null) new ArraySeq.ofUnit(xs) else null

/**
* Forwards to `VarHandle.releaseFence` or `sun.misc.Unsafe.storeFence` on JDK9+ or JDK8 respectively.
*/
@inline def releaseFence(): Unit = VM.RELEASE_FENCE.invoke()
}
49 changes: 49 additions & 0 deletions src/library/scala/runtime/Statics.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package scala.runtime;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

/** Not for public consumption. Usage by the runtime only.
*/

Expand Down Expand Up @@ -117,4 +122,48 @@ public static int anyHash(Object x) {

/** Used as a marker object to return from PartialFunctions */
public static final Object pfMarker = new Object();

// @ForceInline would be nice here.
public static void releaseFence() throws Throwable {
VM.RELEASE_FENCE.invoke();
}

final static class VM {
static final MethodHandle RELEASE_FENCE;

static {
RELEASE_FENCE = mkHandle();
}

private static MethodHandle mkHandle() {
MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
return lookup.findStatic(Class.forName("java.lang.invoke.VarHandle"), "releaseFence", MethodType.methodType(Void.TYPE));
} catch (ClassNotFoundException e) {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
return lookup.findVirtual(unsafeClass, "storeFence", MethodType.methodType(void.class)).bindTo(findUnsafe(unsafeClass));
} catch (NoSuchMethodException | ClassNotFoundException | IllegalAccessException e1) {
ExceptionInInitializerError error = new ExceptionInInitializerError(e1);
error.addSuppressed(e);
throw error;
}
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}

private static Object findUnsafe(Class<?> unsafeClass) throws IllegalAccessException {
Object found = null;
for (Field field : unsafeClass.getDeclaredFields()) {
Copy link
Member

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?

Copy link

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.

Copy link
Member Author

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 :(

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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?

if (field.getType() == unsafeClass) {
field.setAccessible(true);
found = field.get(null);
break;
}
}
if (found == null) throw new IllegalStateException("No instance of Unsafe found");
return found;
}
}
}
46 changes: 0 additions & 46 deletions src/library/scala/runtime/VM.java

This file was deleted.

4 changes: 2 additions & 2 deletions src/reflect/scala/reflect/internal/tpe/FindMembers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package tpe

import util.StatisticsStatics
import Flags._
import scala.runtime.ScalaRunTime
import scala.runtime.Statics.releaseFence

trait FindMembers {
this: SymbolTable =>
Expand Down Expand Up @@ -282,7 +282,7 @@ trait FindMembers {
} else {
if (StatisticsStatics.areSomeColdStatsEnabled) statistics.incCounter(multMemberCount)
lastM.next = Nil
ScalaRunTime.releaseFence()
releaseFence()
initBaseClasses.head.newOverloaded(tpe, members)
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/reflect/scala/reflect/internal/util/Collections.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package reflect.internal.util
import scala.collection.{immutable, mutable}
import scala.annotation.tailrec
import mutable.ListBuffer
import scala.runtime.ScalaRunTime
import scala.runtime.Statics.releaseFence

/** Profiler driven changes.
* TODO - inlining doesn't work from here because of the bug that
Expand Down Expand Up @@ -59,7 +59,7 @@ trait Collections {
tail = next
rest = rest.tail
}
ScalaRunTime.releaseFence()
releaseFence()
head
}

Expand Down Expand Up @@ -131,7 +131,7 @@ trait Collections {
}
}
val result = loop(null, xs, xs, ys)
ScalaRunTime.releaseFence()
releaseFence()
result
}

Expand Down
0