Microsoft के आंतरिक प्रायोरिटी में बग <T> है?


82

PresentationCore.dll में .NET फ्रेमवर्क में, एक सामान्य PriorityQueue<T>वर्ग है जिसका कोड यहां पाया जा सकता है

मैंने छँटाई का परीक्षण करने के लिए एक छोटा कार्यक्रम लिखा, और परिणाम बहुत अच्छे नहीं थे:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using MS.Internal;

namespace ConsoleTest {
    public static class ConsoleTest {
        public static void Main() {
            PriorityQueue<int> values = new PriorityQueue<int>(6, Comparer<int>.Default);
            Random random = new Random(88);
            for (int i = 0; i < 6; i++)
                values.Push(random.Next(0, 10000000));
            int lastValue = int.MinValue;
            int temp;
            while (values.Count != 0) {
                temp = values.Top;
                values.Pop();
                if (temp >= lastValue)
                    lastValue = temp;
                else
                    Console.WriteLine("found sorting error");
                Console.WriteLine(temp);
            }
            Console.ReadLine();
        }
    }
}

परिणाम:

2789658
3411390
4618917
6996709
found sorting error
6381637
9367782

एक सॉर्टिंग त्रुटि है, और यदि नमूना आकार में वृद्धि हुई है, तो सॉर्टिंग त्रुटियों की संख्या कुछ आनुपातिक रूप से बढ़ जाती है।

क्या मैंने कुछ गलत किया? यदि नहीं, तो PriorityQueueकक्षा के कोड में बग कहाँ स्थित है?


3
स्रोत कोड में टिप्पणियों के अनुसार, Microsoft 2005-02-14 से इस कोड का उपयोग कर रहा है। मुझे आश्चर्य है कि इस तरह एक बग 12 साल से अधिक के लिए कैसे बच गया?
नेट

9
@ कोई भी स्थान नहीं है क्योंकि केवल Microsoft ही इसका उपयोग करता है और यहाँ फ़ॉन्ट प्राथमिकता को कम प्राथमिकता देता है जो कि कुछ समय नोटिस करने के लिए एक कठिन बग है।
स्कॉट चैंबरलेन

जवाबों:


84

आरंभीकरण वेक्टर का उपयोग करके व्यवहार को पुन: पेश किया जा सकता है [0, 1, 2, 4, 5, 3]। परिणाम है:

[०, १, २, ४, ३, ५]

(हम देख सकते हैं कि 3 को गलत तरीके से रखा गया है)

Pushएल्गोरिथ्म सही है। यह एक सीधा तरीके से एक मिनट-ढेर बनाता है:

  • नीचे से शुरू करो
  • यदि मान मूल नोड से अधिक है, तो उसे डालें और वापस लौटें
  • अन्यथा, माता-पिता के बजाय नीचे दाईं ओर रखें, फिर मूल स्थान पर मान डालने का प्रयास करें (और जब तक सही जगह न मिल जाए, तब तक पेड़ की अदला-बदली करते रहें)

परिणामी पेड़ है:

                 0
               /   \
              /     \
             1       2
           /  \     /
          4    5   3

मुद्दा Popविधि के साथ है । इसे भरने के लिए शीर्ष नोड को "गैप" मानकर शुरू होता है (क्योंकि हमने इसे पॉप किया है):

                 *
               /   \
              /     \
             1       2
           /  \     /
          4    5   3

इसे भरने के लिए, यह सबसे कम तात्कालिक बच्चे की खोज करता है (इस मामले में: 1)। इसके बाद अंतर को भरने के लिए मूल्य बढ़ जाता है (और बच्चा अब नया अंतर है):

                 1
               /   \
              /     \
             *       2
           /  \     /
          4    5   3

यह फिर नए अंतर के साथ सटीक काम करता है, इसलिए अंतराल फिर से नीचे चला जाता है:

                 1
               /   \
              /     \
             4       2
           /  \     /
          *    5   3

जब अंतर नीचे तक पहुंच गया है, तो एल्गोरिथ्म ... पेड़ के नीचे-सबसे सही मूल्य लेता है और अंतराल को भरने के लिए इसका उपयोग करता है:

                 1
               /   \
              /     \
             4       2
           /  \     /
          3    5   *

अब यह अंतर सबसे निचले नोड पर है, यह _countपेड़ से खाई को हटाने के लिए घटता है:

                 1
               /   \
              /     \
             4       2
           /  \     
          3    5   

और हम अंत में ... एक टूटे हुए ढेर।

