c# मेरे कोड को तेज करने की कोशिश करें?




.net clr (4)

मैंने कोशिश-पकड़ के प्रभाव का परीक्षण करने के लिए कुछ कोड लिखा, लेकिन कुछ आश्चर्यजनक परिणाम देख रहे थे।

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

    long start = 0, stop = 0, elapsed = 0;
    double avg = 0.0;

    long temp = Fibo(1);

    for (int i = 1; i < 100000000; i++)
    {
        start = Stopwatch.GetTimestamp();
        temp = Fibo(100);
        stop = Stopwatch.GetTimestamp();

        elapsed = stop - start;
        avg = avg + ((double)elapsed - avg) / i;
    }

    Console.WriteLine("Elapsed: " + avg);
    Console.ReadKey();
}

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    for (int i = 1; i < n; i++)
    {
        n1 = n2;
        n2 = fibo;
        fibo = n1 + n2;
    }

    return fibo;
}

मेरे कंप्यूटर पर, यह लगातार 0.96 के आसपास एक मूल्य प्रिंट करता है ..

जब मैं Fibo () के अंदर लूप के लिए लपेटता हूं तो इस तरह एक कोशिश-पकड़ ब्लॉक के साथ:

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    try
    {
        for (int i = 1; i < n; i++)
        {
            n1 = n2;
            n2 = fibo;
            fibo = n1 + n2;
        }
    }
    catch {}

    return fibo;
}

अब यह लगातार 0.6 9 प्रिंट करता है ... - यह वास्तव में तेजी से चलता है! पर क्यों?

नोट: मैंने इसे रिलीज कॉन्फ़िगरेशन का उपयोग करके संकलित किया और सीधे EXE फ़ाइल (विजुअल स्टूडियो के बाहर) चलाया।

संपादित करें: जॉन स्कीट के उत्कृष्ट विश्लेषण से पता चलता है कि किसी भी तरह से एक्स 86 सीएलआर इस विशिष्ट मामले में सीपीयू रजिस्टरों का अधिक अनुकूल तरीके से उपयोग करने का कारण बनता है (और मुझे लगता है कि हम अभी तक समझने के लिए क्यों नहीं हैं)। मैंने जॉन की यह पुष्टि की कि x64 सीएलआर में यह अंतर नहीं है, और यह x86 सीएलआर से तेज़ था। मैंने long प्रकार के बजाय एफआईबी विधि के अंदर int प्रकारों का उपयोग करके परीक्षण किया, और फिर x86 सीएलआर x64 सीएलआर जितना तेज़ था।

अद्यतन: ऐसा लगता है कि इस मुद्दे को रोज़लिन द्वारा तय किया गया है। वही मशीन, एक ही सीएलआर संस्करण - वीएस 2013 के साथ संकलित होने पर यह मुद्दा उपर्युक्त है, लेकिन वीएस 2015 के साथ संकलित होने पर समस्या दूर हो जाती है।

https://code.i-harness.com


खैर, जिस तरह से आप समय की चीजें कर रहे हैं वह मेरे लिए बहुत बुरा लग रहा है। पूरे लूप के समय यह अधिक समझदार होगा:

var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
    Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);

इस तरह आप छोटे समय की दया पर नहीं हैं, अस्थायी बिंदु अंकगणित और संचित त्रुटि।

उस परिवर्तन को करने के बाद, देखें कि "कैच" संस्करण की तुलना में "गैर-पकड़" संस्करण अभी भी धीमा है या नहीं।

संपादित करें: ठीक है, मैंने इसे स्वयं करने की कोशिश की है - और मैं वही परिणाम देख रहा हूं। बहुत अजीब। मुझे आश्चर्य हुआ कि कोशिश / पकड़ कुछ खराब इनलाइनिंग को अक्षम कर रहा था, लेकिन [MethodImpl(MethodImplOptions.NoInlining)] का उपयोग करके इसके बजाय ...

असल में आपको कॉर्डबग के तहत अनुकूलित जेआईटीटेड कोड को देखने की आवश्यकता होगी, मुझे संदेह है ...

संपादित करें: जानकारी के कुछ और बिट्स:

  • केवल n++; आस-पास प्रयास / पकड़ n++; रेखा अभी भी प्रदर्शन में सुधार करती है, लेकिन इसे पूरे ब्लॉक के चारों ओर नहीं डालती है
  • यदि आप एक विशिष्ट अपवाद (मेरे परीक्षण में ArgumentException ) पकड़ते हैं तो यह अभी भी तेज़ है
  • यदि आप कैच ब्लॉक में अपवाद मुद्रित करते हैं तो यह अभी भी तेज़ है
  • यदि आप कैच ब्लॉक में अपवाद को फिर से हटा देते हैं तो यह फिर से धीमा हो जाता है
  • यदि आप कैच ब्लॉक के बजाय अंत में ब्लॉक का उपयोग करते हैं तो यह फिर से धीमा हो जाता है
  • यदि आप आखिरकार ब्लॉक और कैच ब्लॉक का उपयोग करते हैं, तो यह तेज़ है

