Scalaの構造的部分型の性能に注意
2015/03/22
今回はScalaの構造的部分型(structual subtyping)の話です。
構造的部分型を使うと、いちいちインタフェースであるtraitやclassを用意しなくて良いため、便利です。ですが、構造的部分型はコンパイルするとリフレクションに置き換えられるため、性能面では若干心配です。
今回は性能を検証してみようと思います。結論から言うと構造的部分型を利用するとメソッド呼び出しにかかるコストが41倍も増えてしまいます。(つまり沢山呼ばれる部分に使うと辛い)
構造的部分型のサンプル
まずはおさらいと例です。構造的部分型はこんな感じですね。
import scala.language.reflectiveCalls object Main { def main(args: Array[String]): Unit = { val obj = new { def findAll():List[Int] = { 1 :: 2 :: Nil } } printAll(obj) } def printAll(obj: {def findAll():List[_]}): Unit = { obj.findAll().foreach(println) } }
このように書くことで、通常インタフェース用のTraitを用意して、継承関係を考慮しつつ関数を定義して・・・ということが不要になります。printAllメソッドにはfindAll():List[_]という関数を持つオブジェクトであればなんでも与えられるようになります。
リフレクションの確認
性能の検証に入る前に、本当にリフレクションが使われているかを確かめてみます。
先ほどのソースをMain.scalaとして保存して、scalacでコンパイルしてみます。
scalac Main.scala
いくつかファイルができました。.classファイルは4つできていますね。
[tsukaby@PC workspace]% ll total 40 -rw-r--r-- 1 tsukaby staff 877 3 19 10:42 Main$$anon$1.class -rw-r--r-- 1 tsukaby staff 1017 3 19 10:42 Main$$anonfun$printAll$1.class -rw-r--r-- 1 tsukaby staff 2235 3 19 10:42 Main$.class -rw-r--r-- 1 tsukaby staff 849 3 19 10:42 Main.class -rw-r--r-- 1 tsukaby staff 304 3 19 10:42 Main.scala
これをjava decompilerのGUI版で読み込んでみます。Main$.classはこのようになっています。
import java.lang.ref.SoftReference; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import scala.Predef.; import scala.Serializable; import scala.collection.immutable.List; import scala.collection.immutable.Nil.; import scala.runtime.AbstractFunction1; import scala.runtime.BoxedUnit; import scala.runtime.BoxesRunTime; import scala.runtime.EmptyMethodCache; import scala.runtime.MethodCache; import scala.runtime.ScalaRunTime.; public final class Main$ { public static final MODULE$; private static Class[] reflParams$Cache1 = new Class[0]; private static volatile SoftReference reflPoly$Cache1 = new SoftReference(new EmptyMethodCache()); static { new (); } public static Method reflMethod$Method1(Class x$1) { MethodCache methodCache1 = (MethodCache)reflPoly$Cache1.get(); if (methodCache1 == null) { methodCache1 = new EmptyMethodCache(); reflPoly$Cache1 = new SoftReference(methodCache1); } Method method1 = methodCache1.find(x$1); if (method1 != null) return method1; method1 = ScalaRunTime..MODULE$.ensureAccessible(x$1.getMethod("findAll", reflParams$Cache1)); reflPoly$Cache1 = new SoftReference(methodCache1.add(x$1, method1)); return method1; } public void main(String[] args) { Object obj = new Object() { public List<Object> findAll() { int i = 1; int j = 2; return Nil..MODULE$.$colon$colon(BoxesRunTime.boxToInteger(j)).$colon$colon(BoxesRunTime.boxToInteger(i)); } }; printAll(obj); } public void printAll(Object obj) { Object qual1 = obj; try { ((List)reflMethod$Method1(qual1.getClass()).invoke(qual1, new Object[0])).foreach(new AbstractFunction1() { public static final long serialVersionUID = 0L; public final void apply(Object x) { Predef..MODULE$.println(x); } }); return; } catch (InvocationTargetException localInvocationTargetException) { throw localInvocationTargetException.getCause(); } } private Main$() { MODULE$ = this; } }
いろいろありますが、重要なのはここで
try { ((List)reflMethod$Method1(qual1.getClass()).invoke(qual1, new Object[0]))
methodオブジェクトを取ってinvokeを呼び出していますね。
確かにリフレクションになっています。
性能検証
いよいよ性能を見ていきたいと思います。
上記の例のままだと検証しづらいので検証用に以下のコードを用意しました。sbt-jmhというプラグインを使って性能測定をします。
package com.example import org.openjdk.jmh.annotations.Benchmark import scala.language.reflectiveCalls trait CalcOperator { def add(a: Int, b: Int): Int = a + b } object Bar extends CalcOperator class Foo { @Benchmark def callerWithoutReflection(): Unit = { notReflectionFunc(Bar, 1, 2) } @Benchmark def callerWithReflection(): Unit = { reflectionFunc(Bar, 1, 2) } def notReflectionFunc(calcOperator: CalcOperator, a: Int, b: Int): Int = { calcOperator.add(a, b) } def reflectionFunc(obj: {def add(a: Int, b: Int): Int}, a: Int, b: Int): Int = { obj.add(a, b) } }
CalcOperatorというtraitを用意して、これを経由して処理を行うnotReflectionFuncと、CalcOperator関係なしに構造的部分型で処理を行うreflectionFuncを用意しました。
これによって片方はリフレクション無しで、片方はリフレクションでaddを呼び出すことになります。
sbt-jmhでベンチマークを行います。
sbt run -i 20 -wi 10 -f1 -t1
結果
いろいろ出力されますが、最後に以下のようにサマリーがでます。
[info] # Run complete. Total time: 00:01:01 [info] [info] Benchmark Mode Cnt Score Error Units [info] Foo.callerWithReflection thrpt 20 51023318.959 ± 940485.334 ops/s [info] Foo.callerWithoutReflection thrpt 20 2058953118.323 ± 69404149.696 ops/s
数値が高い方が速いため、リフレクション無しversionの方が圧倒的に早いことが分かりました。5:205なので、リフレクション無しversionの方が41倍早いです。
まとめ
構造的部分型はリフレクションとして扱われるため、メソッド呼び出しは相当遅くなります。上記の通り41倍という開きがあります。
構造的部分型の利用には注意しましょう。
おまけ
.@s_tsuka あと真逆の発想で “Scalaで、わざと大量のリフレクション呼び出しをしなければならないとき” に structural subtyping 使うと短く(?)書ける、という使い道があります (それも頻繁には使わないけど)
— Kenji Yoshida (@xuwei_k) 2015, 3月 3
なるほど、そんな使い道もあるのか・・・