چرا Reflection کند است؟

شنبه 13 خرداد 1396

برای استفاده از reflection چند راه حل برای بهینه سازی آن وجود دارد تا سرعت آن را بهبود بخشید، در اینجا چند روش دیگر وجود دارد که آن را سریعترمی کند. این سرعت ها با استفاده از دستاورد delegate که به شما اجازه دسترسی property/field/method را بصورت مستقیم می دهد، میسر شده است.بدون همه این ها reflection هر زمانی که استفاده شود overhead دارد.

چرا Reflection کند است؟

چرا Reflection کند است؟

این علم که reflection در دات نت کند است، رایج است اما واقعا چرا؟ این پست دقیقا به این اشاره می کند که چطور بخواهید به reflection نگاه کنید.

اهداف نوع طراحی سیستم CLR

این را همیشه به یاد داشته باشید که اگر واقعا reflection به هر دلیلی کند بود، هیچگاه آن را برای high-performance قرار نمی دادند.

Reflection چگونه کار می کند؟

در اینجا چند اتفاق رخ می دهد. برای تصور این موضوع اجازه دهید به یک کد مدیریت نشده نگاه کنیم.

System.Reflection.RuntimeMethodInfo.Invoke(..) سورس
calling System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..)
System.RuntimeMethodHandle.PerformSecurityCheck(..) سورس
calling System.GC.KeepAlive(..)
System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..) سورس
calling stub for System.RuntimeMethodHandle.InvokeMethod(..)
stub for System.RuntimeMethodHandle.InvokeMethod(..) سورس

حتی اگر منبع کد خیلی از مراجع بالا را نگاه نکنید، متوجه می شوید که حجم زیادی از کد اجرا می شوند.

زمان واکشی متد اطلاعات

قبل از اینکه یک فیلد/خاصیت/متد را فراخوانی کنید توسط reflection، شما باید مدیریت FieldInfo/PropertyInfo/MethodInfo را بگیرید، البته با استفاده از کد زیر:

Type t = typeof(Person);      
FieldInfo m = t.GetField("Name");

همانطور که در بخش قبل به شما نشان دادیم، این کد هزینه ای دارد، به این دلیل که metadata مجبور به واکشی، ترجمه و ... است. زمان اجرا به ما کمک می کند تا یک سیستم cache داخلی از همه فیلدها/خواص/متدها داشته باشیم. این سیستم Cache توسط کلاس RuntimeTypeCache پیاده سازی شده است و یک مثال از استفاده اش، کلاس RuntimeMethodInfo است.

قبل از اینکه کاری انجام دهید با reflection برای بدست آوردن یک FieldInfo، کدش برای شما پیام زیر را چاپ می کند.

Type: ReflectionOverhead.Program
Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo)
m_fieldInfoCache is null, cache has not been initialised yet

اما این فیلد فقط یکبار واکشی می شود سپس پیام زیر را چاپ می کند:

Type: ReflectionOverhead.Program
Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo)
RuntimeTypeCache: System.RuntimeType+RuntimeTypeCache,
m_cacheComplete = True, 4 items in cache
  [0] - Int32 TestField1 - Private
  [1] - System.String TestField2 - Private
  [2] - Int32 <TestProperty1>k__BackingField - Private
  [3] - System.String TestField3 - Private, Static

جایی که ReflectionOverhead.program مانند زیر به نظر می رسد:

class Program
{
    private int TestField1;
    private string TestField2;
    private static string TestField3;

    private int TestProperty1 { get; set; }
}

این به این معنا است که تکرار فیلدهای GetField یا GetFields کم هزینه تر تکرار می شوند درست مانند زمان اجرا که فقط لیست از پیش موجود را فیلتر می کند که اکنون ساخته شده است. همچنین بصورت مشابه GetMethod و GetProperty را اعمال می کند، زمانی که شما برای اولین Cache مقادیر MethodInfo یا PropertyInfo ساخته شده باشد.

آرگومان اعتبارسنجی و خطا

برخلاف اینکه شما یک بار MethodInfo را بدست آوردید، در اینجا هنوز چند کار دیگر برای انجام دادن وجود دارد زمانی که شما آن را فراخوانی می کنید. تصور کنید که شما کدی مانند زیر ساخته اید:

PropertyInfo stringLengthField = 
    typeof(string).GetProperty("Length", 
        BindingFlags.Instance | BindingFlags.Public);
var length = stringLengthField.GetGetMethod().Invoke(new Uri(), new object[0]);

اگر شما این کد را اجرا کنید، خطای استثناء زیر را می دهد:

System.Reflection.TargetException: Object does not match target type.
   at System.Reflection.RuntimeMethodInfo.CheckConsistency(..)
   at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(..)
   at System.Reflection.RuntimeMethodInfo.Invoke(..)
   at System.Reflection.RuntimePropertyInfo.GetValue(..)

