मेरा आवेदन अपने जीवन का 24% शून्य जांच क्यों करता है?


104

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

        public ScTreeNode GetNodeForState(int rootIndex, float[] inputs)
        {
0.2%        ScTreeNode node = RootNodes[rootIndex].TreeNode;

24.6%       while (node.BranchData != null)
            {
0.2%            BranchNodeData b = node.BranchData;
0.5%            node = b.Child2;
12.8%           if (inputs[b.SplitInputIndex] <= b.SplitValue)
0.8%                node = b.Child1;
            }

0.4%        return node;
        }

ब्रांचडाटा एक फील्ड है, प्रॉपर्टी नहीं। मैंने ऐसा नहीं किया ताकि इसके जोखिम को कम न किया जा सके।

BranchNodeData वर्ग निम्नानुसार है:

public sealed class BranchNodeData
{
    /// <summary>
    /// The index of the data item in the input array on which we need to split
    /// </summary>
    internal int SplitInputIndex = 0;

    /// <summary>
    /// The value that we should split on
    /// </summary>
    internal float SplitValue = 0;

    /// <summary>
    /// The nodes children
    /// </summary>
    internal ScTreeNode Child1;
    internal ScTreeNode Child2;
}

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

मैंने कोशिश की:

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

क्या यह एक शाखा भविष्यवाणी मुद्दा है? यदि हां, तो मैं इसके बारे में क्या कर सकता हूं? अगर कुछ भी?

मैं CIL को समझने का नाटक नहीं करूँगा, लेकिन मैं इसे किसी के लिए भी पोस्ट करूँगा ताकि वे इसमें से कुछ जानकारी निकालने की कोशिश कर सकें।

.method public hidebysig
instance class OptimalTreeSearch.ScTreeNode GetNodeForState (
    int32 rootIndex,
    float32[] inputs
) cil managed
{
    // Method begins at RVA 0x2dc8
    // Code size 67 (0x43)
    .maxstack 2
    .locals init (
        [0] class OptimalTreeSearch.ScTreeNode node,
        [1] class OptimalTreeSearch.BranchNodeData b
    )

    IL_0000: ldarg.0
    IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<class OptimalTreeSearch.ScRootNode> OptimalTreeSearch.ScSearchTree::RootNodes
    IL_0006: ldarg.1
    IL_0007: callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1<class OptimalTreeSearch.ScRootNode>::get_Item(int32)
    IL_000c: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.ScRootNode::TreeNode
    IL_0011: stloc.0
    IL_0012: br.s IL_0039
    // loop start (head: IL_0039)
        IL_0014: ldloc.0
        IL_0015: ldfld class OptimalTreeSearch.BranchNodeData OptimalTreeSearch.ScTreeNode::BranchData
        IL_001a: stloc.1
        IL_001b: ldloc.1
        IL_001c: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.BranchNodeData::Child2
        IL_0021: stloc.0
        IL_0022: ldarg.2
        IL_0023: ldloc.1
        IL_0024: ldfld int32 OptimalTreeSearch.BranchNodeData::SplitInputIndex
        IL_0029: ldelem.r4
        IL_002a: ldloc.1
        IL_002b: ldfld float32 OptimalTreeSearch.BranchNodeData::SplitValue
        IL_0030: bgt.un.s IL_0039

        IL_0032: ldloc.1
        IL_0033: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.BranchNodeData::Child1
        IL_0038: stloc.0

        IL_0039: ldloc.0
        IL_003a: ldfld class OptimalTreeSearch.BranchNodeData OptimalTreeSearch.ScTreeNode::BranchData
        IL_003f: brtrue.s IL_0014
    // end loop

    IL_0041: ldloc.0
    IL_0042: ret
} // end of method ScSearchTree::GetNodeForState

संपादित करें: मैंने एक शाखा भविष्यवाणी परीक्षण करने का फैसला किया, मैंने थोड़ी देर के भीतर एक समान जोड़ा, तो हमारे पास है

while (node.BranchData != null)

तथा

if (node.BranchData != null)

उसके अंदर। मैंने उसके बाद प्रदर्शन विश्लेषण चलाया, और पहली तुलना को निष्पादित करने में छह गुना अधिक समय लगा क्योंकि यह दूसरी तुलना को निष्पादित करने के लिए किया था जो पूरी तरह से वापस आ गई थी। तो ऐसा लगता है कि यह वास्तव में एक शाखा भविष्यवाणी मुद्दा है - और मुझे लगता है कि वहाँ कुछ भी नहीं मैं इसके बारे में क्या कर सकता हूँ ?!

एक और संपादन

