Skip to content

Commit 812453c

Browse files
authored
Merge pull request scala#9571 from dwijnand/rework-AlmostFinalValue
Rework AlmostFinalValue, to make it as fast as possible
2 parents ba2f3bb + 807beb6 commit 812453c

File tree

6 files changed

+103
-124
lines changed

6 files changed

+103
-124
lines changed

build.sbt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,12 +664,13 @@ lazy val bench = project.in(file("test") / "benchmarks")
664664
name := "test-benchmarks",
665665
autoScalaLibrary := false,
666666
crossPaths := true, // needed to enable per-scala-version source directories (https://github.com/sbt/sbt/pull/1799)
667+
compileOrder := CompileOrder.JavaThenScala, // to allow inlining from Java ("... is defined in a Java source (mixed compilation), no bytecode is available")
667668
libraryDependencies += "org.openjdk.jol" % "jol-core" % "0.10",
668669
libraryDependencies ++= {
669670
if (benchmarkScalaVersion == "") Nil
670671
else "org.scala-lang" % "scala-compiler" % benchmarkScalaVersion :: Nil
671672
},
672-
scalacOptions ++= Seq("-feature", "-opt:l:inline", "-opt-inline-from:scala.**")
673+
scalacOptions ++= Seq("-feature", "-opt:l:inline", "-opt-inline-from:scala/**", "-opt-warnings"),
673674
).settings(inConfig(JmhPlugin.JmhKeys.Jmh)(scalabuild.JitWatchFilePlugin.jitwatchSettings))
674675

675676

src/reflect/scala/reflect/internal/util/AlmostFinalValue.java

Lines changed: 23 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -14,93 +14,35 @@
1414

1515
import java.lang.invoke.MethodHandle;
1616
import java.lang.invoke.MethodHandles;
17-
import java.lang.invoke.MethodType;
1817
import java.lang.invoke.MutableCallSite;
19-
import java.lang.invoke.SwitchPoint;
2018