अजीब...

संपादित करें: ठीक है, हमारे पास disassembly है ...

यह सी # 2 कंपाइलर और .NET 2 (32-बिट) सीएलआर का उपयोग कर रहा है, एमडीबीजी के साथ अलग हो रहा है (क्योंकि मेरे पास मेरी मशीन पर कॉर्डबग नहीं है)। मैं अभी भी डीबगर के तहत भी एक ही प्रदर्शन प्रभाव देखता हूं। तेज़ संस्करण वैरिएबल घोषणाओं और रिटर्न कथन के बीच सब कुछ के आसपास एक try ब्लॉक का उपयोग करता है, केवल एक catch{} हैंडलर के साथ। जाहिर है धीमा संस्करण एक ही कोशिश / पकड़ के बिना ही है। कॉलिंग कोड (यानी मुख्य) दोनों मामलों में समान है, और समान असेंबली प्रतिनिधित्व है (इसलिए यह एक इनलाइनिंग मुद्दा नहीं है)।

तेजी से संस्करण के लिए अलग कोड:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        edi
 [0004] push        esi
 [0005] push        ebx
 [0006] sub         esp,1Ch
 [0009] xor         eax,eax
 [000b] mov         dword ptr [ebp-20h],eax
 [000e] mov         dword ptr [ebp-1Ch],eax
 [0011] mov         dword ptr [ebp-18h],eax
 [0014] mov         dword ptr [ebp-14h],eax
 [0017] xor         eax,eax
 [0019] mov         dword ptr [ebp-18h],eax
*[001c] mov         esi,1
 [0021] xor         edi,edi
 [0023] mov         dword ptr [ebp-28h],1
 [002a] mov         dword ptr [ebp-24h],0
 [0031] inc         ecx
 [0032] mov         ebx,2
 [0037] cmp         ecx,2
 [003a] jle         00000024
 [003c] mov         eax,esi
 [003e] mov         edx,edi
 [0040] mov         esi,dword ptr [ebp-28h]
 [0043] mov         edi,dword ptr [ebp-24h]
 [0046] add         eax,dword ptr [ebp-28h]
 [0049] adc         edx,dword ptr [ebp-24h]
 [004c] mov         dword ptr [ebp-28h],eax
 [004f] mov         dword ptr [ebp-24h],edx
 [0052] inc         ebx
 [0053] cmp         ebx,ecx
 [0055] jl          FFFFFFE7
 [0057] jmp         00000007
 [0059] call        64571ACB
 [005e] mov         eax,dword ptr [ebp-28h]
 [0061] mov         edx,dword ptr [ebp-24h]
 [0064] lea         esp,[ebp-0Ch]
 [0067] pop         ebx
 [0068] pop         esi
 [0069] pop         edi
 [006a] pop         ebp
 [006b] ret

धीमे संस्करण के लिए डिस्सेबल कोड:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        esi
 [0004] sub         esp,18h
*[0007] mov         dword ptr [ebp-14h],1
 [000e] mov         dword ptr [ebp-10h],0
 [0015] mov         dword ptr [ebp-1Ch],1
 [001c] mov         dword ptr [ebp-18h],0
 [0023] inc         ecx
 [0024] mov         esi,2
 [0029] cmp         ecx,2
 [002c] jle         00000031
 [002e] mov         eax,dword ptr [ebp-14h]
 [0031] mov         edx,dword ptr [ebp-10h]
 [0034] mov         dword ptr [ebp-0Ch],eax
 [0037] mov         dword ptr [ebp-8],edx
 [003a] mov         eax,dword ptr [ebp-1Ch]
 [003d] mov         edx,dword ptr [ebp-18h]
 [0040] mov         dword ptr [ebp-14h],eax
 [0043] mov         dword ptr [ebp-10h],edx
 [0046] mov         eax,dword ptr [ebp-0Ch]
 [0049] mov         edx,dword ptr [ebp-8]
 [004c] add         eax,dword ptr [ebp-1Ch]
 [004f] adc         edx,dword ptr [ebp-18h]
 [0052] mov         dword ptr [ebp-1Ch],eax
 [0055] mov         dword ptr [ebp-18h],edx
 [0058] inc         esi
 [0059] cmp         esi,ecx
 [005b] jl          FFFFFFD3
 [005d] mov         eax,dword ptr [ebp-1Ch]
 [0060] mov         edx,dword ptr [ebp-18h]
 [0063] lea         esp,[ebp-4]
 [0066] pop         esi
 [0067] pop         ebp
 [0068] ret

