Powrót do strony głównej

Kopce i kolejki priorytetowe: optymalizacja wydajności

Artykuł wyjaśnia, dlaczego implementacje kolejek priorytetowych oparte na kopcach binarnych i d-arnych przechowywanych w tablicach są znacznie szybsze niż podobne struktury oparte na drzewach z wskaźnikami. Omawiane są zasady działania, optymalizacja poprzez zwiększenie współczynnika rozgałęzienia oraz praktyczne przykłady zastosowania w systemach czasu rzeczywistego.

Kopce vs drzewa: sekret szybkości kolejek priorytetowych
Advertisement 728x90

Kopce binarne i d-arny: optymalizacja kolejek priorytetowych pod kątem wydajności i lokalności pamięci podręcznej

Podczas projektowania systemów wysokiej wydajności, takich jak planistyczne zadania w czasie rzeczywistym, wybór struktury danych dla kolejki priorytetowej ma kluczowe znaczenie dla końcowej szybkości działania. Spory między stosowaniem czerwono-czarnych drzew a kopców często rozstrzygane są nie teorią, ale benchmarkami praktycznymi, które pokazują różnicę w wydajności nawet kilkukrotną. Główną przyczyną zwycięstwa kopca jest jego fundamentalna cecha: przechowywany jest w tablicy, zapewniając doskonałą lokalność danych w pamięci podręcznej procesora.

Dlaczego kopce w tablicach są szybsze niż drzewa

Kopiec binarny to pełne drzewo binarne, które można skompaktować w zwykłej tablicy bez użycia wskaźników. Dla węzła o indeksie i jego rodzic znajduje się według wzoru (i - 1) / 2, a lewy i prawy potomek — według wzorów 2i + 1 i 2i + 2. Ta arytmetyka indeksów pozwala zrealizować podstawowe operacje:

  • Wstawianie (O(log n)): element dodawany jest na koniec tablicy, następnie wykonuje się "podnoszenie" (heapify-up), porównując go z rodzicami, aż do przywrócenia własności kopca.
  • Wydobycie maksimum (O(log n)): element korzeniowy (maksimum) usuwany, ostatni element tablicy przenoszony do korzenia, następnie wykonuje się "opuszczenie" (heapify-down).
  • Pobranie maksimum (O(1)): po prostu odczyt pierwszego elementu tablicy.

Główną zaletą tej implementacji jest fakt, że wszystkie dane znajdują się w ciągłym bloku pamięci. Podczas operacji podnoszenia i opuszczania procesor odwołuje się do sąsiednich lub bliskich elementów tablicy, które z dużym prawdopodobieństwem już znajdują się w pamięci podręcznej. W strukturach opartych na wskaźnikach, takich jak czerwono-czarne drzewa, każdy dostęp wymaga odwołania do nowej, potencjalnie oddalonej pamięci, co prowadzi do częstych braków pamięci podręcznej. Wyniki testów pokazują, że dla obciążenia z 10 000 zadań kopiec wykonuje operacje cztery razy szybciej i z czterokrotnie mniejszą liczbą braków pamięci podręcznej.

Google AdInline article slot
// Przykład implementacji operacji opuszczania (heapify-down) w max-heap
void heapify_down(int *heap, int size, int i) {
    while (2 * i + 1 < size) { // dopóki istnieje lewy potomek
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int largest = i;
        if (left < size && heap[left] > heap[largest]) largest = left;
        if (right < size && heap[right] > heap[largest]) largest = right;
        if (largest == i) break; // własność kopca została przywrócona
        // Zamiana z największym potomkiem
        int temp = heap[i];
        heap[i] = heap[largest];
        heap[largest] = temp;
        i = largest;
    }
}

Optymalizacja poprzez kopce d-arny

Z rosnącym rozmiarem kopca wysokość drzewa binarnego rośnie, a operacje podnoszenia/opuszczania mogą przejść przez wiele poziomów, każdy z których może znajdować się w nowej linii pamięci podręcznej. Dla kopca z milionem elementów wysokość wynosi około 20 poziomów, co potencjalnie prowadzi do 20 braków pamięci podręcznej podczas opuszczania elementu z korzenia.