2119
/**
2220
* Represents a value that is wrapped with JVM machinery to allow the JVM
23-
* to speculate on its content and effectively optimize it as if it was final.
24-
*
25-
* This file has been drawn from JSR292 cookbook created by Rémi Forax.
26-
* https://code.google.com/archive/p/jsr292-cookbook/. The explanation of the strategy
27-
* can be found in https://community.oracle.com/blogs/forax/2011/12/17/jsr-292-goodness-almost-static-final-field.
28-
*
29-
* Before copying this file to the repository, I tried to adapt the most important
30-
* parts of this implementation and special case it for `Statistics`, but that
31-
* caused an important performance penalty (~10%). This performance penalty is
32-
* due to the fact that using `static`s for the method handles and all the other
21+
* to speculate on its content and effectively optimize it as if it was a constant.
22+
*
23+
* Originally from the JSR-292 cookbook created by Rémi Forax:
24+
* https://code.google.com/archive/p/jsr292-cookbook/.
25+
*
26+
* Implemented in Java because using `static`s for the method handles and all the other
3327
* fields is extremely important for the JVM to correctly optimize the code, and
3428
* we cannot do that if we make `Statistics` an object extending `MutableCallSite`
35-
* in Scala. We instead rely on the Java implementation that uses a boxed representation.
29+
* in Scala.
30+
*
31+
* Subsequently specialised for booleans, to avoid needless Boolean boxing.
32+
*
33+
* Finally reworked to default to false and only allow for the value to be toggled on,
34+
* using Rémi Forax's newer "MostlyConstant" as inspiration, in https://github.com/forax/exotic.
3635
*/
37-
public class AlmostFinalValue {
38-
private final AlmostFinalCallSite callsite =
39-
new AlmostFinalCallSite(this);
40-
41-
protected boolean initialValue() {
42-
return false;
43-
}
44-
45-
public MethodHandle createGetter() {
46-
return callsite.dynamicInvoker();
47-
}
48-
49-
public void setValue(boolean value) {
50-
callsite.setValue(value);
51-
}
52-
53-
private static class AlmostFinalCallSite extends MutableCallSite {
54-
private Boolean value;
55-
private SwitchPoint switchPoint;
56-
private final AlmostFinalValue volatileFinalValue;
57-
private final MethodHandle fallback;
58-
private final Object lock;
59-
60-
private static final Boolean NONE = null;
61-
private static final MethodHandle FALLBACK;
62-
static {
63-
try {
64-
FALLBACK = MethodHandles.lookup().findVirtual(AlmostFinalCallSite.class, "fallback",
65-
MethodType.methodType(Boolean.TYPE));
66-
} catch (NoSuchMethodException|IllegalAccessException e) {
67-
throw new AssertionError(e.getMessage(), e);
68-
}
69-
}
70-
71-
AlmostFinalCallSite(AlmostFinalValue volatileFinalValue) {
72-
super(MethodType.methodType(Boolean.TYPE));
73-
Object lock = new Object();
74-
MethodHandle fallback = FALLBACK.bindTo(this);
75-
synchronized(lock) {
76-
value = null;
77-
switchPoint = new SwitchPoint();
78-
setTarget(fallback);
79-
}
80-
this.volatileFinalValue = volatileFinalValue;
81-
this.lock = lock;
82-
this.fallback = fallback;
83-
}
36+
final class AlmostFinalValue {
37+
private static final MethodHandle K_FALSE = MethodHandles.constant(boolean.class, false);
38+
private static final MethodHandle K_TRUE = MethodHandles.constant(boolean.class, true);
39+
40+
private final MutableCallSite callsite = new MutableCallSite(K_FALSE);
41+
final MethodHandle invoker = callsite.dynamicInvoker();
8442

85-
boolean fallback() {
86-
synchronized(lock) {
87-
Boolean value = this.value;
88-
if (value == NONE) {
89-
value = volatileFinalValue.initialValue();
90-
}
91-
MethodHandle target = switchPoint.guardWithTest(MethodHandles.constant(Boolean.TYPE, value), fallback);
92-
setTarget(target);
93-
return value;
94-
}
95-
}
96-
97-
void setValue(boolean value) {
98-
synchronized(lock) {
99-
SwitchPoint switchPoint = this.switchPoint;
100-
this.value = value;
101-
this.switchPoint = new SwitchPoint();
102-
SwitchPoint.invalidateAll(new SwitchPoint[] {switchPoint});
103-
}
104-
}
43+
void toggleOnAndDeoptimize() {
44+
if (callsite.getTarget() == K_TRUE) return;
45+
callsite.setTarget(K_TRUE);
46+
MutableCallSite.syncAll(new MutableCallSite[] { callsite });
10547
}
106-
}
48+
}

src/reflect/scala/reflect/internal/util/Statistics.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ quant)
301301
@inline final def enabled: Boolean = areColdStatsLocallyEnabled
302302
def enabled_=(cond: Boolean) = {
303303
if (cond && !enabled) {
304-
StatisticsStatics.enableColdStats()
304+
StatisticsStatics.enableColdStatsAndDeoptimize()
305305
areColdStatsLocallyEnabled = true
306306
}
307307
}
@@ -310,7 +310,7 @@ quant)
310310
@inline final def hotEnabled: Boolean = enabled && areHotStatsLocallyEnabled
311311
def hotEnabled_=(cond: Boolean) = {
312312
if (cond && enabled && !areHotStatsLocallyEnabled) {
313-
StatisticsStatics.enableHotStats()
313+
StatisticsStatics.enableHotStatsAndDeoptimize()
314314
areHotStatsLocallyEnabled = true
315315
}
316316
}

src/reflect/scala/reflect/internal/util/StatisticsStatics.java

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
package scala.reflect.internal.util;
1414

15-
import scala.reflect.internal.util.AlmostFinalValue;
1615
import java.lang.invoke.MethodHandle;
1716

