滅入るんるん

何か書きます

Xamarin.iOSならExpressionよりリフレクションのほうが早いかもという話

元ネタはこちら: うさ☆うさ日記 - [C#]Xamarin.iOSでLambdaExpression.Compile()が通る件


最初に断っておきますが結構雑なベンチマーク計測してます

そこで、本題の件に入る前に技術的背景。

C#でのメタプログラミングにはいろいろと方法があって、この話に関係するものでも3つあります。

  1. リフレクション
    • Javaとかにもある奴で、型名とかメソッド名を取得したり実行できる、取得するのも実行するのもとても遅い
  2. IL Emit
    • 動的にCILを生成することでプログラム実行中に処理を追加できたりする、生成はそれなりに遅いが、一度生成すれば実行するのは高速
  3. 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は難しい…(確信)

追記の追記

実機で調べてもらった

リフレクションのほうが早い可能性あるのは正解っぽい?