Kopiec d-arny rozwiązuje ten problem zwiększając liczbę potomków każdego węzła z 2 do d. To zmniejsza wysokość drzewa do log_d(n). Na przykład dla kopca 4-arnego wysokość zmniejsza się dwukrotnie, a dla 8-arnego — około trzykrotnie. Arytmetyka indeksów dostosowana jest następująco:

  • Rodzic węzła i: (i - 1) / d
  • Pierwszy potomek węzła i: d * i + 1
  • Ostatni potomek węzła i: d * i + d

Zaletą jest zwiększenie liczby porównań na jednym poziomie (należy znaleźć maksimum wśród d potomków zamiast 2), ale często to kompensowane jest zmniejszeniem liczby braków pamięci podręcznej. Testy praktyczne dla różnych wartości d pokazują optymalny wynik dla większości przypadków przy d=8. Kopiec 8-arny demonstruje trzykrotny spadek liczby braków pamięci podręcznej i wzrost szybkości operacji o około 70% w porównaniu z kopcem binarnym.

Google AdInline article slot

Porównanie struktur dla planistów zadań

Mimo zalet kopca, w skomplikowanych planistach, takich jak Completely Fair Scheduler (CFS) jądra Linux, stosuje się czerwono-czarne drzewa. Powodem jest szersze spektrum wymagań:

  • Zapytania dotyczące zakresów priorytetów.
  • Usunięcie dowolnego zadania (nie tylko korzeniowego).
  • Obsługa złożonych algorytmów sprawiedliwego planowania.

Kopce nie są efektywne dla tych operacji, ponieważ losowy dostęp i usuwanie wymagają czasu O(n). Jednak dla prostszych, ale wymagających wydajności systemów, takich jak planistyczne w czasie rzeczywistym (RTOS), kopce są idealnym wyborem.

Zastosowania praktyczne i ograniczenia

Kopce to specjalizowany narzędzie. Powinny być używane do:

Google AdInline article slot
  • Kolejek priorytetowych w planistach, systemach przetwarzania zdarzeń, algorytmach Dijkstry czy Huffmana.
  • Rozwiązania zadania Top-K (znalezienie K największych elementów w strumieniu) w czasie O(n log k).
  • Stałe obliczanie mediany strumienia danych przy użyciu dwóch kopców (max-heap i min-heap).

Kopce nie nadają się do zadań wymagających:

  • Dowolnego usuwania elementów.
  • Szukania konkretnego elementu.
  • Zapytań dotyczących zakresów wartości.
  • Iteracji po danych w pełni posortowanym porządku.

Dla takich scenariuszy należy wybrać zrównoważone drzewa wyszukiwania.

Co jest ważne

  • Lokalność pamięci podręcznej — główna zaleta kopców zaimplementowanych w tablicach, która daje wielokrotną przewagę w szybkości przed strukturami opartymi na wskaźnikach.
  • Kopce d-arny pozwalają zmniejszyć wysokość drzewa i jeszcze bardziej zmniejszyć liczbę braków pamięci podręcznej, optymalna wartość d często wynosi 8.
  • Specjalizacja — kopce są idealne dla bardzo określonych operacji (wstawianie, wydobycie maksimum), ale nie są uniwersalne. Wybór struktury powinien opierać się na dokładnych wymaganiach algorytmu.
  • Benchmarkowanie — teoretyczne oszacowania złożoności operacji (O(log n)) mogą ukrywać różnice w rzeczywistej wydajności z powodu wpływu pamięci. Pomiar czasu i liczby braków pamięci podręcznej jest niezbędny.
  • Arytmetyka indeksów — podstawa efektywnej implementacji, umożliwiająca uniknięcie kosztów związanych z zarządzaniem wskaźnikami i pamięcią.

— Editorial Team

Advertisement 728x90

Czytaj dalej