型推論

型推論の動作

型推論とは入力の型から他の値の型を導出する処理のことです。Julia が採用する型推論のアプローチはブログ記事 Inference Convergence Algorithm in JuliaInference Convergence Algorithm in Julia - Revisited で説明されています1

compiler.jl のデバッグ

Julia セッションを開始してから compiler/*.jl を編集 (例えば print を挿入) したときは、base ディレクトリに移動して include("compiler/compiler.jl") を実行することで現在のセッションの Core.Compiler を新しいバージョンで置き換えることができます。このテクニックを使えば編集ごとに Julia を再ビルドするよりもずっと高速な開発が可能です。

あるいは Revise.jl パッケージのコマンド Revise.track(Core.Compiler) をセッションの最初で実行して、コンパイラに対する変更を監視させることもできます。Revise のドキュメントで説明されるように、こうするとファイルを編集して保存した瞬間にコンパイラに対する変更が反映されます。

型推論のコードを見ていくときに最初に見るべき関数は typeinf_code です。convert(Int, UInt(1)) に対する推論を実行するデモを示します:

# メソッドの取得
atypes = Tuple{Type{Int}, UInt}  # 引数の型
mths = methods(convert, atypes)  # メソッドが唯一であることは確認しなければならない。
m = first(mths)

# typeinf_code を呼ぶために必要な変数を作成する。
params = Core.Compiler.Params(typemax(UInt))  # 引数は世界時を表す。
                                              # typemax(UInt) -> 「最も最近の」
sparams = Core.svec()                         # このメソッドは型パラメータを持たない。
optimize = true                               # 型推論時の最適化を全て実行する。
# types = Tuple{typeof(convert), Type{Int}, UInt}
types = Tuple{typeof(convert), atypes.parameters...}
Core.Compiler.typeinf_code(m, types, sparams, optimize, params)

デバッグ中に MethodInstance が必要になったときは、上のコードに含まれる変数を使って Core.Compiler.specialize_method を呼んでください。CodeInfo オブジェクトは次のようにすると取得できます:

# convert(Int, ::UInt) に対する CodeInfo を取得する:
ci = (@code_typed convert(Int, UInt(1)))[1]

インライン化アルゴリズム (inline_worthy)

インライン化の主な処理は ssa_inlining_pass! で行われます。一方で「この関数がインライン化されないのはなぜ?」という疑問を持っているなら、見るべきは isinlineable とそれが呼び出す inline_worthy です。isinlineable は様々な特殊ケースを持ちます (性能のために重要な +, *, min, max, iterate, cconvert といった関数など2)。主たるインライン化の判定は inline_worthy で行われ、インライン化すべきであればこの関数から true が返ります。

inline_worthy はコストモデルを実装し、コストの “安い” 関数がインライン化されます。正確に言うと、インライン化しないで呼び出しを行うのにかかる時間と比較したときに期待される実行時間が大きすぎないときにインライン化が起こります。このコストモデルはこの上なく単純であり、重要な情報をいくつも無視しています: 例えば全ての for ループは一度だけ実行されるものとして解析され、if ... else ... end のコストは全ての分岐の和となります。さらに、このコストモデルが実際の実行時コストをどの程度正確に予測しているかをテストするための関数スイートが現段階では存在しないことも知っておくに値する情報です。ただしインライン化アルゴリズムを変更したときの実行時間の変化については、BaseBenchmarks.jl が間接的な情報を大量に提供します。

インライン化の判断で使われるコストモデルの基礎となるのがルックアップテーブルです。このテーブルは add_tfunc 関数を使って推定コスト (単位は CPU サイクル) を Julia の組み込み命令関数にそれぞれ割り当てることで構築されます。この推定コストは一般的なアーキテクチャにおける値を元にしています (詳細は Agner Fog による解析を参照してください)。

この低水準なルックアップテーブルは多くの特殊ケースで補強されます。例えば :invoke 式 (入力と出力の型が事前に推論されている関数呼び出し) には固定コスト (現在 20) が割り当てられますが、:call 式 (組み込み命令関数でも組み込み関数でもない、多重ディスパッチを必要とする関数呼び出し) には Params.inline_nonleaf_penalty によって設定される値 (現在 1000) が割り当てられます。なお、この値は多重ディスパッチの生のコストを “真面目に” 推定した値ではなく、多重ディスパッチのコストが非常に高いことを反映させるためのヒューリスティックに過ぎません。

statement_cost という関数が関数内の文をそれぞれ解析し、関数全体のコストが計算されます。この関数を自分で試す方法の概略を次に示します。ここで f は任意の関数であり、ttf の引数の型を表すタプルにしたものです:

# fill(3.5, (2, 3)) に対するコストを計算するデモ
f = fill
tt = Tuple{Float64, Tuple{Int,Int}}
# コンパイラと対話するために必要なオブジェクトを作成する。
params = Core.Compiler.Params(typemax(UInt))
mi = Base.method_instances(f, tt)[1]
ci = code_typed(f, tt)[1][1]
opt = Core.Compiler.OptimizationState(mi, params)
# 全ての文のコストを計算する。
cost(stmt::Expr) = Core.Compiler.statement_cost(stmt, -1, ci, opt.sptypes,
                                                opt.slottypes, opt.params)
cost(stmt) = 0
cst = map(cost, ci.code)

# 出力

31-element Array{Int64,1}:
  0
  0
 20
  4
  1
  1
  1
  0
  0
  0
  
  0
  0
  0
  0
  0
  0
  0
  0
  0

出力は Vector{Int} であり、それぞれの要素が ci.code の各文の推定コストを表します。ci に対してはインライン化処理が行われるので、その結果が推定コストにも影響することに注意してください。


  1. 訳注: 一つ目のリンクが切れていたので WebArchive のリンクに変更した。[return]

  2. 訳注: 実際のコードに合うよう例を変更した。[return]

日本語 Julia 書籍 (Amazon アソシエイト)
1 から始める Julia プログラミング
Julia プログラミングクックブック―言語仕様からデータ分析、機械学習、数値計算まで
スタンフォード ベクトル・行列からはじめる最適化数学