F#を精神分析
F#のコンパイラの吐く中間コード(MSIL)をちょっと調べていました。
有益な情報は少ないかもしれませんが、MSILまで気になる変態さんのために記しておこうと思います。
※この記事はF#2.0(VS2010ProRC)時点での内容になります。
関数宣言のシンタックスシュガー
以下の定義は(当たり前だけど)完全に同じコードになっていました。
完全にシンタックスシュガーであると断言してよいでしょう。*1
let Fun1 = fun x -> fun y -> fun z -> x + y + z let Fun2 = fun x y z -> x + y + z let Fun3 x y z = x + y + z
inline
インラインを指定しなくてもインライン展開される(もしくはコンパイル時に既に計算されてしまっている)ものが想像以上に多くあります。どういう基準でそのような無条件展開が行われるのかはわかりませんが、プリミティブな型、標準のオペレータあたりがあやしいです。
例えば、以下のようなコードはこのように置き換わります。
let fst' (x, _) = x let snd' (_, y) = y let (++++) x y = x + y let Survey_Inline0 x = fst' (10, 20) ++++ snd' (30, 40) |> Console.WriteLine fst' (x, 0) ++++ snd' (0, x) |> Console.WriteLine
public static void Survey_Inline0(int x) { Console.WriteLine(50); Console.WriteLine((int) (x + x)); }
ここではそれ以外の、inline指定が比較的有効なケースを取り上げてみました。
type Foo<'T> = val mutable Value : 'T new (x) = { Value = x } let SwapFoo1<'T> (x : Foo<'T>) (y : Foo<'T>) = let a = y.Value in y.Value <- x.Value ; x.Value <- a // こっちはinlineを指定します let inline SwapFoo2<'T> (x : Foo<'T>) (y : Foo<'T>) = let a = y.Value in y.Value <- x.Value ; x.Value <- a let Survey_Inline6 () = let f1 = new Foo<int>( 10 ) let f2 = new Foo<int>( 20 ) for i = 0 to 10 do SwapFoo1 f1 f2 SwapFoo2 f1 f2 done
public static void Survey_Inline6() { Foo<int> f1 = new Foo<int>(10); Foo<int> f2 = new Foo<int>(20); for (int i = 0; i < 11; i++) { SwapFoo1<int>(f1, f2); int num = f2.Value; f2.Value = f1.Value; f1.Value = num; } }
SwapFoo2がインライン展開されています。
お次。リスト内包表記(seq expr)をインライン展開するとどうなるか。
let MyMap1 f list' = [ for x in list' -> f x ] let inline MyMap2 f list' = [ for x in list' -> f x ] let Survey_Inline1 () = MyMap1 (( + ) 1) [1..10] |> Console.WriteLine let Survey_Inline2 () = MyMap2 (( + ) 1) [1..10] |> Console.WriteLine
[Serializable] internal class Survey_Inline1@20 : FSharpFunc<int, int> { // Methods internal Survey_Inline1@20() {} public override int Invoke(int y) { return (1 + y); } } // 長いので各メソッドの中身は省略 [CompilationMapping(SourceConstructFlags.Closure)] internal sealed class Survey_Inline2@22 : GeneratedSequenceBase<int> { // Fields [DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated, DebuggerNonUserCode] public int current; [DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated, DebuggerNonUserCode] public IEnumerator<int> @enum; [DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated, DebuggerNonUserCode] public IEnumerable<int> list'; [DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated, DebuggerNonUserCode] public int pc; [DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated, DebuggerNonUserCode] public int x; // Methods public Survey_Inline2@22(IEnumerable<int> list', int x, IEnumerator<int> @enum, int pc, int current); public override void Close(); public override int GenerateNext(ref IEnumerable<int> next); public override bool get_CheckClose(); [CompilerGenerated, DebuggerNonUserCode] public override int get_LastGenerated(); [CompilerGenerated, DebuggerNonUserCode] public override IEnumerator<int> GetFreshEnumerator(); } public static void Survey_Inline1() { Console.WriteLine(MyMap1<int, int>(new Survey_Inline1@20(), (IEnumerable<int>) SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(1, 1, 10))))); } public static void Survey_Inline2() { IEnumerable<int> enumerable = SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(1, 1, 10))); Console.WriteLine(SeqModule.ToList<int>(new Survey_Inline2@22(enumerable, 0, null, 0, 0))); }
// MyMap1 のコードの一部 if (this.@enum.MoveNext()) { this.x = this.@enum.Current; this.pc = 3; this.current = this.f.Invoke(this.x); return 1; }
// MyMap2 のコードの一部 if (this.@enum.MoveNext()) { this.x = this.@enum.Current; this.pc = 3; this.current = 1 + this.x; return 1; }
一応実行速度も計ってみました。
GCのせいなのか速度に毎回バラつきがあったので結果を載せるのはやめましたが、MyMap1に比べてMyMap2(inline)の方が若干速いようです。リスト内包表記を使ってリストを返す関数などを書いた場合は、inline指定すると少し幸せかもしれません。
ちなみにList.map関数が最も速く、末尾再帰でおれおれMap関数を書いて試してみましたが、これはリスト内包より低速でした。*2
imutable
let Survey_Var2 (x : int * int) = let rec loop1 = function | 0 -> () | n -> let a = fst x Console.WriteLine a loop1 (n - 1) let rec loop2 = function | 0 -> () | n -> let mutable a = fst x Console.WriteLine a loop2 (n - 1) loop1 3 ; loop2 3
public static void Survey_Var2(int x_0, int x_1) { int num = x_0; int num2 = x_0; loop1@102(x_0, 3); loop2@108(x_0, 3); } internal static void loop1@102(int x_0, int _arg1) { while (true) { switch (_arg1) { case 0: return; } Console.WriteLine(x_0); _arg1--; x_0 = x_0; } } internal static void loop2@108(int x_0, int _arg2) { while (true) { switch (_arg2) { case 0: return; } int a = x_0; Console.WriteLine(a); _arg2--; x_0 = x_0; } }
末尾再帰関数は最適化されていると共に、
imutableな方はループ内で変数宣言が現れています。
余談ですが、Computation Expressions(再帰版)は末尾再帰最適化されないようです。これはCPS変換のシンタックスシュガーとも見れるので、最適化の対象になるのではないかと考えていましたが違いました。
こんなところです。
他にも思いついたら更新するかもしれません。