c# 32 और 64 बिट्स के लिए संकलन करते समय विशाल प्रदर्शन अंतर(26x तेज)




performance 32bit-64bit (4)

मैं इसे 4.5.2 पर पुन: पेश कर सकता हूं। यहां कोई RyuJIT नहीं है। X86 और x64 दोनों disassemblies उचित दिखते हैं। रेंज चेक और इसी तरह के हैं। वही बुनियादी संरचना। कोई लूप अनोलिंग नहीं।

x86 फ्लोट निर्देशों के एक अलग सेट का उपयोग करता है। प्रभाग को छोड़कर इन निर्देशों का प्रदर्शन x64 निर्देशों के साथ तुलनीय लगता है:

  1. 32 बिट x87 फ्लोट निर्देश आंतरिक रूप से 10 बाइट परिशुद्धता का उपयोग करते हैं।
  2. विस्तारित सटीक विभाजन सुपर धीमी है।

विभाजन ऑपरेशन 32 बिट संस्करण को बहुत धीमा बनाता है। विभाजन को विघटित करने से बड़ी मात्रा में प्रदर्शन बराबर होता है (430ms से 3.25ms तक 32 बिट नीचे)।

पीटर कॉर्डस बताते हैं कि दो फ्लोटिंग पॉइंट इकाइयों की निर्देश विलंबता भिन्न नहीं हैं। हो सकता है कि कुछ मध्यवर्ती परिणाम denormalized संख्या या NaN हैं। ये इकाइयों में से एक में धीमा पथ ट्रिगर कर सकते हैं। या, हो सकता है कि मूल्य 10 बाइट बनाम 8 बाइट फ्लोट परिशुद्धता के कारण दो कार्यान्वयन के बीच अलग हो जाएं।

पीटर कॉर्डस यह भी बताते हैं कि सभी मध्यवर्ती परिणाम NaN हैं ... इस समस्या को हटा रहा है ( valueList.Add(i + 1) ताकि कोई divisor शून्य न हो) अधिकतर परिणामों को बराबर करता है। जाहिर है, 32 बिट कोड NaN ऑपरेटरों को बिल्कुल पसंद नहीं करता है। आइए कुछ मध्यवर्ती मान मुद्रित करें: if (i % 1000 == 0) Console.WriteLine(result); । यह पुष्टि करता है कि डेटा अब सचेत है।

जब बेंचमार्किंग आपको एक यथार्थवादी वर्कलोड बेंचमार्क करने की आवश्यकता होती है। लेकिन किसने सोचा होगा कि एक निर्दोष विभाजन आपके बेंचमार्क को गड़बड़ कर सकता है ?!

बेहतर बेंचमार्क प्राप्त करने के लिए संख्याओं को संक्षेप में आज़माएं।

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

32 बिट कोड यहां दिया गया है:

64 बिट कोड (समान संरचना, फास्ट डिवीजन):

एसएसई निर्देशों का इस्तेमाल होने के बावजूद यह सदिश नहीं है।

मैं मूल्य प्रकारों और संदर्भ प्रकारों की सूचियों तक पहुंचने के for एक foreach का उपयोग करने के अंतर को मापने की कोशिश कर रहा था।

मैंने निम्नलिखित कक्षा का उपयोग प्रोफाइलिंग करने के लिए किया था।

public static class Benchmarker
{
    public static void Profile(string description, int iterations, Action func)
    {
        Console.Write(description);

        // Warm up
        func();

        Stopwatch watch = new Stopwatch();

        // Clean up
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        watch.Start();
        for (int i = 0; i < iterations; i++)
        {
            func();
        }
        watch.Stop();

        Console.WriteLine(" average time: {0} ms", watch.Elapsed.TotalMilliseconds / iterations);
    }
}

मैंने अपने मूल्य प्रकार के लिए double उपयोग किया। और मैंने संदर्भ प्रकारों का परीक्षण करने के लिए यह 'नकली वर्ग' बनाया है:

class DoubleWrapper
{
    public double Value { get; set; }

    public DoubleWrapper(double value)
    {
        Value = value;
    }
}

अंत में मैंने इस कोड को चलाया और समय अंतर की तुलना की।

