Scalaのspecializedアノテーションを使いこなすための基礎知識
こんにちは、アドテクエンジニアーのトデス子です。ふだんスカラを使っているのでスカラの話をします。
ScalaはJavaと同様、型パラメータを使用したコードは内部的にObject型を通して使用されます。 そのため、Int
やDouble
といったプリミティブ型を指定した場合は boxing/unboxingのオーバーヘッドが発生します。
このオーバヘッドは多くの場合大した問題になりませんが、数値計算などの特定領域においては パフォーマンスのボトルネックになるケースがあります。
Scalaにおいては、@specialized
アノテーションを使用することでこのオーバヘッドを軽減する機構があります。 この記事では、この機構の詳細と使用時の注意点などについて紹介します。
はじめに
@specialized
アノテーションについては公式なドキュメントがかなり乏しく、また将来的に挙動が変更される可能性があります。 この記事ではScala 2.11.7 時点での動作について解説します(とは言っても、バイナリ互換性の関係から2.11系のうちは有効、 2.12系でも変更されたという噂はきかないのでおそらく有効な知識だと思われます)。
クラスファイルのデコンパイルにはJADを使用しました。
@specializedアノテーション基礎
Scalaの仕様において、@specialied
は次のように解説されています:
@specialized When applied to the definition of a type parameter, this annotation causes the compiler to generate specialized definitions for primitive types. An optional list of primitive types may be given, in which case specialization takes into account only those types.
(コード例略)
Whenever the static type of an expression matches a specialized variant of a definition, the compiler will instead use the specialized version. See the specialization sid for more details of the implementation.
意訳すると、型パラメータに
@specialized
アノテーションを適用することで、コンパイラはプリミティブ型に特化した定義を生成するようになります。アノテーションにプリミティブ型のリストを渡すことで、特殊化を特定の型に限定することができます。式の静的な型が特殊化された定義と一致する場合、コンパイラは特殊化バージョンを使用します。詳細はSID #9を見てね
といったところでしょうか。
SID #9はScala2.8時代に書かれたspecializeに関する仕様のドラフトですが、基本的な仕様は今と変わっていないようです。 上記の記述に付け加えることとして、特殊化されたクラスの実装についてやメソッドが特殊化される条件 (引数や返り値の型において、specializedされた型パラメータが単独/Array型として出現)について触れられています。
特殊化されたクラスの構造
1 2 3 |
|
単純な例から見ていきましょう(以下、JavaコードはコンパイルされたクラスファイルをJADにより逆コンパイルしたものです)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
非特殊化バージョンはシンプルですね。型パラメータが消去されてObject
型になっています。続いて特殊化バージョンについて:
1 2 3 4 5 6 7 8 9 10 |
|
specializedアノテーションに引数が指定されない場合、 プリミティブ型に対する特殊化が行われます。
Byte
, Short
, Int
, Long
, Char
, Float
, Double
, Boolean
, Unit
の各プリミティブに対して特殊化されたクラスが生成されているのがわかります。 クラス名のB
, C
などのアルファベットは、Javaバイトコードのfield descriptorにおける命名規則に準拠しているようです。
まずは特殊化されていないSpecialized
クラス(ややこしい)を見てみましょう:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Getterメソッドであるx
について、対応するプリミティブの数だけ特殊化バージョンが生成されています。 Object
として保持されている値を、対応するプリミティブ型としてunboxして返しています。
続いて、Int
型に特化したバージョンを見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
- 非特殊化バージョンの
Specialied
クラスを継承している Int
型に対応するメンバx$mcI$sp
が追加されている- 元のメンバ
Object x
は未使用(null
が代入される) - 非特殊化バージョンのメソッド
Object x()
は、同名のint x()
を通じて特殊化バージョンのメソッドx$mcI$sp()
に処理を委譲+boxingしている- 余談ですが、このメソッドに
volatile
がついているのは フラグの値を混同した表示ミスで、 本当はBridgeメソッドのようです。
- 余談ですが、このメソッドに
ということが読み取れます。
メソッドが特殊化される条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
引数や返り値に使用するケースは当然のように特殊化されるので略。paramSeq
は当然のように特殊化されません。
興味深いのは、paramFunction
のようにA
そのものではないがFunction1[A, A]
のように特殊化可能な型を引数にとった場合。
1 2 3 4 5 6 7 8 9 10 |
|
f.apply
の呼び出しにもきちんと特殊化バージョンが使われています。
返り値にTuple2を返すretTuple2
、返り値内部でメソッドを呼んで結果を返すretAndInternal
も、特殊化されたメソッドが呼び出されています。
1 2 3 4 5 6 7 8 9 10 |
|
一方残念なのは内部的に特殊化可能メソッドを呼んでいるinternal()
で、これは特殊化されませんでした。
1 2 3 4 5 |
|
特殊化されたクラス/メソッドが使用される条件
コンパイル時に型パラメータの値が確定するケースじゃないとだめです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
クラスを特殊化したうえでメソッドも特殊化する
こういったケースはどうなるか:
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
- クラスもメソッドも特殊化されていないバージョン:
bar
- クラスの型パラメータのみ特殊化されたバージョン:
bar$mcID$sp
- クラスとメソッドの型パラメータが特殊化されたバージョンが二つ
foo$mCZc$sp
foo$mCZcID$sp
メソッドの命名は、{base_name}$m{method_types}c{class_types}$sp
形式になっていると推測されます。よく出てくる$mcXX
っていうのはこういう意味だったのか!
クラス・メソッド両方について特殊化されたバージョンが二つあるのは謎です。呼び分けについては、以下のように 「クラスとメソッド両方が特殊化可能な場合のみ特殊化メソッドが使われる」という実装になっているようになってるようです。 foo$mCZcID$sp
以外は無駄なメソッドが生成されているように見えるのですが、意味があるのかどうかは不明です。
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 |
|
複数の型パラメータを特殊化する
1 |
|
注意すべき点として、「一部の型パラメータだけ特殊化されている」というケースには対応していません。オールオアナッシングです。
1 2 |
|
非primitive型の特殊化
@specialized
の引数にはprimitive型のほかにAnyRef
も指定できます。
引数未指定の場合はプリミティブ型のみが使われるので、意図せず非特殊化バージョンが使用されるという罠が!
1 2 3 4 5 6 7 8 |
|
1 2 3 4 |
|
まとめ
@specialized
パラメータを付けることで特定の型に特殊化したクラス/メソッドを生成できる- 特殊化はオールオアナッシング。一部の型パラメータだけ特殊化したバージョンは生成されない。
- アノテーションで
AnyRef
を明示的に指定しないとプリミティブ型のみが特殊化の対象になる - 特殊化されたメソッドは、特殊化されたクラスの上で有効になる
- 特殊化されたバージョンが使用されるのは、特殊化する型パラメータの値が静的に決定可能なとき
- 命名規則は
$m{メソッドの型パラメータ}c{クラスの型パラメータ}$sp
、型名はJVMのfield descriptor準拠 - 疑問を持ったらクラスファイルを逆コンパイルしろ
- ボクシングのコストが問題になることは稀なので、ふつうはこの機能つかわなくていいです(まずはボトルネックを計測せよ。
Integer.valueOf
などが頻繁に出現するようなら検討の余地がある)
命名規則などの詳細は普通知る必要はないのですが、この知識とクラスの自動生成を組み合わせるとさらなる最適化のオポチュニティがあります(次回の予告です)。