डायनामिक वैरिएबल के प्रदर्शन को कैसे प्रभावित करता है?


128

मेरे पास dynamicC # के प्रदर्शन के बारे में एक प्रश्न है। मैंने पढ़ा dynamicहै कि कंपाइलर फिर से चलता है, लेकिन यह क्या करता है?

क्या इसे dynamicएक पैरामीटर के रूप में उपयोग किए जाने वाले चर या गतिशील व्यवहार / संदर्भ के साथ उन पंक्तियों के साथ पूरी विधि को फिर से जोड़ना है?

मैंने देखा है कि dynamicचरों के उपयोग से परिमाण के 2 आदेशों द्वारा लूप के लिए एक सरल को धीमा किया जा सकता है।

कोड मेरे साथ खेला है:

internal class Sum2
{
    public int intSum;
}

internal class Sum
{
    public dynamic DynSum;
    public int intSum;
}

class Program
{
    private const int ITERATIONS = 1000000;

    static void Main(string[] args)
    {
        var stopwatch = new Stopwatch();
        dynamic param = new Object();
        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        Console.ReadKey();
    }

    private static void Sum(Stopwatch stopwatch)
    {
        var sum = 0;
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch, dynamic param)
    {
        var sum = new Sum2();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
    }

    private static void DynamicSum(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.DynSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

नहीं, यह संकलक नहीं चलाता है, जो इसे पहले पास पर धीमी गति से सजा देगा। कुछ हद तक परावर्तन के समान है लेकिन ओवरहेड को कम करने के लिए पहले जो किया गया था उसका ट्रैक रखने के लिए बहुत सारे स्मार्ट के साथ। अधिक जानकारी के लिए Google "डायनेमिक भाषा रनटाइम"। और नहीं, यह कभी भी एक 'देशी' लूप की गति का रुख नहीं करेगा।
हंस पासेंट

यह भी देखें stackoverflow.com/questions/3784317/…
nawfal

जवाबों:


234

मैंने पढ़ा है डायनामिक कंपाइलर को फिर से चलाता है, लेकिन यह क्या करता है। क्या इसे डायनामिक व्यवहार / संदर्भ (?) के साथ एक पैरामीटर के रूप में उपयोग किए जाने वाले डायनामिक या बल्कि उन पंक्तियों के साथ पूरी विधि को फिर से जोड़ना है?

यहाँ सौदा है।

आपके प्रोग्राम के प्रत्येक एक्सप्रेशन के लिए जो डायनेमिक प्रकार का है, कंपाइलर कोड का उत्सर्जन करता है जो एक "डायनेमिक कॉल साइट ऑब्जेक्ट" उत्पन्न करता है जो ऑपरेशन का प्रतिनिधित्व करता है। इसलिए, उदाहरण के लिए, यदि आपके पास:

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();

फिर संकलक कोड उत्पन्न करेगा जो नैतिक रूप से इस तरह का है। (वास्तविक कोड काफी अधिक जटिल है; यह प्रस्तुति उद्देश्यों के लिए सरल है।)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);

देखें कि यह अब तक कैसे काम करता है? हम कॉल साइट को एक बार जनरेट करते हैं , चाहे आप कितनी भी बार एम को कॉल करें। एक बार उत्पन्न करने के बाद कॉल साइट हमेशा के लिए रहती है। कॉल साइट एक ऐसी वस्तु है जो "Foo के लिए एक गतिशील कॉल होने जा रही है" का प्रतिनिधित्व करती है।

ठीक है, तो अब जब आपको कॉल साइट मिल गई है, तो आह्वान कैसे काम करता है?

कॉल साइट डायनेमिक भाषा रनटाइम का हिस्सा है। डीएलआर कहते हैं, "हम्म, कोई इस ऑब्जेक्ट पर एक विधि फू का गतिशील आह्वान करने का प्रयास कर रहा है। क्या मुझे इसके बारे में कुछ भी पता है? नहीं। तब मुझे बेहतर पता चलेगा।"

डीएलआर तब डी 1 में वस्तु से पूछताछ करता है यह देखने के लिए कि क्या यह कुछ विशेष है। हो सकता है कि यह एक विरासत COM ऑब्जेक्ट, या आयरन पायथन ऑब्जेक्ट, या आयरन रूबी ऑब्जेक्ट, या IE डोम ऑब्जेक्ट हो। यदि यह उनमें से कोई नहीं है तो यह एक साधारण सी # वस्तु होनी चाहिए।

यह वह बिंदु है जहां कंपाइलर फिर से शुरू होता है। लेसर या पार्सर की कोई आवश्यकता नहीं है, इसलिए डीएलआर सी # कंपाइलर का एक विशेष संस्करण शुरू करता है जिसमें सिर्फ मेटाडेटा विश्लेषक, अभिव्यक्ति के लिए सिमेंटिक विश्लेषक, और एक एमिटर है जो आईएल के बजाय अभिव्यक्ति पेड़ों का उत्सर्जन करता है।

मेटाडेटा विश्लेषक d1 में वस्तु के प्रकार को निर्धारित करने के लिए परावर्तन का उपयोग करता है, और फिर अर्थमेटिक विश्लेषक के पास यह पूछने के लिए कि क्या होता है जब ऐसी वस्तु विधि फू पर लागू होती है। अधिभार रिज़ॉल्यूशन विश्लेषक आंकड़े जो बाहर निकलते हैं, और फिर एक अभिव्यक्ति ट्री बनाते हैं - जैसे कि आप फू को एक अभिव्यक्ति ट्री लैम्ब्डा में कहते हैं - जो उस कॉल का प्रतिनिधित्व करता है।

C # कंपाइलर फिर उस अभिव्यक्ति ट्री को कैश पॉलिसी के साथ DLR में वापस भेज देता है। नीति आमतौर पर "दूसरी बार जब आप इस प्रकार की वस्तु देखते हैं, तो आप मुझे फिर से वापस बुलाने के बजाय इस अभिव्यक्ति के पेड़ का फिर से उपयोग कर सकते हैं"। डीएलआर तब कंपाइल ट्री पर कम्पाइल को बुलाता है, जो एक्सप्रेशन-ट्री-टू-आईएल कंपाइलर को आमंत्रित करता है और एक प्रतिनिधि में गतिशील रूप से उत्पन्न आईएल के एक ब्लॉक को बाहर निकालता है।

DLR तब कॉल साइट ऑब्जेक्ट से संबद्ध कैश में इस प्रतिनिधि को कैश करता है।

फिर यह प्रतिनिधि को आमंत्रित करता है, और फू कॉल होता है।

दूसरी बार जब आप एम कहते हैं, तो हमारे पास पहले से ही एक कॉल साइट है। DLR वस्तु को फिर से पूछताछ करता है, और यदि वस्तु पिछली बार की तरह ही है, तो यह प्रतिनिधि को कैश से निकालता है और उसे आमंत्रित करता है। यदि वस्तु एक अलग प्रकार की है, तो कैश याद आती है, और पूरी प्रक्रिया फिर से शुरू होती है; हम कॉल का सिमेंटिक विश्लेषण करते हैं और परिणाम को कैश में स्टोर करते हैं।

यह प्रत्येक अभिव्यक्ति के लिए होता है जिसमें गतिशील शामिल होता है । इसलिए उदाहरण के लिए यदि आपके पास:

int x = d1.Foo() + d2;

फिर तीन डायनेमिक कॉल साइट हैं। फू के लिए डायनेमिक कॉल के लिए एक, डायनेमिक जोड़ के लिए एक, और डायनेमिक से इंटिमेट के लिए डायनामिक रूपांतरण के लिए एक। हर एक का अपना रनटाइम विश्लेषण है और विश्लेषण परिणामों का अपना कैश है।

सही बात?


जिज्ञासा से बाहर, बिना पार्सर / लेक्सर के विशेष संकलक संस्करण मानक zsc.exe के लिए एक विशेष ध्वज पास करके आह्वान किया गया है?
रोमन रोटर

@ एरिक, क्या मैं आपको अपनी पिछली ब्लॉग पोस्ट पर इंगित करने के लिए परेशान कर सकता हूँ जहाँ आप संक्षिप्त, अंतर आदि के निहितार्थ के बारे में बात करते हैं? जैसा कि मैंने आपको इसमें बताया है कि Convert.ToXXX के साथ डायनेमिक का उपयोग कैसे / क्यों किया जाता है, जिससे कंपाइलर में आग लग जाती है। मुझे यकीन है कि मैं विवरणों पर चर्चा कर रहा हूं, लेकिन उम्मीद है कि आप जानते हैं कि मैं किस बारे में बात कर रहा हूं।
एडम रैकिस

4
@ रोमन: नहीं। csc.exe C ++ में लिखा गया है, और हमें कुछ ऐसी चीज़ों की आवश्यकता है जिसे हम आसानी से C # कह सकते हैं। इसके अलावा, मेनलाइन कंपाइलर की अपनी प्रकार की वस्तुएं हैं, लेकिन हमें रिफ्लेक्शन प्रकार की वस्तुओं का उपयोग करने में सक्षम होना चाहिए। हमने csc.exe कंपाइलर से C ++ कोड के संबंधित अंश निकाले और उन्हें लाइन-बाय-लाइन C # में अनुवाद किया, और फिर DLR को कॉल करने के लिए उसमें से एक लाइब्रेरी बनाई।
एरिक लिपार्ट

9
@ एरिक, "हमने csc.exe कंपाइलर से C ++ कोड के संबंधित अंश निकाले और उन्हें लाइन-बाय-लाइन C # में अनुवादित किया"। यह तब के बारे में था जब लोगों को लगा कि रोजलिन का पीछा करने लायक हो सकता है :)
ShuggyCoUn