static void Main(string[] args)
{
    int size = 1000000;
    int iterationCount = 100;

    var valueList = new List<double>(size);
    for (int i = 0; i < size; i++) 
        valueList.Add(i);

    var refList = new List<DoubleWrapper>(size);
    for (int i = 0; i < size; i++) 
        refList.Add(new DoubleWrapper(i));

    double dummy;

    Benchmarker.Profile("valueList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < valueList.Count; i++)
        {
             unchecked
             {
                 var temp = valueList[i];
                 result *= temp;
                 result += temp;
                 result /= temp;
                 result -= temp;
             }
        }
        dummy = result;
    });

    Benchmarker.Profile("valueList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in valueList)
        {
            var temp = v;
            result *= temp;
            result += temp;
            result /= temp;
            result -= temp;
        }
        dummy = result;
    });

    Benchmarker.Profile("refList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < refList.Count; i++)
        {
            unchecked
            {
                var temp = refList[i].Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }
        dummy = result;
    });

    Benchmarker.Profile("refList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in refList)
        {
            unchecked
            {
                var temp = v.Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }

        dummy = result;
    });

    SafeExit();
}

मैंने Release और Any CPU विकल्प चुना, प्रोग्राम चलाया और निम्नलिखित बार मिला:

valueList for:  average time: 483,967938 ms
valueList foreach:  average time: 477,873079 ms
refList for:  average time: 490,524197 ms
refList foreach:  average time: 485,659557 ms
Done!

फिर मैंने रिलीज और एक्स 64 विकल्पों का चयन किया, कार्यक्रम चलाया और निम्नलिखित बार मिला:

valueList for:  average time: 16,720209 ms
valueList foreach:  average time: 15,953483 ms
refList for:  average time: 19,381077 ms
refList foreach:  average time: 18,636781 ms
Done!

X64 बिट संस्करण इतना तेज़ क्यों है? मुझे कुछ अंतर होने की उम्मीद है, लेकिन यह कुछ बड़ा नहीं है।

मेरे पास अन्य कंप्यूटर तक पहुंच नहीं है। क्या आप इसे अपनी मशीनों पर चला सकते हैं और मुझे परिणाम बता सकते हैं? मैं विजुअल स्टूडियो 2015 का उपयोग कर रहा हूं और मेरे पास इंटेल Core i7 930 है।

यहां SafeExit() विधि है, इसलिए आप स्वयं द्वारा संकलित / चला सकते हैं:

private static void SafeExit()
{
    Console.WriteLine("Done!");
    Console.ReadLine();
    System.Environment.Exit(1);
}

अनुरोध के रूप में, double? का उपयोग कर double? मेरे DoubleWrapper बजाय:

कोई सीपीयू

valueList for:  average time: 482,98116 ms
valueList foreach:  average time: 478,837701 ms
refList for:  average time: 491,075915 ms
refList foreach:  average time: 483,206072 ms
Done!

64

valueList for:  average time: 16,393947 ms
valueList foreach:  average time: 15,87007 ms
refList for:  average time: 18,267736 ms
refList foreach:  average time: 16,496038 ms
Done!

अंतिम लेकिन कम से कम नहीं: x86 प्रोफाइल बनाना मुझे लगभग Any CPU का उपयोग करने के समान परिणाम देता है


हमारे पास अवलोकन है कि 99.9% सभी फ्लोटिंग प्वाइंट ऑपरेशंस में नाएन शामिल होंगे, जो कम से कम असामान्य है (पीटर कॉर्डस द्वारा पहले पाया गया)। हमारे पास usr द्वारा एक और प्रयोग है, जिसमें पाया गया कि विभाजन निर्देशों को हटाने से समय अंतर लगभग पूरी तरह से दूर हो जाता है।

तथ्य यह है कि NaN केवल उत्पन्न होते हैं क्योंकि पहला विभाजन 0.0 / 0.0 की गणना करता है जो प्रारंभिक NaN देता है। यदि विभाजन नहीं किए जाते हैं, तो परिणाम हमेशा 0.0 होगा, और हम हमेशा 0.0 * temp -> 0.0, 0.0 + temp -> temp, temp - temp = 0.0 की गणना करेंगे। इसलिए विभाजन को हटाने से न केवल डिवीजनों को हटा दिया गया, बल्कि नाएन को भी हटा दिया गया। मैं उम्मीद करता हूं कि नाएन वास्तव में समस्या है, और यह कि एक कार्यान्वयन नाएन के बहुत धीरे-धीरे संभालता है, जबकि दूसरे को समस्या नहीं होती है।