1817
/**
@@ -22,46 +21,15 @@
2221
* which helps performance (see docs to find out why).
2322
*/
2423
public final class StatisticsStatics {
25-
private static final AlmostFinalValue COLD_STATS = new AlmostFinalValue() {
26-
@Override
27-
protected boolean initialValue() {
28-
return false;
29-
}
30-
};
24+
private static final AlmostFinalValue COLD_STATS = new AlmostFinalValue();
25+
private static final AlmostFinalValue HOT_STATS = new AlmostFinalValue();
3126

32-
private static final AlmostFinalValue HOT_STATS = new AlmostFinalValue() {
33-
@Override
34-
protected boolean initialValue() {
35-
return false;
36-
}
37-
};
27+
private static final MethodHandle COLD_STATS_GETTER = COLD_STATS.invoker;
28+
private static final MethodHandle HOT_STATS_GETTER = HOT_STATS.invoker;
3829

39-
private static final MethodHandle COLD_STATS_GETTER = COLD_STATS.createGetter();
40-
private static final MethodHandle HOT_STATS_GETTER = HOT_STATS.createGetter();
41-
42-
public static boolean areSomeColdStatsEnabled() throws Throwable {
43-
return (boolean) COLD_STATS_GETTER.invokeExact();
44-
}
30+
public static boolean areSomeColdStatsEnabled() throws Throwable { return (boolean) COLD_STATS_GETTER.invokeExact(); }
31+
public static boolean areSomeHotStatsEnabled() throws Throwable { return (boolean) HOT_STATS_GETTER.invokeExact(); }
4532

46-
public static boolean areSomeHotStatsEnabled() throws Throwable {
47-
return (boolean) HOT_STATS_GETTER.invokeExact();
48-
}
49-
50-
public static void enableColdStats() throws Throwable {
51-
if (!areSomeColdStatsEnabled())
52-
COLD_STATS.setValue(true);
53-
}
54-
55-
public static void disableColdStats() {
56-
COLD_STATS.setValue(false);
57-
}
58-
59-
public static void enableHotStats() throws Throwable {
60-
if (!areSomeHotStatsEnabled())
61-
HOT_STATS.setValue(true);
62-
}
63-
64-
public static void disableHotStats() {
65-
HOT_STATS.setValue(false);
66-
}
33+
public static void enableColdStatsAndDeoptimize() { COLD_STATS.toggleOnAndDeoptimize(); }
34+
public static void enableHotStatsAndDeoptimize() { HOT_STATS.toggleOnAndDeoptimize(); }
6735
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package scala.reflect.internal.util;
2+
3+
import java.lang.invoke.MethodHandle;
4+
5+
final class AlmostFinalValueBenchmarkStatics {
6+
static final boolean STATIC_FINAL_FALSE = false;
7+
8+
private static final AlmostFinalValue ALMOST_FINAL_FALSE = new AlmostFinalValue();
9+
private static final MethodHandle ALMOST_FINAL_FALSE_GETTER = ALMOST_FINAL_FALSE.invoker;
10+
11+
static boolean isTrue() throws Throwable { return (boolean) ALMOST_FINAL_FALSE_GETTER.invokeExact(); }
12+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package scala.reflect.internal.util
2+
3+
import java.util.concurrent.TimeUnit
4+
5+
import org.openjdk.jmh.annotations._
6+
import org.openjdk.jmh.infra.Blackhole
7+
8+
class AlmostFinalValueBenchSettings extends scala.reflect.runtime.Settings {
9+
val flag = new BooleanSetting(false)
10+
11+
@inline final def isTrue2: Boolean = AlmostFinalValueBenchmarkStatics.isTrue && flag
12+
}
13+
14+
object AlmostFinalValueBenchSettings {
15+
implicit class SettingsOps(private val settings: AlmostFinalValueBenchSettings) extends AnyVal {
16+
@inline final def isTrue3: Boolean = AlmostFinalValueBenchmarkStatics.isTrue && settings.flag
17+
}
18+
19+
@inline def isTrue4(settings: AlmostFinalValueBenchSettings): Boolean =
20+
AlmostFinalValueBenchmarkStatics.isTrue && settings.flag
21+
}
22+
23+
@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
24+
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
25+
@Fork(3)
26+
@BenchmarkMode(Array(Mode.AverageTime))
27+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
28+
@State(Scope.Benchmark)
29+
class AlmostFinalValueBenchmark {
30+
import AlmostFinalValueBenchmarkStatics.STATIC_FINAL_FALSE
31+
val settings = new AlmostFinalValueBenchSettings(); import settings._
32+
33+
private def pretendToWorkHard() = Blackhole.consumeCPU(3)
34+
35+
@Benchmark def bench0_unit = ()
36+
@Benchmark def bench0_usingStaticFinalFalse = if (STATIC_FINAL_FALSE && flag) pretendToWorkHard()
37+
@Benchmark def bench0_workingHard = pretendToWorkHard()
38+
39+
@Benchmark def bench1_usingAlmostFinalFalse = if (AlmostFinalValueBenchmarkStatics.isTrue && flag) pretendToWorkHard()
40+
@Benchmark def bench2_usingInlineMethod = if (settings.isTrue2) pretendToWorkHard()
41+
@Benchmark def bench3_usingExtMethod = if (settings.isTrue3) pretendToWorkHard()
42+
@Benchmark def bench4_usingObjectMethod = if (AlmostFinalValueBenchSettings.isTrue4(settings)) pretendToWorkHard()
43+
44+
/*
45+
This benchmark is measuring two things:
46+
1. verifying that using AlmostFinalValue in an if block makes the block a no-op
47+
2. verifying and comparing which ergonomic wrapper around AlmostFinalValue maintains that
48+
49+
The first point is satisfied.
50+
51+
For the second:
52+
1. inline instance methods add a null-check overhead, slowing it down
53+
2. extension methods perform as quickly, are very ergonomic and so are the best choice
54+
3. object methods also perform as quickly, but can be less ergonomic if it requires an import
55+
*/
56+
}

0 commit comments

Comments
 (0)