5
@ शुग्गीकोउक: एक संकलक के रूप में सेवा करने का विचार कुछ समय के लिए चारों ओर मार रहा था, लेकिन वास्तव में कोड विश्लेषण करने के लिए रनटाइम सेवा की आवश्यकता उस परियोजना के प्रति एक बड़ी प्रेरणा थी, हाँ।
एरिक लिपिपर्ट

108

अद्यतन: जोड़ा गया precompiled और आलसी-संकलित मानक

अद्यतन 2: बाहर मुड़ता है, मैं गलत हूँ। एक संपूर्ण और सही उत्तर के लिए एरिक लिपर्ट की पोस्ट देखें। मैं बेंचमार्क नंबरों की खातिर इसे यहां छोड़ रहा हूं

* अपडेट 3: इस सवाल के मार्क ग्रेवेल के जवाब के आधार पर, IL-Emitted और Lazy IL-Emitted मानक जोड़े गए ।

मेरी जानकारी के लिए, dynamicकीवर्ड का उपयोग रनटाइम के दौरान और स्वयं में किसी भी अतिरिक्त संकलन का कारण नहीं बनता है (हालांकि मुझे लगता है कि यह विशिष्ट परिस्थितियों में ऐसा कर सकता है, इस पर निर्भर करता है कि किस प्रकार की वस्तुएं आपके गतिशील चर का समर्थन कर रही हैं)।