उपरोक्त परिणाम तब भी होगा यदि नोड। BranchData को थोड़ी देर के लिए RAM से लोड किया जाना था - यह तब के स्टेटमेंट के लिए कैश किया जाएगा।


यह एक समान विषय पर मेरा तीसरा प्रश्न है। इस बार मैं कोड की एक पंक्ति पर ध्यान केंद्रित कर रहा हूं। इस विषय पर मेरे अन्य प्रश्न हैं:


3
कृपया BranchNodeसंपत्ति के कार्यान्वयन को दिखाएं । कृपया बदलने का प्रयास करें node.BranchData != null ReferenceEquals(node.BranchData, null)। इससे क्या फ़र्क पड़ता है?
डैनियल हिलगार्थ

4
क्या आप सुनिश्चित हैं कि 24% समय कथन के लिए नहीं हैं और न ही कथन के उस भाग की अभिव्यक्ति की स्थिति के लिए
रुस FS

2
एक और परीक्षण: अपने लूप को इस तरह से फिर से लिखने की कोशिश करें while(true) { /* current body */ if(node.BranchData == null) return node; }:। क्या यह कुछ बदलता है?
डैनियल हिल्गारथ

2
थोड़ा अनुकूलन निम्नलिखित while(true) { BranchNodeData b = node.BranchData; if(ReferenceEquals(b, null)) return node; node = b.Child2; if (inputs[b.SplitInputIndex] <= b.SplitValue) node = b.Child1; }होगा : यह node. BranchDataकेवल एक बार प्राप्त होगा ।
डैनियल हिल्गारथ

2
कृपया सबसे बड़ी समय खपत वाली दो पंक्तियों को कुल मिलाकर निष्पादित करें।
डैनियल हिल्गारथ

जवाबों:


180

पेड़ बड़े पैमाने पर है

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

इस समस्या के लिए प्रोसेसर के पास एक काउंटर-माप है, वे कैश में उपयोग करते हैं , बफ़र्स जो रैम में बाइट्स की एक प्रति संग्रहीत करते हैं। एक महत्वपूर्ण L1 कैश है , आमतौर पर डेटा के लिए 16 किलोबाइट और निर्देशों के लिए 16 किलोबाइट। छोटा, यह निष्पादन इंजन के करीब होने की अनुमति देता है। L1 कैश से बाइट्स पढ़ने में आमतौर पर 2 या 3 सीपीयू चक्र लगते हैं। आगे एल 2 कैश, बड़ा और धीमा है। Upscale प्रोसेसर में L3 कैश भी बड़ा और धीमा है। जैसे-जैसे प्रक्रिया प्रौद्योगिकी में सुधार होता है, वे बफ़र्स कम जगह लेते हैं और स्वचालित रूप से तेजी से बनते जाते हैं क्योंकि वे कोर के करीब पहुंच जाते हैं, एक बड़ा कारण यह है कि नए प्रोसेसर बेहतर होते हैं और कैसे वे ट्रांजिस्टर की बढ़ती संख्या का उपयोग करने का प्रबंधन करते हैं।

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

पेड़ की संरचना एक समस्या है, वे कैश के अनुकूल नहीं हैं । उनके नोड्स पूरे पता स्थान पर बिखरे हुए हैं। मेमोरी तक पहुंचने का सबसे तेज़ तरीका अनुक्रमिक पतों से पढ़ना है। L1 कैश के लिए भंडारण की इकाई 64 बाइट्स है। या दूसरे शब्दों में, एक बार जब प्रोसेसर एक बाइट पढ़ता है , तो अगले 63 बहुत तेज़ होते हैं क्योंकि वे कैश में मौजूद होंगे।

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

इसलिए आपका समय () स्टेटमेंट CPU स्टालों से पीड़ित होने की बहुत संभावना है क्योंकि यह ब्रांच को एक्सेस करने के लिए एक पॉइंटर को डीफ़रेंस कर रहा है। अगला कथन बहुत सस्ता है क्योंकि () कथन में पहले से ही मेमोरी से मान प्राप्त करने की भारी उठाने की बात थी। स्थानीय वैरिएबल सस्ता होने के कारण, प्रोसेसर लिखने के लिए बफर का उपयोग करता है।

अन्यथा हल करने के लिए एक साधारण समस्या नहीं है, अपने पेड़ को सरणियों में समतल करना बहुत ही अव्यावहारिक है। कम से कम इसलिए नहीं क्योंकि आप आमतौर पर यह अनुमान नहीं लगा सकते हैं कि किस क्रम में पेड़ के नोड्स का दौरा किया जाएगा। एक लाल-काला पेड़ मदद कर सकता है, यह सवाल से स्पष्ट नहीं है। तो आकर्षित करने के लिए एक सरल निष्कर्ष यह है कि यह पहले से ही तेजी से चल रहा है, जिसकी आप उम्मीद कर सकते हैं। और अगर आपको इसे और तेज करने की आवश्यकता है, तो आपको तेज मेमोरी बस के साथ बेहतर हार्डवेयर की आवश्यकता होगी। इस वर्ष DDR4 मुख्यधारा में जा रहा है।


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

