はかせのラボ

私の頭の中を書いていく雑記ブログです

C# 悲しい結果になっていたメソッドプロキシの速度を改善した話

あいさつ

どうも、はかせです。
前回の記事読んでいただけたでしょうか?
hakase0274.hatenablog.com

自動メモ化をいろんな方法で試して
高速化してみるという内容の記事でした。

その中でアスペクト指向
メソッドプロキシを使ったやり方があったんですが、
このやり方だけ異様に速度が出ませんでした。
(むしろやらない方が速いという本末転倒ぶり)

今回はその原因を見つけてパフォーマンスを改善した話です。

前回のコード

まずは前回のダメダメだったコードと結果です。

public class MemoizationProxy<TInstance, TArgs, TResult> : RealProxy where TInstance : new()
{
    private readonly TInstance _decorated;
    private Dictionary<TArgs, TResult> memoDic;

    private MemoizationProxy(TInstance decorated) : base(typeof(TInstance))
    {
        _decorated = decorated;
        memoDic = new Dictionary<TArgs, TResult>();
    }

    public static TInstance Create()
    {
        return (TInstance)new MemoizationProxy<TInstance, TArgs, TResult>(new TInstance()).GetTransparentProxy();
    }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase as MethodInfo;
        if (methodCall.InArgs.Length != 1) throw new System.IO.InvalidDataException("引数が誤ってます");
        var key = (TArgs)methodCall.InArgs[0];
        if (!memoDic.TryGetValue(key, out TResult memoResult))
        {
            memoResult = (TResult)methodInfo.Invoke(_decorated, methodCall.InArgs);
            memoDic.Add(key, memoResult);
        }
        return new ReturnMessage(memoResult, null, 0, methodCall.LogicalCallContext, methodCall);
    }
}

public class TestAOP : MarshalByRefObject
{
    public int Fibonacci(int index)
    {
        if (index < 0) throw new ArgumentException();
        return index < 2 ? index : Fibonacci(index - 1) + Fibonacci(index - 2);
    }
}

f:id:hakase0274:20190920224648p:plain

約5秒ですね。
メモ化しない場合の速度が
約4.5秒だったので0.5秒遅くなっています。

もちろんinvokeとか使ってるので
多少のオーバーヘッドがあるのはわかるのですが、
メモ化してむしろ遅くなるのはさすがにおかしいです。

遅くなった原因

結論から言うとメモ化出来てませんでした(・ω<) てへぺろ


どういうことかというと、
今回メソッドプロキシでメソッドが呼ばれるタイミング、
つまりinvokeをオーバーライドしてそこでメモ化してたんですが、
こいつが最初の一回しか呼ばれてなかったわけです。

そらいくらメモ化の理屈があってても、
デリゲートとかでキャッシュしても速くならんわけですよね。
一回しか呼ばれないならキャッシュなど無意味ですから。

じゃなんで呼ばれてなかったのかって話になりますよね。
そもそもこのRealProxyのinvokeが呼ばれるタイミングっていつかって話です。

私は今まで仕組みを作ったクラスの
メソッドを呼んだタイミングだとばっかり思ってました

ただ実際は違くて、
クラスの透過プロキシ経由で呼んだタイミングだったわけです。

ん?どういうことかさっぱりわけわかめ
要は
GetTransparentProxyメソッドで返ってきたobject経由で呼んだタイミング
です。

つまり今回の例で言うと、
MemoizationProxyのCreateメソッドで返ってきた
インスタンス経由で呼んだ場合のみメモ化が機能します。
つまりこの場合のみ
f:id:hakase0274:20190920224246p:plain

再帰関数は自分で自分を呼ぶ関数です。
こいつの場合どういう動きになるかというと
こういう動きになります。
f:id:hakase0274:20190921221804p:plain

自分で自分を呼んじゃうため
メモ化をするAspectの部分を経由してませんね。

なので最初の一回だけメモ化されました。
次回以降の呼び出しはメモ化してないのと同じになります。

つまり前回のコードでは
最初の一回だけ無意味なキャッシュとinvokeのコストを支払い
後はメモ化してないのと同じ動きをしていた

ということになります。

そらやってないときよりか遅くなりますわな。

改善

問題はメモ化用フィルターを経由してないことでした。
つまりフィルターを経由するようにすれば
メモ化が機能し高速化されるはず。

ということでこんな感じ

public class TestAOP : MarshalByRefObject
{
    //メモ化するためにフィルターを引数に受け取る
    public int Fibonacci(int index,TestAOP aop)
    {
        if (index < 0) throw new ArgumentException();
        return index < 2 ? index : aop.Fibonacci(index - 1,aop) + aop.Fibonacci(index - 2,aop);
    }
}

前回と違うのはメモ化用フィルター(呼び出し元)を受け取って
それ経由で再帰しているとこですね。
こうすることで毎回メモ化が行われるので速くなります。

では結果です。
f:id:hakase0274:20190921222435p:plain
約3ミリ秒
ちなみにデリゲートでキャッシュしても結果は変わりませんでした。

やっぱinvokeはコストかかるんですねー

あとがき

今回は前回悲惨な結果に終わったメソッドプロキシ方式を
なんとか頑張って速くしてみました。

ただ色々用意が面倒な割に速度は3ミリと
何とも言えない結果に落ち着きましたね。

これやるならDictionary拡張のが楽だし速い、
さらに上を目指すならIL書くべしって感じですね。

あとやってて思ったのが
デバッグが多少面倒になりますね。

毎回invokeの中にステップインしちゃうんで鬱陶しい。
そしてマクロとかで無効にしたら今度はデバッグしたい
メソッドまで無効化されてしまう(要はそのinvokeの中で呼んでるから)

ただ面白い動きするんで何かのタイミングで使えそうなんですよねー
何かないかな・・・・

それでは今回はこの辺でノシ