प्रदर्शन के बारे में, dynamicस्वाभाविक रूप से कुछ उपरि का परिचय होता है, लेकिन लगभग उतना नहीं जितना आप सोच सकते हैं। उदाहरण के लिए, मैंने सिर्फ एक बेंचमार्क चलाया जो इस तरह दिखता है:

void Main()
{
    Foo foo = new Foo();
    var args = new object[0];
    var method = typeof(Foo).GetMethod("DoSomething");
    dynamic dfoo = foo;
    var precompiled = 
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile();
    var lazyCompiled = new Lazy<Action>(() =>
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile(), false);
    var wrapped = Wrap(method);
    var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
    var actions = new[]
    {
        new TimedAction("Direct", () => 
        {
            foo.DoSomething();
        }),
        new TimedAction("Dynamic", () => 
        {
            dfoo.DoSomething();
        }),
        new TimedAction("Reflection", () => 
        {
            method.Invoke(foo, args);
        }),
        new TimedAction("Precompiled", () => 
        {
            precompiled();
        }),
        new TimedAction("LazyCompiled", () => 
        {
            lazyCompiled.Value();
        }),
        new TimedAction("ILEmitted", () => 
        {
            wrapped(foo, null);
        }),
        new TimedAction("LazyILEmitted", () => 
        {
            lazyWrapped.Value(foo, null);
        }),
    };
    TimeActions(1000000, actions);
}

class Foo{
    public void DoSomething(){}
}

static Func<object, object[], object> Wrap(MethodInfo method)
{
    var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
        typeof(object), typeof(object[])
    }, method.DeclaringType, true);
    var il = dm.GetILGenerator();

    if (!method.IsStatic)
    {
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
    }
    var parameters = method.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldelem_Ref);
        il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
    }
    il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
        OpCodes.Call : OpCodes.Callvirt, method, null);
    if (method.ReturnType == null || method.ReturnType == typeof(void))
    {
        il.Emit(OpCodes.Ldnull);
    }
    else if (method.ReturnType.IsValueType)
    {
        il.Emit(OpCodes.Box, method.ReturnType);
    }
    il.Emit(OpCodes.Ret);
    return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}

जैसा कि आप कोड से देख सकते हैं, मैं एक सरल नो-ऑप विधि को सात अलग-अलग तरीकों से लागू करने का प्रयास करता हूं:

  1. डायरेक्ट मेथड कॉल
  2. का उपयोग करते हुए dynamic
  3. प्रतिबिंब द्वारा
  4. Actionउस का उपयोग करना जो रनटाइम के दौरान पूर्वनिर्धारित हो गया (इस प्रकार परिणामों से संकलन समय को छोड़कर)।
  5. एक का उपयोग करना Action(इस प्रकार संकलन समय सहित) है कि पहली बार यह आवश्यक है संकलित हो जाता है, एक गैर धागा सुरक्षित लेज़ी चर का उपयोग कर
  6. एक गतिशील रूप से उत्पन्न विधि का उपयोग करना जो परीक्षण से पहले बनाया जाता है।
  7. एक गतिशील रूप से उत्पन्न विधि का उपयोग करना जो परीक्षण के दौरान आलसी हो जाता है।