11
थ्रेड्स इस समस्या का समाधान नहीं करते हैं। आपको अधिक कोर देता है, आपके पास अभी भी केवल एक मेमोरी बस है।
हंस पसंत

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

4
सामान्य रूप से संबंधित विस्तृत जानकारी के साथ गहरी व्याख्या। +1
बाघिन

1
यदि आप ट्री के एक्सेस पैटर्न को जानते हैं, और यह 80/20 (80% एक्सेस हमेशा एक ही 20% नोड्स पर होता है) नियम का पालन करता है, तो एक सेल्फ एडजस्टिंग ट्री जैसे स्प्ले ट्री भी तेजी से साबित हो सकता है। en.wikipedia.org/wiki/Splay_tree
जेन्स टिमरमैन

10

मेमोरी कैश प्रभावों के बारे में हंस के महान जवाब को पूरक करने के लिए, मैं भौतिक मेमोरी ट्रांसलेशन और NUMA प्रभावों के लिए वर्चुअल मेमोरी की चर्चा जोड़ता हूं।

वर्चुअल मेमोरी कंप्यूटर (सभी वर्तमान कंप्यूटर) के साथ, मेमोरी एक्सेस करते समय, प्रत्येक वर्चुअल मेमोरी एड्रेस को फिजिकल मेमोरी एड्रेस में ट्रांसलेट करना होगा। यह ट्रांसलेशन टेबल का उपयोग करके मेमोरी मैनेजमेंट हार्डवेयर द्वारा किया जाता है। इस तालिका को प्रत्येक प्रक्रिया के लिए ऑपरेटिंग सिस्टम द्वारा प्रबंधित किया जाता है और इसे स्वयं रैम में संग्रहीत किया जाता है। वर्चुअल मेमोरी के प्रत्येक पृष्ठ के लिए , इस ट्रांसलेशन टेबल में एक एंट्री होती है जो वर्चुअल से फिजिकल पेज की मैपिंग करती है। मेमोरी एक्सेस के बारे में हंस की चर्चा याद रखें जो महंगे हैं: यदि प्रत्येक आभासी से भौतिक अनुवाद के लिए मेमोरी लुकअप की आवश्यकता होती है, तो सभी मेमोरी एक्सेस पर दोगुना खर्च होगा। समाधान के लिए अनुवाद तालिका के लिए एक कैश होना चाहिए जिसे ट्रांसलेशन लुकसाइड बफर कहा जाता है(संक्षेप के लिए TLB)। TLB बड़ी नहीं है (12 से 4096 प्रविष्टियाँ), और x86-64 आर्किटेक्चर पर विशिष्ट पृष्ठ का आकार केवल 4 KB है, जिसका अर्थ है कि टीएलबी हिट के साथ सीधे 16 एमबी तक पहुंच है (यह शायद उससे भी कम है, सैंडी पुल में 512 वस्तुओं का एक TLB आकार है )। टीएलबी की यादों की संख्या को कम करने के लिए, आपके पास ऑपरेटिंग सिस्टम और एप्लिकेशन काम हो सकता है 2 पेज जैसे बड़े आकार का उपयोग करने के लिए, जिससे टीएलबी हिट के साथ सुलभ एक बड़ा मेमोरी स्पेस हो सके। यह पृष्ठ समझाता है कि जावा के साथ बड़े पृष्ठों का उपयोग कैसे किया जाए जो मेमोरी एक्सेस को बहुत तेज़ कर सकता है