पूरी तरह से ईमानदार होने के लिए, मुझे समझ नहीं आता कि लेखक क्या करना चाह रहा था, इसलिए मैं मौजूदा कोड को ठीक नहीं कर सकता। अधिक से अधिक, मैं इसे एक कार्यशील संस्करण के साथ स्वैप कर सकता हूं (बेशर्मी से विकिपीडिया से कॉपी किया गया ):

internal void Pop2()
{
    if (_count > 0)
    {
        _count--;
        _heap[0] = _heap[_count];

        Heapify(0);
    }
}

internal void Heapify(int i)
{
    int left = (2 * i) + 1;
    int right = left + 1;
    int smallest = i;

    if (left <= _count && _comparer.Compare(_heap[left], _heap[smallest]) < 0)
    {
        smallest = left;
    }

    if (right <= _count && _comparer.Compare(_heap[right], _heap[smallest]) < 0)
    {
        smallest = right;
    }

    if (smallest != i)
    {
        var pivot = _heap[i];
        _heap[i] = _heap[smallest];
        _heap[smallest] = pivot;

        Heapify(smallest);
    }
}

उस कोड के साथ मुख्य मुद्दा पुनरावर्ती कार्यान्वयन है, जो तत्वों की संख्या बहुत बड़ी होने पर टूट जाएगा। मैं इसके बजाय एक अनुकूलित तीसरे पक्ष के पुस्तकालय का उपयोग करने की दृढ़ता से सलाह देता हूं।


संपादित करें: मुझे लगता है कि मुझे पता चला है कि क्या गायब है। निचले-दाएं नोड को लेने के बाद, लेखक ने ढेर को फिर से असंतुलित करना भूल गया:

internal void Pop()
{
    Debug.Assert(_count != 0);

    if (_count > 1)
    {
        // Loop invariants:
        //
        //  1.  parent is the index of a gap in the logical tree
        //  2.  leftChild is
        //      (a) the index of parent's left child if it has one, or
        //      (b) a value >= _count if parent is a leaf node
        //
        int parent = 0;
        int leftChild = HeapLeftChild(parent);

        while (leftChild < _count)
        {
            int rightChild = HeapRightFromLeft(leftChild);
            int bestChild =
                (rightChild < _count && _comparer.Compare(_heap[rightChild], _heap[leftChild]) < 0) ?
                    rightChild : leftChild;

            // Promote bestChild to fill the gap left by parent.
            _heap[parent] = _heap[bestChild];

            // Restore invariants, i.e., let parent point to the gap.
            parent = bestChild;
            leftChild = HeapLeftChild(parent);
        }

        // Fill the last gap by moving the last (i.e., bottom-rightmost) node.
        _heap[parent] = _heap[_count - 1];

        // FIX: Rebalance the heap
        int index = parent;
        var value = _heap[parent];

        while (index > 0)
        {
            int parentIndex = HeapParent(index);
            if (_comparer.Compare(value, _heap[parentIndex]) < 0)
            {
                // value is a better match than the parent node so exchange
                // places to preserve the "heap" property.
                var pivot = _heap[index];
                _heap[index] = _heap[parentIndex];
                _heap[parentIndex] = pivot;
                index = parentIndex;
            }
            else
            {
                // Heap is balanced
                break;
            }
        }
    }

    _count--;
}

4
'एल्गोरिदमिक त्रुटि' यह है कि आपको अंतर को नीचे नहीं ले जाना चाहिए, लेकिन पहले पेड़ को सिकोड़ें और उस अंतराल में नीचे-सही तत्व डालें। फिर एक साधारण पुनरावृत्ति लूप में पेड़ की मरम्मत करें।
हेनक होल्टरमैन

5
बग रिपोर्ट के लिए यह अच्छी सामग्री है, आपको इसे इस पोस्ट के लिंक के साथ रिपोर्ट करना चाहिए (मुझे लगता है कि सही जगह एमएस कनेक्ट पर होगी क्योंकि प्रेजेंटकोर गोरिट पर नहीं है)।
लुकास ट्रेज़न्यूस्की

4
@LucasTrzesniewski मैं एक वास्तविक दुनिया के आवेदन पर प्रभाव के बारे में निश्चित नहीं हूँ (क्योंकि यह केवल WPF में कुछ अस्पष्ट फ़ॉन्ट-चयन कोड के लिए उपयोग किया जाता है), लेकिन मुझे लगता है कि यह रिपोर्ट करने के लिए इसे चोट नहीं पहुंचा सकता है
केविन गोसेज़

20

केविन गोसे का जवाब समस्या की पहचान करता है। यद्यपि उसके ढेर का फिर से संतुलन काम करेगा, लेकिन मूल हटाने के लूप में मूलभूत समस्या को ठीक करने पर यह आवश्यक नहीं है।

