Scalaでは`private[this]`を積極的に使うべきなのか

どうもukaznilです。
最近早起きにハマっていると前回の記事でも書いた気がしますが,無事に継続中です。休みの日でも5時に起きてまだ暗いとちょっとテンション上がります。子供心ですね。もう26歳ですけど。

さて,最近は勉強しないとなぁと感じさせられることが非常に多くて焦るばかりなのですが,その対策として「どんなに小さな事でも1日1個は新しい知識を身につける」というノルマを自分に課してみようと思い立ちました。ここでいう「知識」は「経験則」というよりも,「今までなんとなく分かっていたけどいざ説明すると難しい」もしくは「今まで触れてこなかった体系的な知識」のいずれかのつもりです。

で,せっかく早起きなので余裕があれば(たぶん土日限定ですが),それを記事にしてみるのもいいライフハックかなあと。

そんなわけで短めですが記事を書いてみます。

Scalaにおけるprivate[this]

今まで主に(Android)JavaやKotlinに触れてきた私ですが,大学院の研究の過程で同じくJVM言語であるScalaにも1年超触れてきました。
やっぱりpureなJavaだと手が届かない部分にまでリーチできるScalaは,そのパワフルさが売りで機械学習などでもよく使われていますね。
ただその分細かい言語仕様は複雑で,なかなか全容を理解することは難しいです(そもそも私レベルだとJavaですらThread周りはよくわかりません。これは次回以降の宿題)。

で,ちょっと調べ物をしていたら以下のページを見つけました。

この中の項目1・「private[this]をつかえ」に今回はフォーカスを当ててみます。引用しますね。

scalaのvalやvarは、private[this]にしたときのみ、直接のフィールドアクセスになります(それ以外ではメソッド呼び出し)。シングルトンのobjectの場合も同様です。private[this]をつけられる場合はできるだけつけましょう

え。private[this]した方が速いの? これは目から鱗でした。でも正直マユツバなので検証してみましょうか。

検証コード

後述しますが,訳あってScalaのバージョンは2.11.11(JDK 7u80)です。IntelliJ IDEAで検証しました。

package com.ukaznil

object Runner {
  def main(args: Array[String]): Unit = {
    val tester = new Tester()
    tester.test()
  }
}
package com.ukaznil

class Tester {
  private val input1 = 1234567890
  private[this] val input2 = 1234567890

  def test(): Unit = {
    val loopNum = Int.MaxValue
    val ticToc = new TicToc()

    ticToc.tic()
    (0 until loopNum).foreach { _ =>
      val output = input1
    }
    ticToc.toc("access with `private val`")

    ticToc.tic()
    (0 until loopNum).foreach { _ =>
      val output = input2
    }
    ticToc.toc("access with `private[this] val`")
  }
}
package com.ukaznil

class TicToc() {
  private var started: Boolean = false
  private var ticTimeInMillis: Long = 0

  def tic(): Unit = {
    require(!started, {
      "you must call 'toc' in advance."
    })
    started = true
    ticTimeInMillis = System.currentTimeMillis()
  }

  def toc(message: String): Unit = {
    require(started, {
      "you must call 'tic' in advance."
    })
    started = false

    val t = System.currentTimeMillis() - ticTimeInMillis
    println(s"$message has taken $t msec.")
  }
}

やってることは単純で,private valinput1と,private[this] valinput2を大量(=Int.MaxValue回)に呼び出して計測しているだけです。

結果はこんな感じになりました(実行毎に微妙に値が異なります)。

access with `private val` has taken 2597 msec.
access with `private[this] val` has taken 809 msec.

Process finished with exit code 0

結構違いますね!! Java7互換のScala2.11.xだからかもしれませんが,なかなか無視できないくらいの差が出てきました。

クラスファイルはどうなってる?

先ほどの引用にもあったように,本当に

Scalaのvalやvarは、private[this]にしたときのみ、直接のフィールドアクセスになります(それ以外ではメソッド呼び出し)。

になっているのでしょうか。
生成したクラスファイルをデコンパイルしてみます。