I = 1 पर लूप शुरू करना और फिर से मापना फायदेमंद होगा। चार संचालन परिणाम * अस्थायी, + अस्थायी, / अस्थायी, - अस्थायी रूप से प्रभावी रूप से (1 - temp) जोड़ते हैं, इसलिए अधिकांश परिचालनों के लिए हमारे पास कोई असामान्य संख्या (0, अनंतता, NaN) नहीं होती है।

एकमात्र समस्या यह हो सकती है कि विभाजन हमेशा एक पूर्णांक परिणाम देता है, और कुछ विभाजन कार्यान्वयन में शॉर्टकट होते हैं जब सही परिणाम कई बिट्स का उपयोग नहीं करता है। उदाहरण के लिए, 310.0 / 31.0 को विभाजित करने से 0.0 के शेष के साथ पहले चार बिट्स के रूप में 10.0 मिलता है, और कुछ कार्यान्वयन शेष 50 या इतने बिट्स का मूल्यांकन करना बंद कर सकते हैं जबकि अन्य नहीं कर सकते हैं। यदि कोई महत्वपूर्ण अंतर है, तो परिणाम = 1.0 / 3.0 के साथ लूप शुरू करना एक फर्क पड़ता है।


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

प्रोसेसर आर्किटेक्चर:

इंटेल का सीपीयू आर्किटेक्चर पूरी तरह से 64 बिट था। 32 बिट कोड निष्पादित करने के लिए, 32 बिट निर्देशों को निष्पादन से पहले 64 बिट निर्देशों में परिवर्तित करने के लिए (सीपीयू के अंदर) को बदलने की आवश्यकता थी।

एएमडी का सीपीयू आर्किटेक्चर 64 बिट आर्किटेक्चर के शीर्ष पर 64 बिट सही बनाना था; अर्थात, यह 64 बिट सीमाओं के साथ अनिवार्य रूप से 32 बिट आर्किटेक्चर था - कोई कोड रूपांतरण प्रक्रिया नहीं थी।

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

.NET जेआईटी

यह तर्क दिया जाता है कि .NET (और जावा जैसी अन्य प्रबंधित भाषाओं) सी ++ जैसी भाषाओं को बेहतर प्रदर्शन करने में सक्षम हैं क्योंकि जिस तरह से जेआईटी कंपाइलर आपके प्रोसेसर आर्किटेक्चर के अनुसार आपके कोड को अनुकूलित करने में सक्षम है। इस संबंध में, आप पाएंगे कि जेआईटी कंपाइलर 64 बिट आर्किटेक्चर में कुछ उपयोग कर रहा है जो संभवतः उपलब्ध नहीं था या 32 बिट में निष्पादित होने पर एक वर्कअराउंड की आवश्यकता थी।

ध्यान दें:

DoubleWrapper का उपयोग करने के बजाय, क्या आपने Nullable<double> या shorthand वाक्यविन्यास का उपयोग करने पर विचार किया है: double? - मुझे यह देखने में दिलचस्पी होगी कि क्या आपके परीक्षणों पर इसका कोई असर पड़ता है या नहीं।

नोट 2: कुछ लोग आईए -64 के साथ 64 बिट आर्किटेक्चर के बारे में मेरी टिप्पणियां स्वीकार कर रहे हैं। बस स्पष्ट करने के लिए, मेरे उत्तर में, 64 बिट x86-64 को संदर्भित करता है और 32 बिट x86-32 को संदर्भित करता है। आईए -64 का संदर्भ यहां कुछ भी नहीं!


valueList[i] = i , i=0 से शुरू होता है, इसलिए पहला लूप पुनरावृत्ति 0.0 / 0.0 करता है। तो आपके पूरे बेंचमार्क में हर ऑपरेशन NaN एस के साथ किया जाता है।

चूंकि @usr ने डिस्प्लेब्स आउटपुट में दिखाया , 32 बिट संस्करण ने x87 फ्लोटिंग पॉइंट का उपयोग किया, जबकि 64 बिट एसएसई फ्लोटिंग पॉइंट का इस्तेमाल किया।