जैसा कि उन्होंने बताया, विचार यह है कि आइटम को सबसे कम, सही-सबसे अधिक आइटम के साथ ढेर के शीर्ष पर प्रतिस्थापित किया जाए, और फिर उसे उचित स्थान पर भेज दिया जाए। यह मूल लूप का एक सरल संशोधन है:

internal void Pop()
{
    Debug.Assert(_count != 0);

    if (_count > 0)
    {
        --_count;
        // Logically, we're moving the last item (lowest, right-most)
        // to the root and then sifting it down.
        int ix = 0;
        while (ix < _count/2)
        {
            // find the smallest child
            int smallestChild = HeapLeftChild(ix);
            int rightChild = HeapRightFromLeft(smallestChild);
            if (rightChild < _count-1 && _comparer.Compare(_heap[rightChild], _heap[smallestChild]) < 0)
            {
                smallestChild = rightChild;
            }

            // If the item is less than or equal to the smallest child item,
            // then we're done.
            if (_comparer.Compare(_heap[_count], _heap[smallestChild]) <= 0)
            {
                break;
            }

            // Otherwise, move the child up
            _heap[ix] = _heap[smallestChild];

            // and adjust the index
            ix = smallestChild;
        }
        // Place the item where it belongs
        _heap[ix] = _heap[_count];
        // and clear the position it used to occupy
        _heap[_count] = default(T);
    }
}

यह भी ध्यान दें कि लिखे गए कोड में मेमोरी लीक है। कोड के इस बिट:

        // Fill the last gap by moving the last (i.e., bottom-rightmost) node.
        _heap[parent] = _heap[_count - 1];

से मूल्य स्पष्ट नहीं करता है _heap[_count - 1]। यदि ढेर संदर्भ प्रकारों को संग्रहीत कर रहा है, तो संदर्भ हीप में रहते हैं और तब तक कचरा एकत्र नहीं किया जा सकता है जब तक कि ढेर के लिए मेमोरी एकत्र नहीं की जाती है। मुझे नहीं पता कि यह ढेर कहाँ उपयोग किया जाता है, लेकिन यदि यह बड़ा है और किसी भी महत्वपूर्ण समय के लिए रहता है, तो यह अतिरिक्त मेमोरी खपत का कारण बन सकता है। उत्तर कॉपी किए जाने के बाद आइटम को साफ़ करना है:

_heap[_count - 1] = default(T);

मेरे प्रतिस्थापन कोड में वह फिक्स शामिल है।


1
मेरे द्वारा परीक्षण किए गए एक बेंचमार्क में (पास्टबिन.com/Hgkcq3ex पर पाया जा सकता है), यह संस्करण केविन गोसे द्वारा प्रस्तावित एक की तुलना में लगभग ~ 18% धीमा है (भले ही डिफ़ॉल्ट को स्पष्ट) लाइन हटा दी गई हो और _count/2गणना बाहर फहरा दी गई हो सूचित करते रहना)।
मथुसम मट

@MathuSumMut: मैंने एक अनुकूलित संस्करण प्रदान किया है। आइटम को जगह देने और उसे लगातार स्वैप करने के बजाय, मैं केवल जगह में आइटम के साथ तुलना करता हूं। यह लिखने की संख्या को कम करता है, इसलिए गति को बढ़ाना चाहिए। एक अन्य संभावित अनुकूलन _heap[_count]एक अस्थायी पर कॉपी करना होगा, जो सरणी संदर्भों की संख्या को कम करेगा।
जिम मिसल

दुर्भाग्य से मैंने इसे आज़माया है और लगता है कि यह एक बग भी है। प्रकार int की एक कतार सेट करें, और इस कस्टम तुलनित्र का उपयोग करें: Comparer<int>.Create((i1, i2) => -i1.CompareTo(i2))- अर्थात्, इसे कम से कम करने के लिए सबसे बड़ा क्रमबद्ध (नकारात्मक संकेत नोट करें)। क्रम में धकेलने के बाद: 3, 1, 5, 0, 4, और फिर उन सभी को हटाते हुए, वापसी क्रम था: {5,4,1,3,0}, इसलिए ज्यादातर अभी भी हल किया गया, लेकिन 1 और 3 गलत क्रम में हैं। ऊपर गोसे की विधि का उपयोग करने से यह समस्या नहीं हुई। ध्यान दें कि मुझे सामान्य, आरोही क्रम में यह समस्या नहीं थी।
निकोलस पीटरसन

