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




.net clr try-catch performance-testing (5)

जॉन की असेंबली दिखाती है कि दो संस्करणों के बीच का अंतर यह है कि तेज़ संस्करण स्थानीय चरों में से एक को स्टोर करने के लिए रजिस्टरों ( 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 के लिए अंतर भी देखा है।

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

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 के साथ संकलित होने पर समस्या दूर हो जाती है।


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

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

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

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

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


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

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 के तहत प्रयास / पकड़ ब्लॉक एक उल्लेखनीय अंतर नहीं बनाता है।


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


कोशिश / पकड़ प्रदर्शन पर प्रभाव पड़ता है।

लेकिन यह एक बड़ा प्रभाव नहीं है। कोशिश करें / पकड़ जटिलता आम तौर पर ओ (1) है, बस एक साधारण असाइनमेंट की तरह, जब उन्हें लूप में रखा जाता है। तो आपको उन्हें बुद्धिमानी से उपयोग करना होगा।

Here प्रयास / पकड़ प्रदर्शन के बारे में एक संदर्भ है (हालांकि इसकी जटिलता की व्याख्या नहीं करता है, लेकिन यह निहित है)। फेंक अपवाद अनुभाग थ्रो पर एक नज़र डालें







c# .net clr try-catch performance-testing