این به این دلیل است که PropertyInfo را بدست آورده ایم برای خاصیت Length در کلاس String اما آنرا با یک شئ Uri فراخوانی کردیم که دقیقا مشخص است اکنون مشکل از کجاست!

به علاوه، در اینجا چند اعتبار سنجی برای آرگومان هایی که شما می خواهید عبور دهید در میان یک متدی که می خواهید فراخوانی کنید، وجود دارد. برای اینکه آرگومان ها کار کنند، reflection api پارامتر هایی می گیرد که یک آرایه از اشیاء است، به ازای هر آرگومان. بنابراین شما با استفاده از reflection متد Add(int x, int y) را صدا می زنید و فراخوانی متد با استفاده از صدا زدن methodInfo(.., new [] { 5, 6 }) انجام می شود. در زمان اجرا، بررسی می شود که نیاز به عبور مقادیر و انواع مقادیری که پاس داده می شود، در این مورد مطمئن شوید که در اینجا دو تا وجود دارند و آن ها هردو int هستند.

بررسی امنیت

وظیفه دیگری که انجام می شود این است که چندین بار امنیت بررسی می شود. به عنوان مثال، آن باز می گرداند که شما اجازه ندارید از reflection هرطور که دوست دارید صدا بزنید و از آن استفاده کنید. در اینجا چند متد خطرناک دیگر وجود دارد که فقط می تواند توسط صدا زدن کد .NetFramework مورد اعتماد باشد. به علاوه برای یک black-list، اینجا همچنین امنیت پویا بررسی می کند که بستگی دارد به Code Access Security permission که شما در لحظه فراخوانی آن را بررسی کردید.

چگونه reflection هزینه دارد؟

بنابراین اکنون می دانید که در پشت صحنه reflection چه اتفاقی رخ می دهد و الان زمان خوبی است که شما بدانید آن چطور می تواند هزینه داشته باشد. لطفا به نکات مقایسه reading/writing یک خاصیت توسط reflection توجه کنید. در خواص .NET متد های Get/Set را جفت می کند که کامپایلر برای ما آن را تولید کرده است، با این حال زمانی که خاصیت فقط چند فیلد ساده برای برگشت دارد .NET Jit متدی را برای دلایل بهینه سازی صدا می زند. این به این معنا است که استفاده از Reflection دسترسی می دهد به خاصیت که به شما نمایش می دهد reflection را در یک نور واقعیت مشابه، اما آن واقعا که در بیشتر موارد رایج انتخاب شده بود، مثلا ORMها، JSON، Serialisation/deserialisation libraries و object mapping tools.

در عکس زیر چند نتیجه که در سایت BenchmarkDotNet نمایش داده شده است:

خواندن اطلاعات Get

نوشتن یک خاصیت Set

بنابراین می توان به وضوح مشاهده کرد که کد reflection  معمول (GetViewReflection و SetViaReflection) به شکل قابل توجهی کند تر هستند برای دسترسی مستقیم به خاصیت (GetViaProperty و SetViaProperty) اما اجازه دهید به جزئیات بیشتری وارد شویم.

ابتدا یک کلاس Test می نویسیم:

public class TestClass
{
    public TestClass(String data)
    {
        Data = data;
    }

    private string data;
    private string Data
    {
        get { return data; }
        set { data = value; }
    }
}

و کد زیر را به دنبال آن داریم، که تمامی انتخاب های ما را استفاده می کند

// Setup code, done only once 
TestClass testClass = new TestClass("A String");
Type @class = testClass.GetType();
BindingFlag bindingFlags = BindingFlags.Instance | 
                           BindingFlags.NonPublic | 
                           BindingFlags.Public;

Regular Reflection

[Benchmark]
public string GetViaReflection()
{
    PropertyInfo property = @class.GetProperty("Data", bindingFlags);
    return (string)property.GetValue(testClass, null);
}

انتخاب اول Cache PropertyInfo

بعد از آن یک پیشرفت سرعت به وجود می آید به وسیله مرجع PropertyInfo، سپس واکشی اطلاعات در هر بار. اما ما هنوز کندتر از قبل دسترسی داریم به خاصیت آن هم بصورت مستقیم، که نشان می دهد که در اینجا هزینه فراخوانی وجود دارد.

// Setup code, done only once
PropertyInfo cachedPropertyInfo = @class.GetProperty("Data", bindingFlags);

[Benchmark]
public string GetViaReflection()
{    
    return (string)cachedPropertyInfo.GetValue(testClass, null);
}

انتخاب دوم استفاده از FastMember

در اینجا با استفاده از کتابخانه Fast Member Library که توسط Marc Gravell نوشته شده است، می توانید مشاهده کنید که استفاده از آن بسیار ساده است.

// Setup code, done only once
TypeAccessor accessor = TypeAccessor.Create(@class, allowNonPublicAccessors: true);

[Benchmark]
public string GetViaFastMember()
{
    return (string)accessor[testClass, "Data"];
}