1
@ नाइकोलासपेटर्सन: दिलचस्प। मुझे उस पर गौर करना होगा। नोट के लिए धन्यवाद।
जिम मेंथल

2
@ JimMischel कोड में बग: तुलना rightChild < _count-1होनी चाहिए rightChild < _count। यह केवल तभी मायने रखता है जब गिनती को 2 की सटीक शक्ति से कम किया जाता है, और केवल तभी जब अंतर पेड़ के दाहिने किनारे से नीचे तक जाता है। सबसे निचले भाग में, दाहिने हाथ की तुलना उसके बाएं भाई-बहन से नहीं की जाती है, और गलत तत्व को बढ़ावा दिया जा सकता है, जिससे ढेर टूट जाता है। जितना बड़ा पेड़ होगा, उतनी ही कम संभावना होगी; 4 से 3 तक की संख्या को कम करते समय यह दिखाने की सबसे अधिक संभावना है, जो निकोलस पीटरसन के "अंतिम युगल आइटम" के बारे में बताते हैं।
सैम बेंट -

0

.NET फ्रेमवर्क 4.8 में प्रतिलिपि प्रस्तुत करने योग्य नहीं है

2020 में इस मुद्दे को पुन: पेश करने की कोशिश की जा रही है। .NET फ्रेमवर्क 4.8 के साथ PriorityQueue<T>निम्नलिखित XUnitपरीक्षण के उपयोग से जुड़ा हुआ है ...

public class PriorityQueueTests
{
    [Fact]
    public void PriorityQueueTest()
    {
        Random random = new Random();
        // Run 1 million tests:
        for (int i = 0; i < 1000000; i++)
        {
            // Initialize PriorityQueue with default size of 20 using default comparer.
            PriorityQueue<int> priorityQueue = new PriorityQueue<int>(20, Comparer<int>.Default);
            // Using 200 entries per priority queue ensures possible edge cases with duplicate entries...
            for (int j = 0; j < 200; j++)
            {
                // Populate queue with test data
                priorityQueue.Push(random.Next(0, 100));
            }
            int prev = -1;
            while (priorityQueue.Count > 0)
            {
                // Assert that previous element is less than or equal to current element...
                Assert.True(prev <= priorityQueue.Top);
                prev = priorityQueue.Top;
                // remove top element
                priorityQueue.Pop();
            }
        }
    }
}

... सभी 1 मिलियन परीक्षण मामलों में सफल:

यहाँ छवि विवरण दर्ज करें

तो ऐसा लगता है जैसे Microsoft ने उनके कार्यान्वयन में बग को ठीक कर दिया है:

internal void Pop()
{
    Debug.Assert(_count != 0);
    if (!_isHeap)
    {
        Heapify();
    }

    if (_count > 0)
    {
        --_count;

        // discarding the root creates a gap at position 0.  We fill the
        // gap with the item x from the last position, after first sifting
        // the gap to a position where inserting x will maintain the
        // heap property.  This is done in two phases - SiftDown and SiftUp.
        //
        // The one-phase method found in many textbooks does 2 comparisons
        // per level, while this method does only 1.  The one-phase method
        // examines fewer levels than the two-phase method, but it does
        // more comparisons unless x ends up in the top 2/3 of the tree.
        // That accounts for only n^(2/3) items, and x is even more likely
        // to end up near the bottom since it came from the bottom in the
        // first place.  Overall, the two-phase method is noticeably better.

        T x = _heap[_count];        // lift item x out from the last position
        int index = SiftDown(0);    // sift the gap at the root down to the bottom
        SiftUp(index, ref x, 0);    // sift the gap up, and insert x in its rightful position
        _heap[_count] = default(T); // don't leak x
    }
}

जैसा कि प्रश्नों में लिंक केवल Microsoft के स्रोत कोड (वर्तमान में .NET फ्रेमवर्क 4.8) के सबसे हाल के संस्करण की ओर इशारा करता है, यह कहना मुश्किल है कि कोड में वास्तव में क्या बदला गया था, लेकिन सबसे विशेष रूप से अब एक स्पष्ट टिप्पणी है कि मेमोरी को लीक करें, इसलिए हम कर सकते हैं मान लें कि जिममिशेल के उत्तर में उल्लिखित स्मृति रिसाव को संबोधित कर दिया गया है, जिसकी पुष्टि विजुअल स्टूडियो डायग्नोस्टिक टूल का उपयोग करके की जा सकती है:

यहाँ छवि विवरण दर्ज करें

अगर कोई मेमोरी लीक होती तो हम लाख Pop()ऑपरेशन के बाद कुछ बदलाव देख सकते थे ...

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