CLOVER🍀

That was when it all began.

lazy valを使うと、裏で何が起こるのか?

Scalaの機能のひとつに、lazy valを使った遅延評価があります。毎回計算したくないのでメソッドじゃなくてフィールドにしたいけど、オブジェクトを生成した時にすぐに作成するにはコストが大きいような値を束縛するようなケースに使うわけですが…これの裏舞台って、ちゃんと見たことがありません。

Javaには無い機能ですし、「1回だけ初期化する」とか言っている時点で何かしら同期化とかの処理が入っているのは間違いないと思います。代償が何もないなんてことはないはずなので、ちょっと見てみるとしましょう。
※JDのデコンパイル結果が微妙だったので、文章を修正しました

まずは、こんなコードを用意。
LazyValTest.scala

object LazyValTest {
  def main(args: Array[String]): Unit = {
    val o = new LazyValTest
    println("New Object")
    println(o.value)
  }
}

class LazyValTest {
  lazy val value: Int = {
    println("Initialize")
    5 * n
  }

  val n: Int = 3
}

コンパイルします。

$ fsc LazyValTest.scala

クラスファイルが生成されるので、このうちのクラス定義の方をjadでデコンパイルして見てみます。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   LazyValTest.scala

import scala.Predef$;
import scala.ScalaObject;
import scala.runtime.BoxedUnit;

public class LazyValTest
    implements ScalaObject
{

    public static final void main(String args[])
    {
        LazyValTest$.MODULE$.main(args);
    }

    public int value()
    {
        if((bitmap$0 & 1) == 0)
            synchronized(this)
            {
                if((bitmap$0 & 1) == 0)
                {
                    Predef$.MODULE$.println("Initialize");
                    value = 5 * n();
                    bitmap$0 = bitmap$0 | 1;
                }
                BoxedUnit _tmp = BoxedUnit.UNIT;
            }
        return value;
    }

    public int n()
    {
        return n;
    }

    public LazyValTest()
    {
    }

    private int value;
    private final int n = 3;
    public volatile int bitmap$0;
}

注目すべきは、ここと

    public volatile int bitmap$0;

ここですね。

    public int value()
    {
        if((bitmap$0 & 1) == 0)
            synchronized(this)
            {
                if((bitmap$0 & 1) == 0)
                {
                    Predef$.MODULE$.println("Initialize");
                    value = 5 * n();
                    bitmap$0 = bitmap$0 | 1;
                }
                BoxedUnit _tmp = BoxedUnit.UNIT;
            }
        return value;
    }

「bitmap$0」という謎のフィールドがvolatile修飾詞をつけて、しかもなぜかpublicで宣言されています。lazy valで宣言された定義は、最初にbitmap$0をチェックし、次にsynchronizedでthisをガードした上で、もう1度bitmap$0をチェックします。最初にアクセスした処理のみが計算処理を行ってからbitmap$0に1を立てるので、以降のアクセスは2回目の計算処理は行わないって仕掛けですね。

lazy valで宣言したフィールド(定義としてはメソッド)にアクセスする時には、初回はvolatileなフィールドにアクセスし、さらにthisのロックを取りにいくということ挙動になります。2回目以降は、volatileなフィールへのアクセスが発生します。やっぱ、同期化使いますよねー。まあ、当たり前といえば当たり前ですが。

普通に使う分にはあんまり関係ないと思いますが、こういうコストも気にするプログラムを書くような場合は注意した方がいいんでしょうかね?

ところで、_tmpってなんでしょう…?

※7/15、16追記
JDのデコンパイル結果がちょっと微妙だったので、jadでの結果に差し替えました。JDでデコンパイルした場合とjadのデコンパイルした場合で、bitmap$0の使い方に違いがありました…。これは、JDの方が誤っているんでしょうかねぇ…。

慌てて修正したので、恥の上塗りをしてしまいました…。情けない…。