Skip to content

Commit 4df9c87

Browse files
author
Andrew Haley
committed
8360884: Better scoped values
Reviewed-by: liach, alanb
1 parent 9449fea commit 4df9c87

File tree

3 files changed

+116
-51
lines changed

3 files changed

+116
-51
lines changed

src/java.base/share/classes/java/lang/ScopedValue.java

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,19 @@
2626

2727
package java.lang;
2828

29+
import java.lang.ref.Reference;
2930
import java.util.NoSuchElementException;
3031
import java.util.Objects;
31-
import java.lang.ref.Reference;
32-
import java.util.concurrent.StructuredTaskScope;
3332
import java.util.concurrent.StructureViolationException;
33+
import java.util.concurrent.StructuredTaskScope;
34+
import java.util.function.IntSupplier;
3435
import java.util.function.Supplier;
3536
import jdk.internal.access.JavaUtilConcurrentTLRAccess;
3637
import jdk.internal.access.SharedSecrets;
38+
import jdk.internal.vm.ScopedValueContainer;
3739
import jdk.internal.vm.annotation.ForceInline;
3840
import jdk.internal.vm.annotation.Hidden;
39-
import jdk.internal.vm.ScopedValueContainer;
41+
import jdk.internal.vm.annotation.Stable;
4042

4143
/**
4244
* A value that may be safely and efficiently shared to methods without using method
@@ -244,6 +246,9 @@ public final class ScopedValue<T> {
244246
@Override
245247
public int hashCode() { return hash; }
246248

249+
@Stable
250+
static IntSupplier hashGenerator;
251+
247252
/**
248253
* An immutable map from {@code ScopedValue} to values.
249254
*
@@ -526,7 +531,8 @@ public static <T> Carrier where(ScopedValue<T> key, T value) {
526531
}
527532

528533
private ScopedValue() {
529-
this.hash = generateKey();
534+
IntSupplier nextHash = hashGenerator;
535+
this.hash = nextHash != null ? nextHash.getAsInt() : generateKey();
530536
}
531537

532538
/**
@@ -552,11 +558,11 @@ public T get() {
552558
// This code should perhaps be in class Cache. We do it
553559
// here because the generated code is small and fast and
554560
// we really want it to be inlined in the caller.
555-
int n = (hash & Cache.SLOT_MASK) * 2;
561+
int n = (hash & Cache.Constants.SLOT_MASK) * 2;
556562
if (objects[n] == this) {
557563
return (T)objects[n + 1];
558564
}
559-
n = ((hash >>> Cache.INDEX_BITS) & Cache.SLOT_MASK) * 2;
565+
n = ((hash >>> Cache.INDEX_BITS) & Cache.Constants.SLOT_MASK) * 2;
560566
if (objects[n] == this) {
561567
return (T)objects[n + 1];
562568
}
@@ -580,11 +586,11 @@ private T slowGet() {
580586
public boolean isBound() {
581587
Object[] objects = scopedValueCache();
582588
if (objects != null) {
583-
int n = (hash & Cache.SLOT_MASK) * 2;
589+
int n = (hash & Cache.Constants.SLOT_MASK) * 2;
584590
if (objects[n] == this) {
585591
return true;
586592
}
587-
n = ((hash >>> Cache.INDEX_BITS) & Cache.SLOT_MASK) * 2;
593+
n = ((hash >>> Cache.INDEX_BITS) & Cache.Constants.SLOT_MASK) * 2;
588594
if (objects[n] == this) {
589595
return true;
590596
}
@@ -688,17 +694,17 @@ private static Snapshot scopedValueBindings() {
688694

689695
private static int nextKey = 0xf0f0_f0f0;
690696

691-
// A Marsaglia xor-shift generator used to generate hashes. This one has full period, so
692-
// it generates 2**32 - 1 hashes before it repeats. We're going to use the lowest n bits
693-
// and the next n bits as cache indexes, so we make sure that those indexes map
694-
// to different slots in the cache.
697+
// A Marsaglia xor-shift generator used to generate hashes. This one has
698+
// full period, so it generates 2**32 - 1 hashes before it repeats. We're
699+
// going to use the lowest n bits and the next n bits as cache indexes, so
700+
// we make sure that those indexes map to different slots in the cache.
695701
private static synchronized int generateKey() {
696702
int x = nextKey;
697703
do {
698704
x ^= x >>> 12;
699705
x ^= x << 9;
700706
x ^= x >>> 23;
701-
} while (Cache.primarySlot(x) == Cache.secondarySlot(x));
707+
} while (((Cache.primaryIndex(x) ^ Cache.secondaryIndex(x)) & 1) == 0);
702708
return (nextKey = x);
703709
}
704710

@@ -709,7 +715,7 @@ private static synchronized int generateKey() {
709715
* @return the bitmask
710716
*/
711717
int bitmask() {
712-
return (1 << Cache.primaryIndex(this)) | (1 << (Cache.secondaryIndex(this) + Cache.TABLE_SIZE));
718+
return (1 << Cache.primaryIndex(hash)) | (1 << (Cache.secondaryIndex(hash) + Cache.TABLE_SIZE));
713719
}
714720

