TODESKING
技術ブログ

Java8 Nashornのパフォーマンス特性

|

コンパイラ型言語において、動的に生成した処理を実行したいという状況がある。例えば、

Javaには以前からJavaScriptエンジン(Rhino)が同梱されていたが、Java 8からはNashornと呼ばれる高速化されたエンジンになった。従来よりも圧倒的に速いらしいので、動的コード生成が必要な場所で使えるかどうか、パフォーマンス特性について調べてみた。

結論としては、

  • 関数の実行速度は超高速。Scalaでナイーブに実装したインタプリタより速い。
  • かわりに関数定義が遅い(すごく)
  • 動的に生成したい処理が小数で、評価回数がすごく多い場合はインタプリタ作るよりもJSに変換してNashornで実行したほうが高速。
  • 関数定義/実行比が大きい場合は、オーバヘッドがあるので自分でインタプリタ書いたほうがいい。

以下詳細。ベンチマークコードはこちら

関数の実行速度はすごい

適当にでっち上げたLisp風言語インタプリタもどきと比較して、同内容の処理が1.5 〜 2倍速い。

1
2
3
4
Native(N=12000000): 38[ms]
Neive(N=12000000): 4245[ms]
Oracle Nashorn(+)(N=12000000): 1709[ms]
Oracle Nashorn(function)(N=12000000): 2673[ms]

NeiveがScala、下の二つがJS。律儀に関数呼び出ししても1.5倍、+に展開すると2倍速い。

NativeはふつうにScalaの関数オブジェクトで書いたやつ。静的に定義可能な処理は静的に定義したほうがいいのがわかる。

関数定義は遅い

1
2
Oracle Nashorn define function(cached)(N=1000): 35[ms]
Oracle Nashorn define function(uncached)(N=1000): 1280[ms]

(cached)は同一の関数定義を1000回繰り返したケース。(uncached)は一回ごとに関数定義内の定数を変えている。 関数定義があまりにも遅いのでキャッシュが実装されているらしいことが伺える。

適用例