توجه کنید که در این انتخاب مقداری متفاوت است. آن یک TypeAccessor می سازد که اجازه می دهد به همه خواص دسترسی داشته باشید، نه فقط یکی. اما در پایین یک نتیجه می گیریم که اجرا طولانی تری دارد. آن هم به دلیل داخلی بودن است که یک delegate برای خاصیت شما درخواست می کند (در این مورد data)، قبل از اینکه مقدار را واکشی کند. با اینحا این overhead خیلی کوچک است و هنوز FastMember بسیار سریعتر از Reflection است و استفاده از آن بسیار آسان است، بنابراین به شما توصیه می کنیم که حتما به آن نگاه کنید.

این انتخاب این مورد را به دنبال دارد که تبدیل می شود به کد delegate که مستقما فراخوانی شود بدون آنکه overheadای از reflection هر زمان صورت گیرد، از این رو سرعت کار افزایش میابد.

با اینحال می دانید که ساختن delegate هزینه ای دارد. بنابراین، افزایش سرعت به این دلیل است که ما یک کار با ارزش انجام می دهیم (بررسی امنیت و ...) و ذخیره یک strongly typed delegate که ما می توانیم از آن مجددا استفاده کنیم با کمی overhead. شما نیازی نیست که از این دسته تکنیک ها استفاده کنید اگر شما یکبار از reflection استفاده کردید، اما اگر شما فقط یک بار انجام می دهید، performance شما را پایین نمی آورد.

دلیل اینکه خواندن خاصیت در delegate سریع نیست به این دلیل است که .NET JIL یک delegate را صورت inline در متد مانند یک دسترسی خاصیت صدا نمی زند. بنابراین delegate برای ما هزینه ای دارد تا بخواهیم متدی صدا بزنیم.

انتخاب سوم ساخت یک Delegate

در این مورد ما استفاده می کنیم از تابع CreateDelegate برای برگشت PropertyInfo به یک regular delegate:

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
Func<TestClass, string> getDelegate = 
    (Func<TestClass, string>)Delegate.CreateDelegate(
             typeof(Func<TestClass, string>), 
             property.GetGetMethod(nonPublic: true));

[Benchmark]
public string GetViaDelegate()
{
    return getDelegate(testClass);
}

بخش Func<TestClass, string> بخشی در کد بالا است (شما نمی توانید از Func<object, string> استفاده کنید)، اگر شما اینکار را انجام دهید یک استثناء رخ می دهد. در بیشتر موقعیت ها زمانی که شما یک reflection انجام می دهید، شما این حالت ایده آل را ندارید، در عوض شما نمی خواهید که از reflection در موقعیت اول استفاده کنید، بنابراین این یک راه حل کامل نیست.

انتخاب چهارم Compiled Expression Trees

در اینجا یک کد delegate تولید کردیم، اما تفاوت آن در عبور یک شیئ است، بنابراین یک محدودیت از انتخاب چهارم را کم می کند. ما با استفاده از .NET Expression tree api که به شما اجازه می دهد کد پویا تولید کنید، استفاده می کنیم:

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
ParameterExpression = Expression.Parameter(typeof(object), "instance");
UnaryExpression instanceCast = 
    !property.DeclaringType.IsValueType ? 
        Expression.TypeAs(instance, property.DeclaringType) : 
        Expression.Convert(instance, property.DeclaringType);
Func<object, object> GetDelegate = 
    Expression.Lambda<Func<object, object>>(
        Expression.TypeAs(
            Expression.Call(instanceCast, property.GetGetMethod(nonPublic: true)),
            typeof(object)), 
        instance)
    .Compile();

[Benchmark]
public string GetViaCompiledExpressionTrees()
{
    return (string)GetDelegate(testClass);
}

انتخاب پنجم Code-gen پویا با IL Emit

در نهایت به پایین ترین سطح رسیدیم، emiting raw IL:

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
Sigil.Emit getterEmiter = Emit<Func<object, string>>
    .NewDynamicMethod("GetTestClassDataProperty")
    .LoadArgument(0)
    .CastClass(@class)
    .Call(property.GetGetMethod(nonPublic: true))
    .Return();
Func<object, string> getter = getterEmiter.CreateDelegate();

[Benchmark]
public string GetViaILEmit()
{
    return getter(testClass);

با استفاده از Expression tress که در انتخاب چهارم نشان داده شد، انعطاف پذیری زیادی به شما نمی داد مانند emitting IL code بصورت مستقیم، با این حال شما از emitting invalid code جلوگیری می کنید.

ما واقعا استفاده از Sigil library را به شما توصیه می کنیم، آن به شما پیام خطا ها را بهتر می دهد زمانی که جایی را اشتباه کد می زنید.

آموزش سی شارپ

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان
  • C#.net
  • 2k بازدید
  • 2 تشکر

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید