C # में बेंचमार्किंग छोटे कोड के नमूने, क्या इस कार्यान्वयन में सुधार किया जा सकता है?


104

एसओ पर अक्सर मैं खुद को कोड के छोटे हिस्से बेंचमार्किंग करता हूं यह देखने के लिए कि कौन सा उपकरण सबसे तेज़ है।

काफी बार मैं ऐसी टिप्पणियां देखता हूं कि बेंचमार्किंग कोड जटिंग या कचरा संग्रहकर्ता को ध्यान में नहीं रखता है।

मेरे पास निम्नलिखित सरल बेंचमार्किंग फंक्शन हैं जो मैंने धीरे-धीरे विकसित किए हैं:

  static void Profile(string description, int iterations, Action func) {
        // warm up 
        func();
        // clean up
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++) {
            func();
        }
        watch.Stop();
        Console.Write(description);
        Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
    }

उपयोग:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});

क्या इस कार्यान्वयन में कोई दोष है? क्या यह दिखाना काफी अच्छा है कि कार्यान्वयन X, Z पुनरावृत्तियों पर कार्यान्वयन Y से तेज है? क्या आप इसे सुधारने के किसी भी तरीके के बारे में सोच सकते हैं?

संपादित करें यह बहुत स्पष्ट है कि एक समय आधारित दृष्टिकोण (पुनरावृत्तियों के विपरीत) को प्राथमिकता दी जाती है, क्या किसी के पास कोई कार्यान्वयन है जहां समय की जांच प्रदर्शन को प्रभावित नहीं करती है?


जवाबों:


95

यहां संशोधित कार्य है: जैसा कि समुदाय द्वारा अनुशंसित है, इसे इसके लिए एक समुदाय विकी में संशोधन करने के लिए स्वतंत्र महसूस करें।

static double Profile(string description, int iterations, Action func) {
    //Run at highest priority to minimize fluctuations caused by other processes/threads
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
    Thread.CurrentThread.Priority = ThreadPriority.Highest;

    // warm up 
    func();

    var watch = new Stopwatch(); 

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

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
    return watch.Elapsed.TotalMilliseconds;
}

सुनिश्चित करें कि आप जारी किए गए अनुकूलन के साथ रिलीज़ में संकलित करें , और विज़ुअल स्टूडियो के बाहर परीक्षण चलाएं । यह अंतिम भाग महत्वपूर्ण है क्योंकि JIT डिबगर संलग्न के साथ इसके अनुकूलन को भी जारी करता है, यहां तक ​​कि रिलीज़ मोड में भी।


आप लूप ओवरहेड को कम करने के लिए लूप को 10 की तरह कुछ समय तक अनियंत्रित करना चाह सकते हैं।
माइक डनलैवी

2
मैंने बस स्टॉपवॉच का उपयोग करने के लिए अद्यतन किया। SartNew। एक कार्यात्मक परिवर्तन नहीं है, लेकिन कोड की एक पंक्ति को बचाता है।
ल्यूक

1
@ ल्यूक, महान बदलाव (काश मैं इसे +1 करता)। @ मुझे यकीन नहीं है, मुझे संदेह है कि वर्चुअलाइक ओवरहेड तुलना और असाइनमेंट से बहुत अधिक होगा, इसलिए प्रदर्शन भिन्नता नगण्य होगी
सैम केसर

मैं आपको एक्शन के लिए इटिरेशन काउंट पास करना चाहता हूं, और वहां लूप बनाना (संभवतः - यहां तक ​​कि अनियंत्रित भी)। यदि आप अपेक्षाकृत छोटे ऑपरेशन को माप रहे हैं तो यह एकमात्र विकल्प है। और मैं उलटा मीट्रिक देखना पसंद करूंगा - जैसे पास / सेकंड की गिनती।
एलेक्स याकुनिन

2
औसत समय दिखाने के बारे में आप क्या सोचते हैं। कुछ इस तरह से: Console.WriteLine ("औसत समय बीत गया {0} ms", watch.ElapsedMilliseconds / पुनरावृत्तियों);
rudimenter

22

