- 会社のelmプロジェクトのコンパイルが遅いのでelm-compilerのプロファイル取って調べた
- プロファイルの結果、 .elmiファイルの型情報のパース時間がほとんどを占めているとわかった
- 細かいことはよくわからんけどレコードを剥いだら倍速になったよ! やったね!
type aliasの中身を何もかも展開している
例えばこのコードのvarの型を考える。
type alias Three a = {p: a, q: a, r: a}
var: Three Int
コンパイラ内部ではこう。elmiでのバイナリエンコードも多少圧縮してあるが意味としては同じ
(TAlias
(Canonical {_package = Name {_author = author, _project = project}, _module = Main})
Three
[(a,TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])]
(Filled (TRecord (fromList [(p,FieldType 0 (TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])),(q,FieldType 0 (TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])),(r,FieldType 0 (TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int []))]) Nothing))
)
シンタックスを目に優しくするとだいたいこう。エイリアス名、エイリアスの型引数、展開後の型が全部ベタ書きされている
TAlias (author.project.Main.Three)
[(a, elm.core.Basics.Int[])]
(Record
[(p, elm.core.Basics.Int[])
,(q, elm.core.Basics.Int[])
,(r, elm.core.Basics.Int[])
])
Intでなく巨大なレコードが渡ると、elmのコード上では Three Record
と書いているだけで内部では型が4回展開される。
これが何重にも重なると大変よろしくないことに
TAlias (author.project.Main.Three)
[(a, {VERY_BIG_RECORD})]
(Record
[(p, {VERY_BIG_RECORD})
,(q, {VERY_BIG_RECORD})
,(r, {VERY_BIG_RECORD})
])
HogeとHugaの2つのtype aliasを噛ませた場合
type alias Hoge a = a
type alias Huga a = Hoge a
hoge : Huga Int
hoge = 1
(hoge,
Forall (fromList [])
(TAlias
(Canonical {_package = Name {_author = author, _project = project}, _module = Main})
Huga
[(a, TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])]
(Filled
(TAlias
(Canonical {_package = Name {_author = author, _project = project}, _module = Main})
Hoge [(a,TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])]
(Filled (TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int []))
))))
Hugaだけにした場合
type alias Huga a = a
hoge : Huga Int
hoge = 1
上と比べるとFilled以下がシンプルになっている。単純な型引数を一度使うtype aliasを噛ませるだけでその中身が倍に増える
(hoge,
Forall (fromList [])
(TAlias
(Canonical {_package = Name {_author = author, _project = project}, _module = Main})
Huga
[(a,TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])]
(Filled (TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int []))
))
https://discourse.elm-lang.org/t/some-advice-on-elm-compile-performance/604 より。
この記事ではOpaque Typeにするといいと書いてあるが、実は通常のCustom Typeでも同様の効果がある。
上の例と同様のHoge,Hugaで二重のtype aliasをLibモジュールで定義した場合
module Lib exposing(Hoge)
type alias Hoge = Huga Int
type alias Huga a = a
module Main exposing (..)
import Lib exposing(..)
hoge : Hoge
hoge = 1
(hoge,
Forall (fromList []) (TAlias
(Canonical {_package = Name {_author = author, _project = project}, _module = Lib})
Hoge [] (Filled
(TAlias (Canonical {_package = Name {_author = author, _project = project}, _module = Lib})
Huga [(a,TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])]
(Filled
(TType (Canonical {_package = Name {_author = elm, _project = core}, _module = Basics}) Int [])
)))))
Lib.HogeをOpaque typeにした場合
module Lib exposing(Hoge, getVal, makeHoge)
getVal : Hoge -> Int
getVal (Hoge huga) = huga
makeHoge : Int -> Hoge
makeHoge = Hoge
type Hoge = Hoge (Huga Int)
type alias Huga a = a
module Main exposing (..)
import Lib exposing(..)
hoge : Hoge
hoge = makeHoge 1
Lib.Hogeという名前の型だという情報しか展開されなくなる!(型引数がある場合は一度だけ展開される)
(hoge,
Forall (fromList [])
(TType (Canonical {_package = Name {_author = author, _project = project}, _module = Lib}) Hoge []))
SPAの各ページで共有されるSharedデータ型がelmi上で500kbになるところまで太っていてこれが主な原因であるということまで突き止めたため、これをCustom Type化した。
Success! Compiled 515 modules.
AdminApp ───> /tmp/adminapp.js
51,393,437,512 bytes allocated in the heap
10,860,466,032 bytes copied during GC
4,068,292,856 bytes maximum residency (8 sample(s))
11,589,384 bytes maximum slop
3879 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 17 colls, 0 par 28.301s 33.395s 1.9644s 8.0567s
Gen 1 8 colls, 0 par 17.776s 21.548s 2.6935s 10.8063s
TASKS: 60 (1 bound, 59 peak workers (59 total), using -N16)
SPARKS: 0(0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.012s ( 0.027s elapsed)
MUT time 52.212s ( 9.845s elapsed)
GC time 46.077s ( 54.943s elapsed)
EXIT time 0.001s ( 0.007s elapsed)
Total time 98.302s ( 64.821s elapsed)
Alloc rate 984,315,939 bytes per MUT second
Productivity 53.1% of total user, 15.2% of total elapsed
Success! Compiled 519 modules.
AdminApp ───> /tmp/adminapp.js
8,834,810,432 bytes allocated in the heap
30,342,376 bytes copied during GC
149,273,496 bytes maximum residency (3 sample(s))
1,741,928 bytes maximum slop
142 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 3 colls, 0 par 0.741s 0.793s 0.2645s 0.3971s
Gen 1 3 colls, 0 par 0.315s 0.389s 0.1297s 0.3328s
TASKS: 60 (1 bound, 59 peak workers (59 total), using -N16)
SPARKS: 0(0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.012s ( 0.028s elapsed)
MUT time 8.826s ( 3.310s elapsed)
GC time 1.056s ( 1.183s elapsed)
EXIT time 0.001s ( 0.009s elapsed)
Total time 9.895s ( 4.529s elapsed)
Alloc rate 1,000,987,575 bytes per MUT second
Productivity 89.2% of total user, 73.1% of total elapsed
ごついレコードをCustom Typeに包むとコンパイル時間が改善します
https://github.com/tsukimizake/elm-compiler
elm-compilerを小改造してelmiを人間が読める形でdumpする機能をつけたもの
cabal v2-exec elm -- --dump-elmi ~/elm-test-proj/elm-stuff/0.19.1/Main.elmi
のように使用するとモジュールの型情報を吐く