2012-08-26 16 views
5

में एक सरणी को विभाजित करने का सबसे तेज़ (पोर्टेबल) तरीका मैं पूरी तरह से प्रबंधित Mercurial लाइब्रेरी लिख रहा हूं (पूरी तरह से प्रबंधित Mercurial Server for Windows में जल्द ही उपयोग किया जा रहा है), और सबसे गंभीर प्रदर्शन समस्याओं में से एक भागों में एक सरणी विभाजित, आश्चर्यजनक रूप से पर्याप्त है।सी #

विचार इस प्रकार है: कई सौ बाइट्स से लेकर मेगाबाइट तक के आकार के साथ एक बाइट सरणी है और मुझे इसके साथ करने की ज़रूरत है, इसे मेरे विशिष्ट मामले में \n वर्णों से विभाजित भागों में विभाजित करना है ।

अब क्या dotTrace मुझे पता चलता है कि मेरी "optimized" version की Split 2,300 कॉल के लिए 11 सेकंड तक ले जाता है (वहाँ एक स्पष्ट प्रदर्शन हिट dotTrace से ही शुरू की है, लेकिन (कोड सही, here's the naive संस्करण मैं के साथ शुरू हुआ है) सब कुछ है पैमाने तक)।

यहाँ नंबर दिए गए हैं:

  • unsafe संस्करण: 2 312 कॉल

तो यहाँ के लिए 20 001 एमएस चला जाता है: 2 312 कॉल

  • कामयाब ("अनुभवहीन") संस्करण के लिए 11 297 एमएस क्या सबसे तेज़ (अधिमानतः पोर्टेबल, अर्थात् x86 और x64 दोनों का समर्थन करना होगा) सी # में एक सरणी को विभाजित करने का तरीका होगा।

  • +3

    हम अपने * "अनुकूलित" स्प्लिट * के संस्करण देख सकते हैं ताकि हम इसमें कैसे सुधार करने के लिए के बारे में सोच सकते हैं? –

    +0

    पोर्टेबल पोर्टेबल क्लास लाइब्रेरी के अर्थ में पोर्टेबल जैसा वर्णन किया गया है: http://msdn.microsoft.com/en-us/library/gg597391.aspx? –

    +2

    आप अपना कोड '" अनुकूलित "संस्करण' लिंक में पा सकते हैं। –

    उत्तर

    3

    स्प्लिट के लिए, 32-बिट मशीन पर उलंग को संभालना वास्तव में धीमा है, इसलिए निश्चित रूप से कम हो जाता है। यदि आप वास्तव में उलझन चाहते हैं, तो दो संस्करणों को लागू करें, 32-बिट के लिए एक, 64-बिट के लिए एक।

    आपको यह भी मापना चाहिए कि एक समय में बाइट को संभालना तेज है या नहीं।

    स्मृति आवंटन की लागत को प्रोफ़ाइल करने की आवश्यकता है। यदि यह काफी बड़ा है, तो कई कॉलों में स्मृति का पुन: उपयोग करने का प्रयास करें।

    अन्य:

    ToString: यह "(" + Offset.ToString() + "," + Length.ToString() + ")" का उपयोग करने के लिए तेजी से है,

    GetHashCode: कोशिश निश्चित (बाइट * ख = & बफर [ऑफसेट])


    इस संस्करण वास्तव में तेजी से होना चाहिए, अगर कई बार इस्तेमाल किया। मुख्य बिंदु: आंतरिक सरणी के बाद सही आकार में विस्तारित होने के बाद कोई नई स्मृति आवंटन, न्यूनतम डेटा प्रति।

    class ArraySplitter 
    { 
        private byte[] m_data; 
        private int m_count; 
        private int[] m_stops; 
    
        private void AddRange(int start, int stop) 
        { 
         // Skip empty range 
         if (start > stop) 
         { 
          return; 
         } 
    
         // Grow array if needed 
         if ((m_stops == null) || (m_stops.Length < (m_count + 2))) 
         { 
          int[] old = m_stops; 
    
          m_stops = new int[m_count * 3/2 + 4]; 
    
          if (old != null) 
          { 
           old.CopyTo(m_stops, 0); 
          } 
         } 
    
         m_stops[m_count++] = start; 
         m_stops[m_count++] = stop; 
        } 
    
        public int Split(byte[] data, byte sep) 
        { 
         m_data = data; 
         m_count = 0;  // reuse m_stops 
    
         int last = 0; 
    
         for (int i = 0; i < data.Length; i ++) 
         { 
          if (data[i] == sep) 
          { 
           AddRange(last, i - 1); 
           last = i + 1; 
          } 
         } 
    
         AddRange(last, data.Length - 1); 
    
         return m_count/2; 
        } 
    
        public ArraySegment<byte> this[int index] 
        { 
         get 
         { 
          index *= 2; 
          int start = m_stops[index]; 
    
          return new ArraySegment<byte>(m_data, start, m_stops[index + 1] - start + 1); 
         } 
        } 
    } 
    

    टेस्ट कार्यक्रम:

    static void Main(string[] args) 
        { 
         int count = 1000 * 1000; 
    
         byte[] data = new byte[count]; 
    
         for (int i = 0; i < count; i++) 
         { 
          data[i] = (byte) i; 
         } 
    
         Stopwatch watch = new Stopwatch(); 
    
         for (int r = 0; r < 10; r++) 
         { 
          watch.Reset(); 
          watch.Start(); 
    
          int len = 0; 
    
          foreach (var seg in data.MySplit(13)) 
          { 
           len += seg.Count; 
          } 
    
          watch.Stop(); 
    
          Console.WriteLine("MySplit  : {0} {1,8:N3} ms", len, watch.Elapsed.TotalMilliseconds); 
    
          watch.Reset(); 
          watch.Start(); 
    
          ArraySplitter splitter = new ArraySplitter(); 
    
          int parts = splitter.Split(data, 13); 
    
          len = 0; 
    
          for (int i = 0; i < parts; i++) 
          { 
           len += splitter[i].Count; 
          } 
    
          watch.Stop(); 
          Console.WriteLine("ArraySplitter: {0} {1,8:N3} ms", len, watch.Elapsed.TotalMilliseconds); 
         } 
        } 
    

    परिणाम:

    MySplit  : 996093 9.514 ms 
    ArraySplitter: 996093 4.754 ms 
    MySplit  : 996093 7.760 ms 
    ArraySplitter: 996093 2.710 ms 
    MySplit  : 996093 8.391 ms 
    ArraySplitter: 996093 3.510 ms 
    MySplit  : 996093 9.677 ms 
    ArraySplitter: 996093 3.468 ms 
    MySplit  : 996093 9.685 ms 
    ArraySplitter: 996093 3.370 ms 
    MySplit  : 996093 9.700 ms 
    ArraySplitter: 996093 3.425 ms 
    MySplit  : 996093 9.669 ms 
    ArraySplitter: 996093 3.519 ms 
    MySplit  : 996093 9.844 ms 
    ArraySplitter: 996093 3.416 ms 
    MySplit  : 996093 9.721 ms 
    ArraySplitter: 996093 3.685 ms 
    MySplit  : 996093 9.703 ms 
    ArraySplitter: 996093 3.470 ms 
    
    +0

    एक और बात: आपके आंतरिक पाश में बहुत अधिक चर हैं। –

    +0

    परीक्षण डेटा में कितने अलग सेगमेंट हैं? –

    +0

    1000 * 1000/256 = 3906 –

    4

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

    साथ ही, यह बताना मुश्किल है कि आप किस प्रकार के डेटा के साथ अपने परीक्षण चलाते हैं, इसलिए यह केवल अनुमान है।

    public static unsafe Segment[] Split2(byte[] _src, byte value) 
    { 
        var _ln = _src.Length; 
    
        if (_ln == 0) return new Segment[] { }; 
    
        fixed (byte* src = _src) 
        { 
         var segments = new LinkedList<Segment>(); // Segment[c]; 
    
         byte* last = src; 
         byte* end = src + _ln - 1; 
         byte lastValue = *end; 
         *end = value; // value-termination 
    
         var cur = src; 
         while (true) 
         { 
          if (*cur == value) 
          { 
           int begin = (int) (last - src); 
           int length = (int) (cur - last + 1); 
           segments.AddLast(new Segment(_src, begin, length)); 
    
           last = cur + 1; 
    
           if (cur == end) 
           { 
            if (lastValue != value) 
            { 
             *end = lastValue; 
            } 
            break; 
           } 
          } 
          cur++; 
         } 
    
         return segments.ToArray(); 
        } 
    } 
    

    संपादित: फिक्स्ड कोड, तो यह सही परिणाम देता है।

    +0

    यह सेंटीनेल का एक बहुत ही मुश्किल/चालाक उपयोग है .. हालांकि, केवल एक सशर्त/बाइट का रेडक्स .. समय देखना अच्छा लगेगा। –

    +0

    अच्छा! डॉटट्रेस के तहत '2 312' कॉल के लिए' 4 696' एमएस। –

    +0

    @pst: यह मेरे 'असुरक्षित' संस्करण से लगभग तीन गुना तेज है। –

    2

    एंटोन,

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

    मूल तर्क को फिर से लिखने के अलावा, मैंने आपके कस्टम कार्यान्वयन के बजाय फ्रेमवर्क में निर्मित मूल ArraySegment<> का उपयोग करने का निर्णय लिया। केवल महत्वपूर्ण अंतर यह है कि ArraySegment<> संपत्ति के बजाय Count संपत्ति का खुलासा करता है। नीचे दिए गए कोड को असुरक्षित कीवर्ड की आवश्यकता नहीं है क्योंकि मैं पॉइंटर्स का उपयोग नहीं कर रहा हूं, हालांकि ऐसा करने के लिए बदले में थोड़ा प्रदर्शन सुधार प्रतीत होता है।


    public static ArraySegment<byte>[] SplitEx(this byte[] source, byte value) { 
         var _previousIndex = -1; 
         var _segments = new List<ArraySegment<byte>>(); 
         var _length = source.Length; 
    
         if (_length > 0) { 
          int _index; 
    
          for (_index = 0; _index < _length; _index++) { 
           var _value = source[_index]; 
           if (_value == value) { 
            _segments.Add(new ArraySegment<byte>(source, _previousIndex + 1, _index - _previousIndex)); 
            _previousIndex = _index; 
           } 
          } 
    
          if (--_index != _previousIndex) { 
           _segments.Add(new ArraySegment<byte>(source, _previousIndex + 1, _index - _previousIndex)); 
          } 
         } 
    
         return _segments.ToArray(); 
        }