जरूरी नहीं कि GC.Collectरिटर्न भरने से पहले फाइनल किया जाए । अंतिम रूप पंक्तिबद्ध है और फिर एक अलग धागे पर चलता है। यह थ्रेड आपके परीक्षणों के दौरान अभी भी सक्रिय हो सकता है, जिससे परिणाम प्रभावित होंगे।

यदि आप यह सुनिश्चित करना चाहते हैं कि आपके परीक्षण शुरू करने से पहले अंतिम रूप दिया जा चुका है, तो आप कॉल करना चाह सकते हैं GC.WaitForPendingFinalizers, जो अंतिम रूप से कतार साफ होने तक अवरुद्ध हो जाएगा:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

10
GC.Collect()एक बार फिर क्यों ?
कॉलिनफैंग

7
@colinfang क्योंकि "अंतिम रूप" वाली वस्तुओं को अंतिम रूप से GC'ed नहीं किया जाता है। तो दूसरा Collectयह है कि यह सुनिश्चित करने के लिए कि "अंतिम रूप से" ऑब्जेक्ट भी एकत्र किए गए हैं।
MAV

15

यदि आप GC इंटरेक्शन को समीकरण से बाहर ले जाना चाहते हैं, तो आप GC.Collect कॉल के बाद अपना 'वार्म अप' कॉल चलाना चाह सकते हैं , इससे पहले नहीं। इस तरह से आप जानते हैं कि .NET आपके फ़ंक्शन के कार्य सेट के लिए पहले से ही ओएस से पर्याप्त मेमोरी आवंटित करेगा।

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

इसके अलावा, इस बात पर निर्भर करता है कि आप किस तरह के सामान की रूपरेखा तैयार कर रहे हैं, आप एक निश्चित संख्या में पुनरावृत्तियों की बजाय एक निश्चित समय के लिए अपनी समय-आधारित गतिविधि करना चाहते हैं - इसके बिना अधिक आसानी से तुलनीय संख्याओं को जन्म दे सकते हैं सबसे अच्छा कार्यान्वयन और / या बहुत बुरे के लिए एक बहुत लंबे समय के लिए एक बहुत ही कम रन होना।


1
अच्छा अंक, क्या आपके मन में समय आधारित कार्यान्वयन होगा?
सैम केसर

6

मैं प्रतिनिधि को पास करने से बचता हूँ:

  1. प्रतिनिधि कॉल ~ आभासी विधि कॉल है। सस्ता नहीं: ~ .NET में सबसे छोटी मेमोरी आवंटन का 25%। यदि आप विवरण में रुचि रखते हैं, तो यह लिंक देखें ।
  2. अनाम प्रतिनिधियों को बंद करने का उपयोग करने के लिए नेतृत्व कर सकते हैं, कि आप भी ध्यान नहीं देंगे। एक बार फिर, स्टैक पर एक चर का उपयोग करने की तुलना में बंद खेतों तक पहुंच काफ़ी है।

उपयोग बंद करने के लिए अग्रणी एक उदाहरण कोड:

public void Test()
{
  int someNumber = 1;
  Profiler.Profile("Closure access", 1000000, 
    () => someNumber + someNumber);
}

यदि आप क्लोज़र के बारे में जागरूक नहीं हैं, तो .NET रिफ्लेक्टर में इस पद्धति पर एक नज़र डालें।


दिलचस्प बिंदु, लेकिन यदि आप एक प्रतिनिधि को पास नहीं करते हैं तो आप फिर से प्रयोग करने योग्य प्रोफ़ाइल () विधि कैसे बनाएंगे? क्या विधि के लिए मनमाने कोड पारित करने के अन्य तरीके हैं?
ऐश

1
हम "(नए माप (...)) {... मापा कोड ...}" का उपयोग करते हैं। तो हम प्रतिनिधि को पारित करने के बजाय आईडीसोपायरी को लागू करने वाले मापन ऑब्जेक्ट प्राप्त करते हैं। देखें code.google.com/p/dataobjectsdotnet/source/browse/Xtensive.Core/...
एलेक्स Yakunin

यह बंद होने के साथ किसी भी मुद्दे को जन्म नहीं देगा।
एलेक्स याकुनिन