Scalaによる遺伝的プログラミング実装に、ツリーの内容をJSに変換する最適化処理を実装してみたんですが、このユースケース(関数1k回定義→600k回実行くらい。実行速度はナイーブ実装で1ms以下)だと10倍くらい遅くなるので使えななかったです(´・_・`)

JDBCドライバを動的にロードする

|

古典的な話題だけど意外と日本語リソースなかった。

クラスパス外にあるJDBCドライバを使いたいというケース。

クラスパスにある場合は、以下のようなコードでドライバを使用できる。

1
2
3
4
5
6
// クラス名を指定して明示的にドライバクラスを初期化する。
// JDBC4対応のドライバは、ServiceLoaderの機構を使用して自動でロードされるため不要。
Class.forName("com.example.MySuperDBDriver")

// 登録されたドライバの中から自動で適合するものが使用される
DriverManager.getConnection("jdbc:mysuperdb:....")

クラスパス外のドライバを使いたい場合、追加で

  • 外部のドライバクラスをロードするためのClassLoaderを作成
  • DriverManagerはシステムクラスローダを使ったDriverしか使ってくれないので、ラップする

という処理を行う必要がある。

Scalaで書くとこうなった

主要部分はこんなかんじ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 外部jarを読むためのクラスローダ作成
val systemClassLoader = getClass.getClassLoader
val driverClassLoader = java.net.URLClassLoader.newInstance(config.driverJars.map(_.toURI.toURL).toArray, systemClassLoader)

// 必要に応じてドライバをラップしてDriverManagerに登録する処理
def register(driver:Driver) = DriverManager.registerDriver(DriverProxy.wrapIfNeeded(driver, systemClassLoader))


// JDBC4非対応のドライバは、クラス名を元に手動でロードする
val unmanagedDrivers = config.uninitializedDriverClasses.map { klass =>
  Class.forName(klass, true, driverClassLoader).newInstance.asInstanceOf[Driver]
}

// DriverManagerに登録済みのドライバをクリア
// 必須ではないが、ドライバ一覧を表示したいときに重複が発生するのを避けるため。
deregisterAllDrivers()

unmanagedDrivers.foreach { driver => register(driver) }

// JDBC4に対応したドライバの場合、ServiceLoaderを使用してDriverのインスタンスを取得できる
val serviceLoader = java.util.ServiceLoader.load(classOf[java.sql.Driver], driverClassLoader)
serviceLoader.iterator.asScala.foreach { driver => register(driver) }

ドライバがシステムクラスローダ以外を使っている場合は、DriverManagerを騙すためにラップするためのクラス。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DriverProxy(val original:java.sql.Driver) extends java.sql.Driver {
  def acceptsURL(x$1: String): Boolean = original.acceptsURL(x$1)
  def connect(x$1: String,x$2: java.util.Properties): java.sql.Connection = original.connect(x$1, x$2)
  def getMajorVersion(): Int = original.getMinorVersion()
  def getMinorVersion(): Int = original.getMinorVersion()
  def getParentLogger(): java.util.logging.Logger = original.getParentLogger()
  def getPropertyInfo(x$1: String,x$2: java.util.Properties): Array[java.sql.DriverPropertyInfo] = original.getPropertyInfo(x$1, x$2)
  def jdbcCompliant(): Boolean = original.jdbcCompliant()
}

object DriverProxy {
  import java.sql.Driver

  def wrapIfNeeded(driver:Driver, classloader:ClassLoader):Driver = driver match {
    case d if d.getClass != (try { Class.forName(d.getClass.getName, true, classloader) } catch { case e:ClassNotFoundException => null }) =>
      new DriverProxy(d)
    case d => d
  }
  def unwrap(driver:Driver):Driver = driver match {
    case d:DriverProxy => d.original
    case d => d
  }
}

参照

Scalaで作ったコンソールアプリケーションをConscriptで配布する

|

Conscriptの概要はREADME日本語訳などを参照

ざっくり説明すると、ConscriptというのはGitHub上のコンソールアプリプロジェクトを自動でビルド+インストールしてくれるツール。 何かツールを配布したいときは、自分のプロジェクトにConscriptの設定ファイルを含めておけばcsコマンド一発でインストール可能になって便利というやつです。

Giter8というプロジェクトテンプレート管理システム用にConscriptプロジェクトのテンプレートが提供されてるので、新規プロジェクトの時はこれ使うと良さそう。

Conscriptは何をしているのか

  • GitHub上のlaunchconfigを元に、sbt-launcherのラッパーコマンドを作成する
  • sbt-launcherは、launchconfigの内容に応じて依存ライブラリの解決とアプリケーションの起動を行う

最初勘違いしてたんだけど、GitHubからソース一式落としてきてビルドしてるわけじゃないです。launchconfig以外はmvnリポジトリ経由で取得しているので、ビルド済みのjarを公開しておかないとインストールできない。

launchconfigの書き方などが知りたいときはsbt-launcherのドキュメント読むと書いてあります。

既存プロジェクトに導入する場合

launchconfig

まずsrc/main/conscript/(実行ファイル名)/launchconfig に設定を書く

1
2
3
4
5
6
7
8
9
10
11
12
[app]
  version: 1.0.0
  org: com.todesking
  name: example
  class: com.todesking.example.Main
[scala]
  version: 2.11.4
[repositories]
  local
  scala-tools-releases
  maven-central
  todesking: http://todesking.github.io/mvn/

インストール時は[repositories]の定義を元に依存ライブラリ(アプリ本体含む)を探すので、必要な物を書いておく。 [app] class:には起動用のクラスを指定する。xsbti.AppMainを継承している必要がある。

sbtと定義内容がかぶってるので、自動生成するようにしてみた。versionは「現在mvnリポジトリから入手可能なバージョン」である必要があるので微妙なことをしている……。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// build.sbt
compile <<= (compile in Compile) dependsOn Def.task {
    val content = s"""[app]
      |  version: ${version.value.replaceAll("\\+$", "")}
      |  org: ${organization.value}
      |  name: ${name.value}
      |  class: com.todesking.nyandoc.Main
      |[scala]
      |  version: ${scalaVersion.value}
      |[repositories]
      |  local
      |  scala-tools-releases
      |  maven-central
      |  todesking: http://todesking.github.io/mvn/""".stripMargin
    val dir = (sourceDirectory in Compile).value / "conscript" / "nyandoc"
    dir.mkdirs()
    val launchconfig = dir / "launchconfig"
    IO.write(launchconfig, content)
  }

AppMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.todesking.example

case class Exit(val code: Int) extends xsbti.Exit
class Main extends xsbti.AppMain {
  def run(config: xsbti.AppConfiguration) = {
    Exit(Main.run(config.arguments))
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    System.exit(run(args))
  }

  def run(args: Array[String]): Int = {
    // ここに実際の処理を書く

    0 // exit code
  }
}

sbt plugin

Conscript sbt plugin を指定することで、依存関係とか設定してくれるっぽい。

1
2
// project/plugins.sbt
addSbtPlugin("net.databinder" % "conscript-plugin" % "0.3.5")
1
2
// build.sbt
seq(conscriptSettings :_*)

publish設定

自前のリポジトリでjarをホスティングするためのpublish設定の例。 この設定でsbt publishすると./repo/以下に必要なファイル一式が出力されるので、適当なサーバに公開するとよい。

1
2
// build.sbt
publishTo := Some(Resolver.file("com.todesking", file("./repo/"))(Patterns(true, Resolver.mavenStyleBasePattern)))

ScalaプロジェクトをCircleCIでビルド+テストして、scoverageで計測したカバレッジをCOVERALLSに送るための諸設定をした

|

タイトルがわかりやすい。

環境

  • Scala 2.11.4
  • sbt 0.13.6

テストフレームワークはScalaTest使ってます

sbtでカバレッジを測定する+COVERALLSに送信する

Scalaのカバレッジ測定、scctというのが一般的だったけど、なにか色々あって現在はScoverageという名前になって開発が継続されています。

Scoverageの最新バージョンはScala2.10をサポートしていないので注意しましょう(旧バージョン指定したら動くかも。試してないけど。)

導入は以下の通り。

1
2
3
4
5
6
7
8
9
// project/plugins.sbt

resolvers += Classpaths.sbtPluginReleases

// sbtでScoverage使えるようにするプラグイン
addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "0.99.11")

// Scoverageの結果をCOVERALLSに送信するプラグイン
addSbtPlugin("org.scoverage" %% "sbt-coveralls" % "0.99.0")
1
2
3
4
5
6
7
8
9
// build.sbt

// scoverage
instrumentSettings

org.scoverage.coveralls.CoverallsPlugin.coverallsSettings

// この設定をしないと、結果レポートのhtmlのハイライトがおかしくなる
ScoverageKeys.highlighting := true

これでカバレッジ測定用のsbtタスクが使えるようになります。

  • sbt scoverage:compile
    • カバレッジ計測のためのデータつきでコンパイルする
  • sbt scoverage:test
    • カバレッジ計測してレポートを生成
  • sbt coveralls
    • 生成されたレポートをCOVERALLSに送る
    • 環境変数 COVERALLS_REPO_TOKEN が設定されてる必要あり。

人間が見られる形式のレポートは、target/scala-2.11/scoverage-report/index.html にある。

CircleCIでビルド+テスト+カバレッジ測定する

circle.yml の書き方は公式ドキュメント参照。

sbtのバージョンは、project/build.properties で指定しておけばそれを使ってくれるようです。

1
2
# project/build.properties
sbt.version=0.13.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# circle.yml

# scoverageのレポートを保存するよう設定
general:
  artifacts:
    - "target/*/scoverage-report"

# テスト準備コマンドを上書きしてScoverageに対応させる
dependencies:
  override:
    - "sbt scoverage:compile"

test:
  override:
    - "sbt scoverage:test"
  post:
    - "sbt coveralls" # テスト終了後COVERALLSに結果送信

COVERALLSのトークンを入れる環境変数 COVERALLS_REPO_TOKEN は、リポジトリに含めたくないのでCircleCIのプロジェクト設定画面から追加する。

あと、CircleCIはjunit形式のテスト結果xmlを認識してくれるようなのでその設定もする。

1
2
3
4
// build.sbt

// ScalaTest: Generate junit-style xml report
testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-u", {val dir = System.getenv("CI_REPORTS"); if(dir == null) "target/reports" else dir} )

xmlは環境変数 CI_REPORTS から探されるので、ScalaTestのオプションとしてxmlの出力先を適切に指定してやる。

結果

CircleCI。 ビルド結果の”Artifacts”からカバレッジレポートのhtmlが見られる。

COVERALLS。結果が取れていてよかったですね。

以上、ご査収の程お願いしたく。

Surround.vimからvim-operator-surroundに移行した

|

プリセットされた設定を使うぶんには問題なかったんだけど、textobj-userで定義したテキストオブジェクトを surroundでも使おうとしたら不可能なことが発覚(削除処理決め打ちでカスタマイズの余地がない)。 どうしたものかと思って調査してたらvim-operator-surroundが良さそうだったので乗り換えた。

基本設定

1
2
3
4
5
6
7
8
" surround.vimはアンインストールしておきましょう
NeoBundle 'kana/vim-operator-user'
NeoBundle 'rhysd/vim-operator-surround'

" 公式サンプルだとsa/sd/srだがsurround.vimに合わせた
nmap ys <Plug>(operator-surround-append)
nmap ds <Plug>(operator-surround-delete)
nmap cs <Plug>(operator-surround-replace)

基本機能使うならこれだけでよし。注意点としては、surround.vimとはキーストロークが変わる。

  • surround.vim: cs{surround text objectを表す一文字}
  • operator-surround: cs{テキストオブジェクトを選択する任意のキーストローク(a”, i’等)}

具体的には、surround.vimにおいて囲みの種類を変更するキーストロークcs'(は、operator-surroundではcsa'(になります。 削除も同様。 囲みを追加するysaw"等については、変更なしでそのまま通る。

textobjの追加

新しいsurround text objを定義したいときは g:operator#surround#blocks を設定します。

1
2
3
4
5
" デフォルト値を使いつつユーザ定義を追加する。もっとマシな方法ある気がするけど動くのでまあよし。
" この例は関数呼び出しっぽいパターンを定義してる。
let g:operator#surround#blocks = deepcopy(g:operator#surround#default_blocks)
call add(g:operator#surround#blocks['-'],
\     {'block': ['\<\[a-zA-z0-9_?!]\+\[(\[]', '\[)\]]'], 'motionwise': 'char', 'keys': ['c']} )

blockに囲み開始と終了のパターン、motionwiseは選択モード(文字、行、ブロック)、keysは囲みを追加するときのキー。 囲みの追加は、パターンが正規表現だとうまく動きません。

正規表現を使用する場合、\V前提なので注意。

surround.vimと比較してカスタマイズは大幅に楽になったので、乗り換える価値はあった。

Scala、unapplyまとめ

|

match式のパターンとしてunapply/unapplySeqが定義されたオブジェクトを指定することで、動作をカスタマイズできる。

参考資料は例によってScala Language Specification Version 2.9(pdf)。ちょっと古いけどこのへんのルールは今も変わってないと思われる。Chapter 8あたり。

引数 メソッド
() unapply(a:A):Boolean
(p1) unapply(a:A):Option[T1]
(p1, ..., pn) unapply(a:A):Option[(T1, ..., Tn)]
(p1, ..., _*) unapplySeq(a:A):Seq[T]

Scala for内包表記(for comprehension)変換ルールメモ

|

Scalaのforは構文糖衣なんだけど、書き方に応じてmapflatMapforEachに変換されてよくわからないので、ルールをまとめた。

参考資料: Scala Language Specification Version 2.9(pdf)。ちょっと古いけどこのへんのルールは今も変わってないと思われる。

ステップ1

出現する全てのnot irrefutableなパターン部(※)を持つジェネレータp <- eを以下の形に変形する

p <- e.withFilter { case p => true; case _ => false }

※not irrefutable: 必ずマッチするとは限らないようなパターンの意。

ステップ2

すべてのfor内包表記が消滅するまで、以下のルールを繰り返し適用する

ジェネレータの変換に関するルール

パターン: p <- e; if g

p <- e.withFilter((x1, ..., xn) => g )、ただしx1, …, xn はpの自由変数

パターン: p1 <- e1; p2 = e2

(p1, p2) <- for(x1@p1 <- e1) yield { val x2@p2 = e2; (x1, x2) }

for内包表記の変換に関するルール

パターン: for(p <- e) yield ee

e.map { case p => ee }

パターン: for(p1 <- e1; p2 <- e2 ...) yield ee

e1.flatMap { case p1 => for(p2 <- e2 ...) yield ee }

パターン: for(p <- e) ee

e.foreach { case p => ee }

パターン: for(p1 <- e1; p2 <- e2 ...) ee

e1.foreach { case p1 => for(p2 <- e2 ...) ee }

難しすぎるので5秒でわかるようにしろ

generatorが複数ある場合は外側から順に、以下のルールで展開する:

  • yield?
    • いいえ → foreach
    • はい → generatorの数は1個?
      • はい → map
      • いいえ → flatMap
generator yield? メソッド
1 yes map
>1 yes flatMap
>0 no foreach

ScalaMatsuriでFrom Ruby to Scalaという発表をしてきました

|

RubyはScalaに比べてクソとかいう話ではなく、Scalaから見たRubyの興味深い点のご紹介みたいな趣旨の発表です。発表資料が見にくくて申し訳ございません。

なぜかトリに発表することになった結果、Scalaのイベントで延々とRubyの話をしてる異常者みたいな印象が強化されたけど、私はただ、言語の多様性について皆さんに考えていただこうと……

RubyとScalaの印象についてですが、動的でアジリティとハック性に優れるRubyと、静的で記述性が高くアカデミック要素の強いScalaという感じですね。エコシステムの変化が激しくて、関連フレームワークのアップデートを怠るとすぐ死ぬのはどちらにも共通した特性(そういうのがつらい人はジャバエイトをやるとよい)。 私はJava出身といってもmavenエコシステムに関する知識に乏しかったので、mavenの上に載ってるsbtの依存性管理には悩まされました。

あと広告システムに関する話は全然しなかったけど、遅いことをやると遅いのでふつうに速いコード書こうくらいしか言うことはないですね。プロダクトを高速に保つというのは、高度な知識よりも絶え間ない実践に関する問題であることが多いので、とにかく各位やっていけ(以上です)

あとScalaの人は事あるごとにモナドモナド言ってて、いっぽうRubyの人は技術よりもエモい話のほうが好きそうだみたいな偏見があるという話をした。

1_as_identity_functionという超便利gemを作ったけどRuby 2.2により有難味が薄れる模様

|

Ruby2.2でObject#itselfというメソッドが導入されるとのこと

group_by等のメソッドで「その要素自身」を返すブロックを渡したいことはたまにあるので、{|x| x} のかわりに&:itselfって書けばいいのは便利ですね。

という記事を読んで、以前同じ動機でgemを作ったことを思い出した。

1_as_identity_function

名前そのままなんだけど、なんと!! &1{|x| x} 相当です。便利。

1
2
3
group_by {|x| x}
group_by(&:itself)
group_by(&1)

itselfなげえ……

ちなみになぜ&1かというと、 圏論ではidentityを表現するのに1を使う風習があって かっこよかったからです