元ネタはこちら: うさ☆うさ日記 - [C#]Xamarin.iOSでLambdaExpression.Compile()が通る件
最初に断っておきますが結構雑なベンチマーク計測してます
C#でのメタプログラミングにはいろいろと方法があって、この話に関係するものでも3つあります。
- リフレクション
- Javaとかにもある奴で、型名とかメソッド名を取得したり実行できる、取得するのも実行するのもとても遅い
- IL Emit
- 動的にCILを生成することでプログラム実行中に処理を追加できたりする、生成はそれなりに遅いが、一度生成すれば実行するのは高速
- Expression
- IL EmitはCILをC#コードから生成することになるので普通の人にはできない、それを式木という形でC#っぽいコードでできるようにしたもの、これも一度生成すれば実行するのは高速
※正確な表現にはなってないかもしれませんが、だいたいこんな感じです。
そこで、問題のXamarin.iOSですが、iOSでは動的コード生成が許されてません(それぐらいいいじゃないかと思うかもしれませんが林檎の方針なので従うしかないです)。
動的コード生成が許されないということは、動的にコードを生成してるIL Emitはできないということです。 これはMSのドキュメント: Xamarin.iOS の制限事項にも書かれていることです。
ということで、IL Emitをしているパフォーマンス重視のライブラリーとかは軒並みXamarin.iOSでは利用できないことになりますが、同じような仕組みのExpressionを使っているライブラリーはなぜか動きます。
はて、なぜ動くのだろう?と考えると式木でリフレクションっぽいことをするようにしてるんだろうなぁということは想像つきますね。
問題のベンチマーク
元ネタでは丁寧にベンチマークをとられていましたが、めんどくさいのでインスタンス生成だけ軽くどんなものか計測してみることにします。
public async void BenchmarkAsync()
{
string time = await timerAsync();
// Xamarin.FormsのLabel
Text.Text = time;
}
private async Task<string> timerAsync()
{
return await Task.Run(() =>
{
var sb = new StringBuilder();
var sw = new Stopwatch();
ConstructorInfo constructorInfo = typeof(Data).GetConstructors().First();
sw.Start();
for (int i = 0; i < 1000; i++) CreateInstanceReflection<Data>(constructorInfo);
sw.Stop();
sb.AppendLine($"createReflection: {sw.Elapsed.Ticks}");
sw.Restart();
for (int i = 0; i < 1000; i++) CreateInstanceExpression<Data>(constructorInfo);
sw.Stop();
sb.AppendLine($"createExpression: {sw.Elapsed.Ticks}");
Func<Data> createInstanceDelegateReflection = CreateInstanceReflection<Data>(constructorInfo);
Func<Data> createInstanceDelegateExpression = CreateInstanceExpression<Data>(constructorInfo);
sw.Restart();
for (int i = 0; i < 1000; i++) createInstanceDelegateReflection();
sw.Stop();
sb.AppendLine($"callReflection: {sw.Elapsed.Ticks}");
sw.Restart();
for (int i = 0; i < 1000; i++) createInstanceDelegateExpression();
sw.Stop();
sb.AppendLine($"callExpression: {sw.Elapsed.Ticks}");
return sb.ToString();
});
}
public static Func<TTarget> CreateInstanceReflection<TTarget>(ConstructorInfo constructorInfo)
{
return () => (TTarget)Activator.CreateInstance(constructorInfo.DeclaringType);
}
public static Func<TTarget> CreateInstanceExpression<TTarget>(ConstructorInfo constructorInfo)
{
return Expression.Lambda<Func<TTarget>>(Expression.New(constructorInfo)).Compile();
}
計測コードはこんな感じにすごいてきとーです。なので正確性は全くないです。計測時もDebugモードでしたし、iPhoneシミュレーター使ったりAndroidエミュレーター使ったりしましたし。iPhoneシミュレーターに関してはホストマシンがWindowsなのでMacbook Proにシミュレートしてもらいました。
なので、(当たり前ですが)OS間の速度比は出ませんしReleaseビルドでどうなるかもわかりません。ただ、どれぐらいのリフレクションとExpressionの速度比なんだろうなぁということの参考になる程度です。
そして、結果ですが(左からiPhoneシミュレーター, UWP, Androidエミュレーター)、見事にiOSだけExpressionで生成したデリゲートの実行が遅いです。というか数値的にはリフレクションより遅い?
すでにあるメソッドを呼び出すだけなら大差ないかもしれませんが、動的コード生成みたいな感じでExpressionでデリゲートを組み立てたりすると仕組み上とても遅くなるかもしれません(未確認)。
作成中のライブラリーではExpressionの利用は控えてFodyとかでAOPに逃げようかなと思います……(確実に逃げれるとは言っていない)
追記
これ、iOS側のベンチマークはアカンくて、前に言った通りiOS Simulator はJITコンパイルで動くので動的コード生成できます(IL EmitはわからないけどC# Scriptは動くはず)
— k.yamamoto🐈 (@kymmt24) August 12, 2018
またReleaseビルドになると余計なコードが削除されるのでAOTの場合特に実行速度が顕著に変わる傾向があります
なので実機(文字数)
実際のところは実機確認しろということですね、iOSは難しい…(確信)
追記の追記
実機で調べてもらった
数字が暴れるので標準偏差とかもろもろ出さないとなんとも言えないけど式木はiOSだと単なるReflectionになってそうですねー
— k.yamamoto🐈 (@kymmt24) August 13, 2018
(Release用証明書持ってないのでDebugでoptimizeをtrueにしただけなのでもう少し変わりそうだけど) pic.twitter.com/2DvPM7Qcvg
リフレクションのほうが早い可能性あるのは正解っぽい?