715721
// Return true iff bitmask, considered as a set of bits, contains all
@@ -727,57 +733,100 @@ private static final class Cache {
727733
static final int TABLE_MASK = TABLE_SIZE - 1;
728734
static final int PRIMARY_MASK = (1 << TABLE_SIZE) - 1;
729735

730-
// The number of elements in the cache array, and a bit mask used to
731-
// select elements from it.
732-
private static final int CACHE_TABLE_SIZE, SLOT_MASK;
733-
// The largest cache we allow. Must be a power of 2 and greater than
734-
// or equal to 2.
735-
private static final int MAX_CACHE_SIZE = 16;
736-
737-
static {
738-
final String propertyName = "java.lang.ScopedValue.cacheSize";
739-
var sizeString = System.getProperty(propertyName, "16");
740-
var cacheSize = Integer.valueOf(sizeString);
741-
if (cacheSize < 2 || cacheSize > MAX_CACHE_SIZE) {
742-
cacheSize = MAX_CACHE_SIZE;
743-
System.err.println(propertyName + " is out of range: is " + sizeString);
744-
}
745-
if ((cacheSize & (cacheSize - 1)) != 0) { // a power of 2
746-
cacheSize = MAX_CACHE_SIZE;
747-
System.err.println(propertyName + " must be an integer power of 2: is " + sizeString);
736+
737+
// This class serves to defer initialization of some values until they
738+
// are needed. In particular, we must not invoke System.getProperty
739+
// early in the JDK boot process, because that leads to a circular class
740+
// initialization dependency.
741+
//
742+
// In more detail:
743+
//
744+
// The size of the cache depends on System.getProperty. Generating the
745+
// hash of an instance of ScopedValue depends on ThreadLocalRandom.
746+
//
747+
// Invoking either of these early in the JDK boot process will cause
748+
// startup to fail with an unrecoverable circular dependency.
749+
//
750+
// To break these cycles we allow scoped values to be created (but not
751+
// used) without invoking either System.getProperty or
752+
// ThreadLocalRandom. To do this we defer querying System.getProperty
753+
// until the first reference to CACHE_TABLE_SIZE, and we define a local
754+
// hash generator which is used until CACHE_TABLE_SIZE is initialized.
755+
756+
private static class Constants {
757+
// The number of elements in the cache array, and a bit mask used to
758+
// select elements from it.
759+
private static final int CACHE_TABLE_SIZE, SLOT_MASK;
760+
// The largest cache we allow. Must be a power of 2 and greater than
761+
// or equal to 2.
762+
private static final int MAX_CACHE_SIZE = 16;
763+
764+
private static final JavaUtilConcurrentTLRAccess THREAD_LOCAL_RANDOM_ACCESS
765+
= SharedSecrets.getJavaUtilConcurrentTLRAccess();
766+
767+
static {
768+
final String propertyName = "java.lang.ScopedValue.cacheSize";
769+
var sizeString = System.getProperty(propertyName, "16");
770+
var cacheSize = Integer.valueOf(sizeString);
771+
if (cacheSize < 2 || cacheSize > MAX_CACHE_SIZE) {
772+
cacheSize = MAX_CACHE_SIZE;
773+
System.err.println(propertyName + " is out of range: is " + sizeString);
774+
}
775+
if ((cacheSize & (cacheSize - 1)) != 0) { // a power of 2
776+
cacheSize = MAX_CACHE_SIZE;
777+
System.err.println(propertyName + " must be an integer power of 2: is " + sizeString);
778+
}
779+
CACHE_TABLE_SIZE = cacheSize;
780+
SLOT_MASK = cacheSize - 1;
781+
782+
// hashGenerator is set here (in class Constants rather than
783+
// in global scope) in order not to initialize
784+
// j.u.c.ThreadLocalRandom early in the JDK boot process.
785+
// After this static initialization, new instances of
786+
// ScopedValue will be initialized by a thread-local random
787+
// generator.
788+
hashGenerator = new IntSupplier() {
789+
@Override
790+
public int getAsInt() {
791+
int x;
792+
do {
793+
x = THREAD_LOCAL_RANDOM_ACCESS
794+
.nextSecondaryThreadLocalRandomSeed();
795+
} while (Cache.primarySlot(x) == Cache.secondarySlot(x));
796+
return x;
797+
}
798+
};
748799
}
749-
CACHE_TABLE_SIZE = cacheSize;
750-
SLOT_MASK = cacheSize - 1;
751800
}
752801

753-
static int primaryIndex(ScopedValue<?> key) {
754-
return key.hash & TABLE_MASK;
802+
static int primaryIndex(int hash) {
803+
return hash & Cache.TABLE_MASK;
755804
}
756805

757-
static int secondaryIndex(ScopedValue<?> key) {
758-
return (key.hash >> INDEX_BITS) & TABLE_MASK;
806+
static int secondaryIndex(int hash) {
807+
return (hash >> INDEX_BITS) & Cache.TABLE_MASK;
759808
}
760809

761810
private static int primarySlot(ScopedValue<?> key) {
762-
return key.hashCode() & SLOT_MASK;
811+
return key.hashCode() & Constants.SLOT_MASK;
763812
}
764813

765814
private static int secondarySlot(ScopedValue<?> key) {
766-
return (key.hash >> INDEX_BITS) & SLOT_MASK;
815+
return (key.hash >> INDEX_BITS) & Constants.SLOT_MASK;
767816
}
768817

769818
static int primarySlot(int hash) {
770-
return hash & SLOT_MASK;
819+
return hash & Constants.SLOT_MASK;
771820
}
772821

773822
static int secondarySlot(int hash) {
774-
return (hash >> INDEX_BITS) & SLOT_MASK;
823+
return (hash >> INDEX_BITS) & Constants.SLOT_MASK;
775824
}
776825

777826
static void put(ScopedValue<?> key, Object value) {
778827
Object[] theCache = scopedValueCache();
779828
if (theCache == null) {
780-
theCache = new Object[CACHE_TABLE_SIZE * 2];
829+
theCache = new Object[Constants.CACHE_TABLE_SIZE * 2];
781830
setScopedValueCache(theCache);
782831
}
783832
// Update the cache to replace one entry with the value we just looked up.
@@ -813,26 +862,23 @@ private static void setKey(Object[] objs, int n, Object key) {
813862
objs[n * 2] = key;
814863
}
815864

816-
private static final JavaUtilConcurrentTLRAccess THREAD_LOCAL_RANDOM_ACCESS
817-
= SharedSecrets.getJavaUtilConcurrentTLRAccess();
818-
819865
// Return either true or false, at pseudo-random, with a bias towards true.
820866
// This chooses either the primary or secondary cache slot, but the
821867
// primary slot is approximately twice as likely to be chosen as the
822868
// secondary one.
823869
private static boolean chooseVictim() {
824-
int r = THREAD_LOCAL_RANDOM_ACCESS.nextSecondaryThreadLocalRandomSeed();
870+
int r = Constants.THREAD_LOCAL_RANDOM_ACCESS.nextSecondaryThreadLocalRandomSeed();
825871
return (r & 15) >= 5;
826872
}
827873

828874
// Null a set of cache entries, indicated by the 1-bits given
829875
static void invalidate(int toClearBits) {
830-
toClearBits = (toClearBits >>> TABLE_SIZE) | (toClearBits & PRIMARY_MASK);
876+
toClearBits = ((toClearBits >>> Cache.TABLE_SIZE) | toClearBits) & PRIMARY_MASK;
831877
Object[] objects;
832878
if ((objects = scopedValueCache()) != null) {
833879
for (int bits = toClearBits; bits != 0; ) {
834880
int index = Integer.numberOfTrailingZeros(bits);
835-
setKeyAndObjectAt(objects, index & SLOT_MASK, null, null);
881+
setKeyAndObjectAt(objects, index & Constants.SLOT_MASK, null, null);
836882
bits &= ~1 << index;
837883
}
838884
}

test/micro/org/openjdk/bench/java/lang/ScopedValues.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ public int thousandIsBoundQueries(Blackhole bh) throws Exception {
8080
return result;
8181
}
8282

83+
@Benchmark
84+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
85+
public int thousandUnboundQueries(Blackhole bh) throws Exception {
86+
var result = 0;
87+
for (int i = 0; i < 1_000; i++) {
88+
result += ScopedValuesData.unbound.isBound() ? 1 : 0;
89+
}
90+
return result;
91+
}
92+
8393
@Benchmark
8494
@OutputTimeUnit(TimeUnit.NANOSECONDS)
8595
public int thousandMaybeGets(Blackhole bh) throws Exception {
@@ -213,4 +223,11 @@ public void counter_ThreadLocal() {
213223
var ctr = tl_atomicInt.get();
214224
ctr.setPlain(ctr.getPlain() + 1);
215225
}
226+
227+
@Benchmark
228+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
229+
public Object newInstance() {
230+
ScopedValue<Integer> val = ScopedValue.newInstance();
231+
return val;
232+
}
216233
}

test/micro/org/openjdk/bench/java/lang/ScopedValuesData.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,13 @@ public class ScopedValuesData {
5757
public static void run(Runnable action) {
5858
try {
5959
tl1.set(42); tl2.set(2); tl3.set(3); tl4.set(4); tl5.set(5); tl6.set(6);
60-
tl1.get(); // Create the ScopedValue cache as a side effect
6160
tl_atomicInt.set(new AtomicInteger());
6261
VALUES.where(sl_atomicInt, new AtomicInteger())
6362
.where(sl_atomicRef, new AtomicReference<>())
64-
.run(action);
63+
.run(() -> {
64+
sl1.get(); // Create the ScopedValue cache as a side effect
65+
action.run();
66+
});
6567
} finally {
6668
tl1.remove(); tl2.remove(); tl3.remove(); tl4.remove(); tl5.remove(); tl6.remove();
6769
tl_atomicInt.remove();

0 commit comments

Comments
 (0)