प्रत्येक को एक साधारण लूप में 1 मिलियन बार कहा जाता है। यहाँ समय के परिणाम हैं:

प्रत्यक्ष: 3.4248ms
गतिशील: 45.0728ms
चिंतन: 888.4011ms
Precompiled: 21.9166ms
LazyCompiled: 30.2045ms
ILEmitted: 8.4918ms
LazyILEmitted: 14.3483ms

इसलिए जब dynamicकीवर्ड का उपयोग सीधे तरीके से कॉल करने की तुलना में अधिक समय तक होता है, तब भी यह ऑपरेशन को लगभग 50 मिलीसेकंड में एक लाख बार पूरा करने का प्रबंधन करता है, जिससे यह प्रतिबिंब की तुलना में बहुत तेज हो जाता है। यदि हम जिस विधि को कॉल करते हैं, वह कुछ गहन करने की कोशिश कर रही है, जैसे कुछ तारों को एक साथ जोड़ना या मूल्य के लिए एक संग्रह की खोज करना, उन ऑपरेशनों को प्रत्यक्ष कॉल और कॉल के बीच के अंतर को दूर करना होगा dynamic

प्रदर्शन dynamicअनावश्यक रूप से उपयोग नहीं करने के कई अच्छे कारणों में से एक है , लेकिन जब आप वास्तव में dynamicडेटा के साथ काम कर रहे होते हैं , तो यह उन लाभों को प्रदान कर सकता है जो नुकसान को दूर करते हैं।

अद्यतन ४

जॉनबॉट की टिप्पणी के आधार पर, मैंने परावर्तन क्षेत्र को चार अलग-अलग परीक्षणों में तोड़ दिया:

    new TimedAction("Reflection, find method", () => 
    {
        typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
    }),
    new TimedAction("Reflection, predetermined method", () => 
    {
        method.Invoke(foo, args);
    }),
    new TimedAction("Reflection, create a delegate", () => 
    {
        ((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
    }),
    new TimedAction("Reflection, cached delegate", () => 
    {
        methodDelegate.Invoke();
    }),

... और यहाँ बेंचमार्क परिणाम हैं:

यहाँ छवि विवरण दर्ज करें

इसलिए यदि आप एक विशिष्ट विधि को पूर्व निर्धारित कर सकते हैं, जिसे आपको कॉल करने की आवश्यकता होगी, तो उस विधि का संदर्भ देते हुए कैश्ड प्रतिनिधि को आमंत्रित करना उतना ही तेज़ होगा जितना कि विधि को कॉल करना। हालाँकि, अगर आपको यह निर्धारित करने की आवश्यकता है कि किस विधि को कॉल करना है जैसा कि आप इसे लागू करने वाले हैं, तो इसके लिए एक प्रतिनिधि बनाना बहुत महंगा है।


2
इस तरह की एक विस्तृत प्रतिक्रिया, धन्यवाद! मैं वास्तविक संख्या के बारे में भी सोच रहा था।
सेर्गेई सिरोटकिन

4
खैर, डायनामिक कोड मेटाडेटा आयातक, सिमेंटिक एनालाइज़र और कंपाइलर के एक्सप्रेशन ट्री को शुरू करता है, और फिर उसी के आउटपुट पर एक एक्सप्रेशन-ट्री-टू-आईआईएल कंपाइलर चलाता है, इसलिए मुझे लगता है कि यह कहना उचित है कि यह शुरू होता है। संकलक अप रनटाइम पर। सिर्फ इसलिए कि यह लेसर नहीं चलाता है और पार्सर शायद ही प्रासंगिक लगता है।
एरिक लिपर्ट

6
आपके प्रदर्शन की संख्या निश्चित रूप से दिखाती है कि डीएलआर की आक्रामक कैशिंग नीति कैसे भुगतान करती है। यदि आपके उदाहरण में नासमझ चीजें होती हैं, जैसे कि यदि आपके पास कॉल करने पर हर बार एक अलग प्रकार का प्राप्त होता है, तो आप देखेंगे कि डायनामिक संस्करण बहुत धीमा है, जब वह पहले से संकलित विश्लेषण परिणामों के अपने कैश का लाभ नहीं ले सकता है । लेकिन जब वह इसका फायदा उठा सकता है, तो पवित्रता अच्छा है।
एरिक लिपर्ट

1
एरिक के सुझाव के अनुसार कुछ नासमझ। किस लाइन पर टिप्पणी की गई है, स्वैप करके टेस्ट करें। 8964ms बनाम 814ms, dynamicनिश्चित रूप से हारने के साथ:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
ब्रायन

1
प्रतिबिंब और विधि की जानकारी से एक प्रतिनिधि बनाने के लिए निष्पक्ष रहें:var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);
जॉनबोट
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.