प्रत्येक मामले में * दिखाता है कि डीबगर एक सरल "चरण-इन" में दर्ज किया गया था।

संपादित करें: ठीक है, अब मैंने कोड को देखा है और मुझे लगता है कि मैं देख सकता हूं कि प्रत्येक संस्करण कैसे काम करता है ... और मेरा मानना ​​है कि धीमे संस्करण धीमे हैं क्योंकि यह कम रजिस्टरों और अधिक स्टैक स्पेस का उपयोग करता है। n के छोटे मूल्यों के लिए जो संभवतः तेज़ है - लेकिन जब लूप उस समय का बड़ा हिस्सा लेता है, तो यह धीमा होता है।

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

संपादित करें: बस मेरी x64 मशीन पर यह कोशिश की। X64 सीएलआर इस कोड पर x86 सीएलआर की तुलना में बहुत तेज (लगभग 3-4 गुना तेज) है, और x64 के तहत प्रयास / पकड़ ब्लॉक एक उल्लेखनीय अंतर नहीं बनाता है।


जॉन की असेंबली दिखाती है कि दो संस्करणों के बीच का अंतर यह है कि तेज़ संस्करण स्थानीय चरों में से एक को स्टोर करने के लिए रजिस्टरों ( esi,edi ) की एक जोड़ी का उपयोग करता है जहां धीमी संस्करण नहीं है।

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

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

उदाहरण के लिए, निम्नलिखित दो विधियों पर विचार करें। वे वास्तविक जीवन उदाहरण से अनुकूलित किए गए थे:

interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed { 
    public int[] Array;
    public int this[int index] {
        get { return Array[index]; }
        set { Array[index] = value; }
    }
}

static int Generic<T>(int length, T a, T b) where T : IIndexed {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}

एक दूसरे का एक सामान्य संस्करण है। सामान्य प्रकार को StructArray साथ StructArray विधियों को समान बना देगा। चूंकि StructArray एक मान प्रकार है, इसलिए इसे सामान्य विधि का अपना संकलित संस्करण मिलता है। फिर भी वास्तविक चलने का समय विशेष विधि की तुलना में काफी लंबा है, लेकिन केवल x86 के लिए। X64 के लिए, समय काफी समान हैं। अन्य मामलों में, मैंने x64 के लिए अंतर भी देखा है।


यह खराब हो जाने वाले इनलाइनिंग के मामले की तरह दिखता है। X86 कोर पर, जिटर में ईबीएक्स, एडीएक्स, एएसआई और एडी रजिस्टर स्थानीय चर के सामान्य उद्देश्य भंडारण के लिए उपलब्ध है। Ecx रजिस्टर एक स्थिर विधि में उपलब्ध हो जाता है, इसे इसे स्टोर करने की आवश्यकता नहीं है। गणना के लिए अक्सर ईएक्स रजिस्टर की आवश्यकता होती है। लेकिन ये 32-बिट रजिस्ट्रार हैं, प्रकार के चर के लिए इसे रजिस्टरों की एक जोड़ी का उपयोग करना चाहिए। कौन सी edx हैं: गणना और edi के लिए eax: भंडारण के लिए ebx।

जो धीमी संस्करण के लिए डिस्सेप्लर में खड़ा है, न तो एडी और न ही ईबीएक्स का उपयोग किया जाता है।

जब जिटर को स्थानीय चरों को स्टोर करने के लिए पर्याप्त रजिस्टरों को नहीं मिल पाता है तो उसे स्टैक फ्रेम से लोड और स्टोर करने के लिए कोड उत्पन्न करना होगा। यह कोड धीमा कर देता है, यह "रजिस्टर नामकरण" नामक प्रोसेसर ऑप्टिमाइज़ेशन को रोकता है, एक आंतरिक प्रोसेसर कोर ऑप्टिमाइज़ेशन चाल जो एक रजिस्टर की कई प्रतियों का उपयोग करती है और सुपर-स्केलर निष्पादन की अनुमति देती है। जो एक ही रजिस्टर का उपयोग करते समय भी कई निर्देशों को एक साथ चलाने के लिए अनुमति देता है। पर्याप्त रजिस्ट्रार नहीं होने पर x86 कोर पर एक आम समस्या है, जिसे x64 में संबोधित किया गया है जिसमें 8 अतिरिक्त रजिस्ट्रार हैं (आर 15 के माध्यम से आर 9)।

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