मैं NaN साथ प्रदर्शन पर विशेषज्ञ नहीं हूं, या इसके लिए x87 और एसएसई के बीच का अंतर नहीं हूं, लेकिन मुझे लगता है कि यह 26x perf अंतर बताता है। मैं शर्त लगाता हूं कि यदि आप valueList[i] = i+1 प्रारंभ valueList[i] = i+1 तो आपके परिणाम 32 और 64 बिट के बीच बहुत करीब होंगे। (अपडेट: यूएसआर ने पुष्टि की कि इसने 32 और 64 बिट प्रदर्शन को काफी करीब बनाया है।)

डिवीजन अन्य परिचालनों की तुलना में बहुत धीमी है। @ Usr के उत्तर पर मेरी टिप्पणियां देखें। हार्डवेयर के बारे में बहुत सी चीजों के लिए agner.org/optimize और एएसएम और सी / सी ++ को अनुकूलित करने के लिए भी देखें, इनमें से कुछ सी # से प्रासंगिक हैं। सभी हालिया x86 CPUs के लिए अधिकांश निर्देशों के लिए उनके पास विलंबता और थ्रूपुट की निर्देश तालिकाएं हैं।

हालांकि, 10 बी x87 fdiv सामान्य मूल्यों के लिए एसएसई 2 के 8 बी डबल परिशुद्धता divsd से बहुत धीमी नहीं है। NaN, infinities, या denormals के साथ perf मतभेदों के बारे में आईडीके।

हालांकि, एनएएन और अन्य एफपीयू अपवादों के साथ क्या होता है, उनके लिए उनके पास अलग-अलग नियंत्रण होते हैं। एक्स 87 एफपीयू नियंत्रण शब्द एसएसई राउंडिंग / अपवाद नियंत्रण रजिस्टर (एमएक्ससीएसआर) से अलग है। यदि x87 को प्रत्येक विभाजन के लिए सीपीयू अपवाद मिल रहा है, लेकिन एसएसई नहीं है, तो यह आसानी से 26 के कारक को समझाता है। या शायद नाइन को संभालने में बड़ा प्रदर्शन होता है। NaN बाद NaN माध्यम से मंथन के लिए हार्डवेयर को अनुकूलित नहीं किया गया है।

आईडीके अगर एसएसई नियंत्रणों के साथ मंदी से बचने के लिए नियंत्रण यहां आ जाएगा, क्योंकि मेरा मानना ​​है कि result हर समय NaN होगा। आईडीके अगर सी # एमएक्ससीएसआर में denormals-zero-zero ध्वज सेट करता है, या फ्लश-टू-शून्य-फ्लैग (जो पहले स्थान पर शून्य लिखता है, तो वापस पढ़ने के दौरान शून्य के रूप में denormals का इलाज करने के बजाय)।

मुझे एसएसई फ्लोटिंग पॉइंट कंट्रोल के बारे में एक इंटेल आलेख मिला, जो इसे x87 एफपीयू नियंत्रण शब्द से अलग करता है। हालांकि, NaN बारे में ज्यादा कुछ कहना नहीं है। यह इसके साथ समाप्त होता है:

निष्कर्ष

Denormals और underflow संख्याओं के कारण क्रमिकरण और प्रदर्शन समस्याओं से बचने के लिए, फ्लोट-टू-शून्य और डेनॉर्मल्स-एर-शून्य मोड सेट करने के लिए एसएसई और एसएसई 2 निर्देशों का उपयोग करें ताकि फ्लोटिंग-पॉइंट अनुप्रयोगों के लिए उच्चतम प्रदर्शन सक्षम हो सके।

आईडीके अगर यह विभाजित-शून्य-शून्य के साथ किसी की सहायता करता है।

बनाम foreach के लिए

एक लूप बॉडी का परीक्षण करना दिलचस्प हो सकता है जो केवल एक एकल लूप-निर्भर निर्भरता श्रृंखला होने के बजाय थ्रूपुट-सीमित है। जैसा कि है, सभी काम पिछले परिणामों पर निर्भर करता है; सीपीयू के समानांतर में करने के लिए कुछ भी नहीं है (सीमाओं के अलावा, अगली सरणी लोड की जांच करें जबकि मुल / div श्रृंखला चल रही है)।

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





32bit-64bit