まずはみんな大好き jad でデコンパイルします。
jad持っていない人は,以下のサイトからダウンロードするか,mac OS使いであればhomebrewを使ってbrew cask install jadでもインストールできます。

大事なのは上のTester.scalaなので,それだけ掲載します。

// 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:   Tester.scala
package com.ukaznil;

import scala.Predef$;
import scala.Serializable;
import scala.collection.immutable.Range;
import scala.runtime.*;

// Referenced classes of package com.ukaznil:
//            TicToc

public class Tester {
  public int com$ukaznil$Tester$$input1() {
    return com$ukaznil$Tester$$input1;
  }

  public void test() {
    int loopNum = 0x7fffffff;
    TicToc ticToc = new TicToc();
    ticToc.tic();
    RichInt$.MODULE$.until$extension0(Predef$.MODULE$.intWrapper(0), loopNum).foreach$mVc$sp(new Serializable() {
      public final void apply(int x$1) {
        apply$mcVI$sp(x$1);
      }

      public void apply$mcVI$sp(int x$1) {
        int output = $outer.com$ukaznil$Tester$$input1();
      }

      public final volatile Object apply(Object v1) {
        apply(BoxesRunTime.unboxToInt(v1));
        return BoxedUnit.UNIT;
      }

      public static final long serialVersionUID = 0L;
      private final Tester $outer;

      public {
        if (Tester.this == null) {
          throw null;
        } else {
          this.$outer = Tester.this;
          super();
          return;
        }
      }
    });
    ticToc.toc("access with `private val`");
    ticToc.tic();
    RichInt$.MODULE$.until$extension0(Predef$.MODULE$.intWrapper(0), loopNum).foreach$mVc$sp(new Serializable() {
      public final void apply(int x$2) {
        apply$mcVI$sp(x$2);
      }

      public void apply$mcVI$sp(int x$2) {
        int output = $outer.com$ukaznil$Tester$$input2;
      }

      public final volatile Object apply(Object v1) {
        apply(BoxesRunTime.unboxToInt(v1));
        return BoxedUnit.UNIT;
      }

      public static final long serialVersionUID = 0L;
      private final Tester $outer;

      public {
        if (Tester.this == null) {
          throw null;
        } else {
          this.$outer = Tester.this;
          super();
          return;
        }
      }
    });
    ticToc.toc("access with `private[this] val`");
  }

  public Tester() {
  }

  private final int com$ukaznil$Tester$$input1 = 0x499602d2;
  public final int com$ukaznil$Tester$$input2 = 0x499602d2;
}

ハイライトしてある29-31行目と,58-60行目を見ると,それぞれ

int output = $outer.com$ukaznil$Tester$$input1();
int output = $outer.com$ukaznil$Tester$$input2;

になっているのがわかります。16-18行目には対応するinput1()メソッドが生成されていますね。
確かに,private[this]にしたときは直接のフィールドアクセスになっていました。なにこれ目から鱗(2回目)。

でもこれくらいの最適化はコンパイラがやってくれてもいいのなー。

おまけ(Scala2.12.x系の場合)

上の検証ではScala2.11.11を用いたのですが,なんで最新の2.12.xじゃないの? と。
当然最初はそれで試していたのですが,jadがうまく動かなくてですね……。慣れないデコンパイルとかするもんじゃないですね……。

ちなみに,まったく同じコードでScala2.12.3(JDK 8u144)を使うと,結果はこんな感じになります。

access with `private val` has taken 1152 msec.
access with `private[this] val` has taken 771 msec.

Process finished with exit code 0

おー。結構速くなってますね。特にprivate val2597 msec → 1152 msec と頑張ってます。
jadできなかったのでクラスファイル見られていませんが,恐らく同じ,なのかな……?

おわりに

今日は早起きで時間があったので,珍しくコードを交えて検証記事を書いてみました。
検証・記事執筆で1時間ちょっとくらいなので,土日の朝の体操にはうってつけかもしれません。
(といいつつ記事まで続くかどうかは微妙なところ……笑)