यदि आपके कंप्यूटर में कई सॉकेट हैं, तो यह शायद NUMA आर्किटेक्चर है। NUMA का अर्थ है गैर-वर्दी मेमोरी एक्सेस। इन आर्किटेक्चर में, कुछ मेमोरी एक्सेस दूसरों की तुलना में अधिक खर्च करते हैं। उदाहरण के लिए, 32 जीबी रैम के साथ 2 सॉकेट कंप्यूटर के साथ, प्रत्येक सॉकेट में संभवतः 16 जीबी रैम है। इस उदाहरण के कंप्यूटर पर, स्थानीय मेमोरी एक्सेस दूसरे सॉकेट की मेमोरी तक पहुंच से सस्ती है (रिमोट एक्सेस 20 से 100% धीमी है, शायद और भी अधिक)। यदि ऐसे कंप्यूटर पर, आपका पेड़ 20 GB RAM का उपयोग करता है, तो आपका कम से कम 4 GB डेटा अन्य NUMA नोड पर है, और यदि दूरस्थ मेमोरी के लिए एक्सेस 50% धीमा है, तो NUMA आपकी मेमोरी एक्सेस को 10% तक धीमा कर देता है। इसके अलावा, यदि आपके पास केवल एक NUMA नोड पर मुफ्त मेमोरी है, तो भूखे नोड पर मेमोरी की आवश्यकता वाली सभी प्रक्रियाओं को अन्य नोड से मेमोरी आवंटित की जाएगी जो एक्सेस अधिक महंगी हैं। सबसे खराब स्थिति में भी, ऑपरेटिंग सिस्टम यह सोच सकता है कि भूखे नोड की स्मृति के एक हिस्से को स्वैप करना एक अच्छा विचार है,जो और भी महंगी मेमोरी एक्सेस का कारण होगा । यह MySQL "स्वैप पागलपन" समस्या और NUMA आर्किटेक्चर के प्रभावों के बारे में अधिक जानकारी में बताया गया है, जहां लिनक्स के लिए कुछ समाधान दिए गए हैं (सभी NUMA नोड्स पर मेमोरी एक्सेस को फैलाना, स्वैपिंग से बचने के लिए रिमोट NUM एक्सेस पर बुलेट को काटते हुए)। मैं एक सॉकेट (16 और 16 जीबी के बजाय 24 और 8 जीबी) को अधिक रैम आवंटित करने के बारे में सोच सकता हूं और यह सुनिश्चित कर सकता हूं कि आपका प्रोग्राम बड़े एनयूएमए नोड पर शेड्यूल किया गया है, लेकिन इसके लिए कंप्यूटर और एक पेचकश ;-) तक भौतिक पहुंच की आवश्यकता है; ।


4

यह प्रति उत्तर नहीं है, बल्कि इस बात पर जोर है कि हंस पसंत ने स्मृति प्रणाली में देरी के बारे में क्या लिखा है।

वास्तव में उच्च प्रदर्शन सॉफ्टवेयर - जैसे कि कंप्यूटर गेम - केवल खेल को लागू करने के लिए ही नहीं लिखा गया है, यह भी इस तरह के रूप में अनुकूलित है कि कोड और डेटा संरचनाएं कैश और मेमोरी सिस्टम का सबसे अधिक उपयोग करती हैं अर्थात उन्हें सीमित संसाधन के रूप में मानती हैं। जब मैं कैश मुद्दों से निपटता हूं तो मैं आमतौर पर यह मानता हूं कि यदि डेटा वहां मौजूद है तो L1 3 चक्रों में वितरित करेगा। अगर यह नहीं है और मुझे L2 में जाना है तो मैं 10 चक्र लगाता हूं। L3 30 चक्र के लिए और RAM मेमोरी 100 के लिए।

एक अतिरिक्त मेमोरी-संबंधित कार्रवाई है - यदि आपको इसका उपयोग करने की आवश्यकता है - एक भी बड़ा जुर्माना लगाता है और यह एक बस लॉक है। यदि आप Windows NT कार्यक्षमता का उपयोग करते हैं तो बस लॉक को महत्वपूर्ण खंड कहा जाता है। यदि आप एक घरेलू किस्म का उपयोग करते हैं, तो आप इसे पालक कह सकते हैं। जो भी नाम है वह सिस्टम में सबसे धीमी गति से चलने वाले बस-मास्टर डिवाइस को लॉक करने से पहले सिंक्रनाइज़ करता है। सबसे धीमी गति से चलने वाली बस-माहिर डिवाइस @ 33MHz से जुड़ा एक क्लासिक 32-बिट PCI कार्ड हो सकता है। 33MHz एक विशिष्ट x86 CPU (@ 3.3 GHz) की आवृत्ति का सौवां हिस्सा है। मुझे लगता है कि एक बस लॉक को पूरा करने के लिए 300 से कम चक्र नहीं हैं, लेकिन मुझे पता है कि वे कई बार ले सकते हैं ताकि अगर मैं 3000 चक्र देखूं तो मुझे आश्चर्य नहीं होगा।

नौसिखिया मल्टी-थ्रेडिंग सॉफ्टवेयर डेवलपर्स सभी जगह बस लॉक का उपयोग करेंगे और फिर आश्चर्य करेंगे कि उनका कोड धीमा क्यों है। चाल - सब कुछ है कि स्मृति के साथ क्या करना है - तक पहुँच को कम करना है।

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