ऐसे कई नियम हैं जो निर्धारित करते हैं कि एक विधि को रेखांकित किया जा सकता है। वे बिल्कुल दस्तावेज नहीं हैं लेकिन ब्लॉग पोस्ट में उल्लेख किया गया है। एक नियम यह है कि यह तब नहीं होगा जब विधि निकाय बहुत बड़ा हो। इससे इनलाइनिंग से लाभ कम हो जाता है, यह बहुत अधिक कोड उत्पन्न करता है जो एल 1 निर्देश कैश में फिट नहीं होता है। यहां लागू होने वाला एक और कठिन नियम यह है कि जब कोई प्रयास / पकड़ विवरण होता है तो एक विधि को रेखांकित नहीं किया जाएगा। उस के पीछे की पृष्ठभूमि अपवादों का कार्यान्वयन विवरण है, वे एसईएच (स्ट्रक्चर अपवाद हैंडलिंग) के लिए विंडोज़ के अंतर्निर्मित समर्थन पर पिग-बैक हैं जो स्टैक-फ्रेम आधारित है।

जिटर में रजिस्टर आवंटन एल्गोरिदम का एक व्यवहार इस कोड के साथ खेलने से अनुमान लगाया जा सकता है। ऐसा लगता है कि जिटर एक विधि को रेखांकित करने का प्रयास कर रहा है। एक नियम यह प्रतीत होता है कि केवल edx: eax register pair को इनलाइन कोड के लिए उपयोग किया जा सकता है जिसमें स्थानीय चर के प्रकार लंबे होते हैं। लेकिन edi नहीं: ebx। इसमें कोई संदेह नहीं है क्योंकि कॉलिंग विधि के लिए कोड पीढ़ी के लिए यह बहुत हानिकारक होगा, दोनों ईडीआई और ईबीएक्स महत्वपूर्ण भंडारण रजिस्टर हैं।

तो आपको तेज़ संस्करण मिलता है क्योंकि जिटर आगे जानता है कि विधि निकाय में कोशिश / पकड़ने वाले बयान शामिल हैं। यह जानता है कि इसे कभी भी रेखांकित नहीं किया जा सकता है ताकि ईडीई का उपयोग आसानी से किया जा सके: लंबे चर के लिए भंडारण के लिए ईबीएक्स। आपको धीमा संस्करण मिला क्योंकि जिटर को यह नहीं पता था कि इनलाइनिंग काम नहीं करेगी। यह विधि निकाय के लिए कोड उत्पन्न करने के बाद ही पता चला।

तब दोष यह है कि यह वापस नहीं गया और विधि के लिए कोड फिर से उत्पन्न नहीं किया। जो समझ में आता है, उस समय की बाधाओं को देखते हुए इसे संचालित करना होता है।

यह धीमा-डाउन x64 पर नहीं होता है क्योंकि एक के लिए इसमें 8 और रजिस्ट्रार होते हैं। दूसरे के लिए क्योंकि यह केवल एक रजिस्टर (रैक्स की तरह) में लंबे समय तक स्टोर कर सकता है। और जब आप लंबे समय के बजाय int का उपयोग करते हैं तो धीमा-डाउन तब नहीं होता है क्योंकि जिटर के पास रजिस्टरों को चुनने में बहुत अधिक लचीलापन होता है।


Roslyn इंजीनियरों में से एक जो स्टैक उपयोग के अनुकूलन को समझने में माहिर हैं, ने इस पर एक नज़र डाली और मुझे बताया कि सी # कंपाइलर स्थानीय चर स्टोर्स जेनरेट करने के तरीके और JIT कंपाइलर के तरीके के तरीके के बीच बातचीत में एक समस्या प्रतीत होता है संबंधित x86 कोड में शेड्यूलिंग। नतीजा स्थानीय लोगों के भार और दुकानों पर उप-कोड कोड कोड है।

किसी कारण से हम सभी को अस्पष्ट, समस्याग्रस्त कोड जनरेशन पथ से बचा जाता है जब जिटर जानता है कि ब्लॉक एक प्रयास-संरक्षित क्षेत्र में है।

यह बहुत अजीब है। हम जिटर टीम के साथ अनुवर्ती करेंगे और देखेंगे कि हम एक बग दर्ज कर सकते हैं ताकि वे इसे ठीक कर सकें।

इसके अलावा, हम रोज़लिन के लिए सी # और वीबी कंपाइलर्स के एल्गोरिदम में सुधार करने के लिए काम कर रहे हैं, यह निर्धारित करने के लिए कि स्थानीय लोगों को "क्षणिक" बनाया जा सकता है - यानी स्टैक पर एक विशिष्ट स्थान आवंटित करने के बजाय, केवल स्टैक पर धक्का दिया जाता है और पॉप किया जाता है सक्रियण की अवधि। हमारा मानना ​​है कि जेआईटीटर पंजीकरण आवंटन का बेहतर काम करने में सक्षम होगा और अगर हम इससे बेहतर संकेत देते हैं कि स्थानीय लोगों को पहले "मृत" बनाया जा सकता है।

इसे हमारे ध्यान में लाने के लिए धन्यवाद, और अजीब व्यवहार के लिए क्षमा चाहते हैं।





performance-testing