java.lang.StableValueはScalaのlazy valより10万倍くらい速い場合がある

以下の話

https://openjdk.org/jeps/526

https://openjdk.org/jeps/502

xuwei-k.hatenablog.com

JDK 25時点では java.lang.StableValue ですが、26以降で名前も機能もある程度変わるらしいです。

いずれにせよまだpreview APIなので、一般人が使うものではありませんが。

https://openjdk.org/jeps/526

最新の26のEAでもまだLazyConstantは試せなかったので、25でのStableValueでJMHでベンチマークとりました。

すごく大雑把にいうと、目的としてはほぼScalaのlazy valに相当するような特別なclassをJavaに入れよう、という理解でいいと思います。

Java言語自体の文法拡張ではなく、あくまでclassとして入るけれど、

そのclassをJVMが特別扱いすることによって*1、特定のパターンでConstant foldingが有効になってすごくパフォーマンスが良い、

という雰囲気だと思います。

最初の初期化時の速度と、初期化済みの値に繰り返しアクセスした場合の速度と、主に2通りの観点があり得ますが、後者を計測するようなコードを書きました。

先にベンチーマーク結果が以下

[info] Benchmark    Mode  Cnt           Score           Error  Units
[info] Main.test1  thrpt    5  1754279333.496 ± 122744390.832  ops/s
[info] Main.test2  thrpt    5       16656.956 ±       777.404  ops/s
[info] Main.test3  thrpt    5       14790.373 ±      1996.905  ops/s
  • test1の明らかに大きい数字がStableValueで、おそらくConstant foldingが有効になるようなコードを書いたもの
  • test2がScalaのlazy val
  • test3がStableValueを別の使い方したもの

です。つまり

1754279333.496 / 16656.956 = 105318倍速いです。わーい

しかし 「StableValueを別の使い方したもの」に関しては、lazy valと変わらないか、むしろ若干遅いです。 これは、Constant foldingされるようなstaticなfield相当ではないとダメなのかなんなのか・・・詳細な条件がよくわかりませんでした。 そもそも、今の時点で詳細な条件を出したところで、まだpreviewAPIなので、今後変わる可能性もありますし。

以下に、ベンチマークのコードと、もう少し詳細なログを貼っておきます。

$ java --version
openjdk 25.0.1 2025-10-21 LTS
OpenJDK Runtime Environment Corretto-25.0.1.8.1 (build 25.0.1+8-LTS)
OpenJDK 64-Bit Server VM Corretto-25.0.1.8.1 (build 25.0.1+8-LTS, mixed mode, sharing)

sbtのshellで以下を実行

Jmh/run -i 5 -wi 5 -f1 -t1

build.sbt

enablePlugins(JmhPlugin)

scalaVersion := "3.7.3"

project/plugins.sbt

addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8")

src/main/scala/Main.scala

package example

import org.openjdk.jmh.annotations.Benchmark
import java.util.function.Supplier

object A1 {
  private val x1: Supplier[String] = StableValue.supplier(() => "x1")
  private val x2: Supplier[String] = StableValue.supplier(() => "x2")
  private val x3: Supplier[String] = StableValue.supplier(() => "x3")

  def value: String = x1.get + x2.get + x3.get
}

object A2 {
  private lazy val x1: String = "x1"
  private lazy val x2: String = "x2"
  private lazy val x3: String = "x3"

  def value: String = x1 + x2 + x3
}

final class A3 {
  private val x1: Supplier[String] = StableValue.supplier(() => "x1")
  private val x2: Supplier[String] = StableValue.supplier(() => "x2")
  private val x3: Supplier[String] = StableValue.supplier(() => "x3")

  def value: String = x1.get + x2.get + x3.get
}

object A3 {
  val instance = new A3
  def value: String = instance.value
}

class Main {
  @Benchmark
  def test1(): Int = {
    var s: Int = 0
    var i: Int = 0
    while(i < 1000) {
      s += A1.value.length
      i += 1
    }
    s
  }

  @Benchmark
  def test2(): Int = {
    var s: Int = 0
    var i: Int = 0
    while(i < 1000) {
      s += A2.value.length
      i += 1
    }
    s
  }

  @Benchmark
  def test3(): Int = {
    var s: Int = 0
    var i: Int = 0
    while(i < 1000) {
      s += A3.value.length
      i += 1
    }
    s
  }
}
# Warmup Iteration   1: 1743539488.716 ops/s
# Warmup Iteration   2: 1709617887.693 ops/s
# Warmup Iteration   3: 1757739240.491 ops/s
# Warmup Iteration   4: 1775353507.676 ops/s
# Warmup Iteration   5: 1779145061.840 ops/s
Iteration   1: 1698628448.162 ops/s
Iteration   2: 1757745279.793 ops/s
Iteration   3: 1771000427.312 ops/s
Iteration   4: 1776855341.000 ops/s
Iteration   5: 1767167171.212 ops/s
Result "example.Main.test1":
  1754279333.496 ±(99.9%) 122744390.832 ops/s [Average]
  (min, avg, max) = (1698628448.162, 1754279333.496, 1776855341.000), stdev = 31876328.507
  CI (99.9%): [1631534942.664, 1877023724.327] (assumes normal distribution)
# Warmup Iteration   1: 16614.712 ops/s
# Warmup Iteration   2: 16421.645 ops/s
# Warmup Iteration   3: 16053.824 ops/s
# Warmup Iteration   4: 16831.016 ops/s
# Warmup Iteration   5: 16920.232 ops/s
Iteration   1: 16812.376 ops/s
Iteration   2: 16573.129 ops/s
Iteration   3: 16345.819 ops/s
Iteration   4: 16833.110 ops/s
Iteration   5: 16720.344 ops/s
Result "example.Main.test2":
  16656.956 ±(99.9%) 777.404 ops/s [Average]
  (min, avg, max) = (16345.819, 16656.956, 16833.110), stdev = 201.889
  CI (99.9%): [15879.552, 17434.359] (assumes normal distribution)
# Warmup Iteration   1: 15897.091 ops/s
# Warmup Iteration   2: 15017.683 ops/s
# Warmup Iteration   3: 15459.037 ops/s
# Warmup Iteration   4: 14982.158 ops/s
# Warmup Iteration   5: 15466.664 ops/s
Iteration   1: 14569.954 ops/s
Iteration   2: 15686.090 ops/s
Iteration   3: 14593.902 ops/s
Iteration   4: 14361.737 ops/s
Iteration   5: 14740.184 ops/s
Result "example.Main.test3":
  14790.373 ±(99.9%) 1996.905 ops/s [Average]
  (min, avg, max) = (14361.737, 14790.373, 15686.090), stdev = 518.590
  CI (99.9%): [12793.469, 16787.278] (assumes normal distribution)

*1:VM側でどの程度特別扱いなのか?の詳細実装は調べてない。jdk internalでしか使えないアノテーションやclass使って工夫してるだけという可能性もある