3
@AlexYakunin: आपका लिंक टूटा हुआ प्रतीत होता है। क्या आप अपने उत्तर में मापन वर्ग के लिए कोड शामिल कर सकते हैं? मुझे संदेह है कि आप इसे कैसे लागू करते हैं, इससे कोई फर्क नहीं पड़ता, आप कई बार इस आईडीसोफाइल दृष्टिकोण के साथ कोड को चलाने के लिए सक्षम नहीं होंगे। हालांकि, यह वास्तव में उन स्थितियों में बहुत उपयोगी है, जहां आप मापना चाहते हैं कि किसी जटिल (इंटरवेट) एप्लिकेशन के विभिन्न भाग कैसे प्रदर्शन कर रहे हैं, इसलिए जब तक आप ध्यान रखें कि माप अलग-अलग हो सकते हैं, और अलग-अलग समय पर चलने पर असंगत हो सकते हैं। मैं अपनी अधिकांश परियोजनाओं में उसी दृष्टिकोण का उपयोग कर रहा हूं।
ShdNx

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

6

मुझे लगता है कि बेंचमार्किंग के तरीकों पर काबू पाने के लिए सबसे कठिन समस्या इस तरह से है कि एज मामलों और अप्रत्याशित के लिए लेखांकन है। उदाहरण के लिए - "दो कोड स्निपेट्स उच्च CPU लोड / नेटवर्क उपयोग / डिस्क थ्रेशिंग / आदि के तहत कैसे काम करते हैं।" वे बुनियादी तर्क जांच के लिए महान हैं कि क्या एक विशेष एल्गोरिथ्म दूसरे की तुलना में काफी तेजी से काम करता है । लेकिन अधिकांश कोड प्रदर्शन का ठीक से परीक्षण करने के लिए आपको एक परीक्षण बनाना होगा जो उस विशेष कोड के विशिष्ट बाधाओं को मापता है।

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


1
महत्वपूर्ण उन शर्तों में से एक है जो वास्तव में भरी हुई है। कभी-कभी 20% तेजी से कार्यान्वयन महत्वपूर्ण है, कभी-कभी इसे महत्वपूर्ण होने के लिए 100 गुना तेज होना पड़ता है। स्पष्टता पर आपके साथ सहमत देखें: stackoverflow.com/questions/1018407/…
सैम केसर

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

5

मैं func()वार्म-अप के लिए कई बार फोन करता हूं , सिर्फ एक के लिए नहीं।


1
इरादा यह सुनिश्चित करने के लिए था कि जीआईटी संकलन का प्रदर्शन किया जाता है, माप से पहले कई बार फंक बुलाने से आपको क्या फायदा होता है?
सैम केसर

3
जेआईटी को अपने पहले परिणामों में सुधार करने का मौका देने के लिए।
एलेक्सी रोमानोव

1
.NET JIT समय के साथ परिणाम (जैसे जावा एक करता है) में सुधार नहीं करता है। यह केवल पहली कॉल पर IL से असेंबली के लिए एक विधि को परिवर्तित करता है।
मैट वॉरेन

4

सुधार हेतु सुझाव

  1. यह पता लगाना कि क्या निष्पादन का माहौल बेंचमार्किंग के लिए अच्छा है (जैसे कि यह पता लगाना कि क्या डिबगर जुड़ा हुआ है या यदि जेट ऑप्टिमाइज़ेशन अक्षम है, जिसके परिणामस्वरूप गलत माप होगा)।

  2. कोड के कुछ हिस्सों को स्वतंत्र रूप से मापना (यह देखना कि अड़चन कहां है)।

  3. कोड के विभिन्न संस्करणों / घटकों / विखंडू की तुलना में (आपके पहले वाक्य में आप कहते हैं '... कोड का छोटा हिस्सा बेंचमार्किंग यह देखने के लिए कि कौन सा कार्यान्वयन सबसे तेज़ है।')।

# 1 के बारे में:

  • यह पता लगाने के लिए कि क्या डिबगर संलग्न है, संपत्ति पढ़ें System.Diagnostics.Debugger.IsAttached(यह भी याद रखें कि जहां डिबगर शुरू में संलग्न नहीं है, लेकिन कुछ समय बाद संलग्न है)।

  • यह पता लगाने के लिए कि क्या जीट ऑप्टिमाइज़ेशन अक्षम है, DebuggableAttribute.IsJITOptimizerDisabledसंबंधित असेंबली की संपत्ति पढ़ें :

    private bool IsJitOptimizerDisabled(Assembly assembly)
    {
        return assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
            .Select(customAttribute => (DebuggableAttribute) customAttribute)
            .Any(attribute => attribute.IsJITOptimizerDisabled);
    }

