まめ - たんたんめん

備忘録 C# / WPF 多め

private なメンバーをC#の黒魔術で高速に取得する

こんばんは。 今日はC#のprivateメンバーを取得してみます。

private なメンバーと言えばReflectionですがいかんせん速度がネックです。

これを.NetCoreから使えるUnsafe.Asを使うとどこまで高速化できるか検証してみました。

今回用意するクラスはこんな感じです。

    public class TopSecret
    {
        private string _field = "体重は80kg";
        private int Property { get; set; } = 80;
    }

このクラスのフィールドをReflectionで取得する場合はこんな感じです。

       var propertyInfo = typeof(TopSecret).GetProperty("Property", BindingFlags.NonPublic|BindingFlags.Instance);
       var value = (int)propertyInfo.GetValue(_topSecret);  // "体重は80kg"

Unsafe.Asは指定した型を別の型にメモリ配置レベルで強制的に変換してしまう恐ろしい黒魔術です。

    public class TopSecretHack
    {
        public string hackedFileld;
        public int HackProperty { get; set; }
        
    }

なのでこのように同じ配置でメンバーをで定義してあげると...

var val = UnSafe.As<TopSecretHack>(_topSecret); // "体重は80kg"

privateだろうがなんだろうが値が取れてしまいます。

それではパフォーマンスを比較してみます。

計測にはBenchMarkDotnetを利用します。

今回はPropertyとFieldのReflectionまた、FieldInfoとPropertyInfoをキャッシュしたものとUnsafeを比較してみます。

テストコード

        [GlobalSetup]
        public void GlobalSetup()
        {
            _cachedFieldInfo = typeof(TopSecret).GetField("_field",BindingFlags.NonPublic | BindingFlags.Instance);
            _cachedPropertyInfo = typeof(TopSecret).GetProperty("Property",BindingFlags.NonPublic|BindingFlags.Instance);
        }
        
        [Benchmark]
        public void ReflectionField()
        {
            var fieldInfo = typeof(TopSecret).GetField("_field",BindingFlags.NonPublic|BindingFlags.Instance);
            var str = (string)fieldInfo.GetValue(_topSecret);
        }
        
        [Benchmark]
        public void ReflectionProperty()
        {
            var propertyInfo = typeof(TopSecret).GetProperty("Property", BindingFlags.NonPublic|BindingFlags.Instance);
            var value = (int)propertyInfo.GetValue(_topSecret);
        }

        [Benchmark]
        public (string,int) UnsafeAs()
        {
            var hack = Unsafe.As<TopSecretHack>(_topSecret);
            return (hack.hackedFileld, hack.HackProperty);
        }
        
        [Benchmark]
        public void CachedReflectionField()
        {
            var str = (string)_cachedFieldInfo.GetValue(_topSecret);
        }
        
        [Benchmark]
        public void CachedReflectionProperty()
        {
            var value = (int)_cachedPropertyInfo.GetValue(_topSecret);
        }

結果 環境はdotnet5 でReleaseビルドデバッガアタッチ無しの結果です。

f:id:at12k313:20201216211248p:plain

string フィールドの場合は40倍差! int プロパティboxingも発生しないのでは80倍の速度差!

キャッシュしていても20倍差と50倍差!

Unsafe圧倒的ですね。

ちなみにBoxingが無くなるようにプロパティもstringにするとこんな感じでした。 f:id:at12k313:20201216212052p:plain