# 2 के बारे में:

यह कई तरीकों से किया जा सकता है। एक तरीका यह है कि कई प्रतिनिधियों को आपूर्ति की अनुमति दी जाए और फिर उन प्रतिनिधियों को व्यक्तिगत रूप से मापें।

# 3 के बारे में:

यह कई तरीकों से भी किया जा सकता है, और विभिन्न उपयोग-मामले बहुत अलग समाधानों की मांग करेंगे। यदि बेंचमार्क मैन्युअल रूप से लागू किया जाता है, तो कंसोल पर लिखना ठीक हो सकता है। हालाँकि यदि बेंचमार्क स्वचालित रूप से बिल्ड सिस्टम द्वारा किया जाता है, तो कंसोल पर लिखना संभवतः इतना ठीक नहीं है।

ऐसा करने का एक तरीका बेंचमार्क परिणाम को एक दृढ़ता से टाइप की गई वस्तु के रूप में वापस करना है जिसे आसानी से विभिन्न संदर्भों में खाया जा सकता है।


Etimo.Benchmarks

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

यह एक उदाहरण आउटपुट है जहां दो घटकों की तुलना की जाती है और परिणाम कंसोल को लिखे जाते हैं। इस मामले में तुलना किए गए दो घटकों को 'कीडेकोलिक्शन' और 'मल्टीप्लीइंडेक्सकेयेडकॉलिनेशन' कहा जाता है:

Etimo.Benchmark - नमूना कंसोल आउटपुट

वहाँ एक NuGet पैकेज , एक नमूना NuGet पैकेज और स्रोत कोड GitHub पर उपलब्ध है । एक ब्लॉग पोस्ट भी है ।

यदि आप जल्दी में हैं, तो मेरा सुझाव है कि आप नमूना पैकेज प्राप्त करें और आवश्यकतानुसार केवल नमूना प्रतिनिधियों को संशोधित करें। यदि आप जल्दी में नहीं हैं, तो विवरणों को समझने के लिए ब्लॉग पोस्ट को पढ़ना एक अच्छा विचार हो सकता है।


1

JIT संकलक को आपके कोड को छोड़ने पर खर्च होने वाले समय को बाहर करने के लिए वास्तविक माप से पहले आपको "वार्म अप" पास भी चलाना होगा।


यह माप से पहले किया जाता है
सैम केसर

1

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


1

यदि आप बेंचमार्क से गारबेज कलेक्शन के प्रभाव को खत्म करने की कोशिश कर रहे हैं, तो क्या यह स्थापित करने लायक है GCSettings.LatencyMode?

यदि नहीं, और आप funcबेंचमार्क का हिस्सा बनने के लिए बनाए गए कचरे का प्रभाव चाहते हैं , तो क्या आपको परीक्षण के अंत में (टाइमर के अंदर) भी संग्रह को मजबूर नहीं करना चाहिए?


0

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

एक अन्य उत्तर मूल प्रदर्शन को मापने का एक अच्छा तरीका देता है।

static void Profile(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

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

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

हालांकि, यह एकल माप कचरा संग्रहण के लिए जिम्मेदार नहीं है। कई कॉलों में फैले कचरे के संग्रह के सबसे खराब मामले के प्रदर्शन के लिए एक उचित प्रोफ़ाइल अतिरिक्त खाता है (यह संख्या बेकार की तरह है क्योंकि वीएम कभी भी कचरे को बिना इकट्ठा किए समाप्त कर सकता है लेकिन फिर भी दो अलग-अलग कार्यान्वयनों की तुलना करने के लिए उपयोगी है func।)

static void ProfileGarbageMany(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

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

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

    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

और एक विधि के लिए कचरा संग्रह के सबसे खराब मामले के प्रदर्शन को मापने के लिए भी हो सकता है जिसे केवल एक बार कहा जाता है।

static void ProfileGarbage(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

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

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

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

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

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.