This repository benchmarks the performance of several heap data structures across various workloads and hardware configurations. The goal is to provide insights into why certain heaps perform better under specific conditions, based on their underlying implementations.
Heaps Tested (Implementations Provided):
Pairing Heap: Node-based (std::shared_ptr) structure using linked children. Features O(1)meldand amortized O(1)push.popinvolves a potentially complex multi-pass merging strategy for amortized O(log N) cost.Priority Queue: Wrapper aroundstd::priority_queue, which typically uses an optimized array-based (std::vector) binary heap.meldis simulated via small-to-large merging.Quaternary Heap: Array-based (std::vector) d-ary heap with d=4. Operations involve sifting up/down within the vector.meldis simulated via small-to-large merging.Randomized Heap: Node-based (std::shared_ptr) binary structure. Relies on a coremergefunction that uses randomization to decide merge paths.push,pop, andmeldall utilize thismergeoperation.
The following tests were designed to stress different aspects of heap performance. Unless otherwise specified, all random operations can include push, pop, and meld (where applicable or simulated).
Test 1: General Throughput (Large Scale)
- Operations: 100,000,000 random operations.
- Purpose: To evaluate overall performance under high volume, stressing cache performance, memory allocation/deallocation, and the average cost of mixed operations.
Test 2: General Throughput (Medium Scale)
- Operations: 10,000,000 random operations.
- Purpose: Similar to Test 1, assessing mixed operation performance on a smaller scale.
Test 3: Basic Operations Focus (No Meld)
- Operations: 10,000,000 random
pushandpopoperations only. - Purpose: To isolate the performance characteristics of the core insertion and extraction algorithms, removing the influence of explicit
meldcalls.
Test 4: Push-Heavy Workload
- Operations: 10,000,000 random operations (75%
push, 25% other:pop,meld). - Purpose: To evaluate how efficiently each implementation handles frequent insertions, including node allocation costs (for node-based heaps) and sift-up efficiency.
Test 5: Increasing Key Insertion Order
- Operations: 10,000,000 random operations, with pushed values increasing sequentially.
- Purpose: To test behavior when insertions might hit best-case scenarios (e.g., minimal sifting) but are still mixed with random
pop/meld.
Test 6: Decreasing Key Insertion Order
- Operations: 10,000,000 random operations, with pushed values decreasing sequentially.
- Purpose: To test behavior under potentially challenging insertion patterns that might lead to worst-case structural scenarios or cache performance for some implementations.
| Test | Scenario | Pairing Heap |
Priority Queue |
Quaternary Heap |
Randomized Heap |
|---|---|---|---|---|---|
| Test 1 | 100M Random Ops | 67.7 s | 35.58 s | 23.27 s | 125.4 s |
| Test 2 | 10M Random Ops | 4.874 s | 3.082 s | 2.25 s | 12.21 s |
| Test 3 | 10M Random (No Meld) | 6.322 s | 3.353 s | 2.179 s | 3.27 s |
| Test 4 | 10M Random (75% Push) | 11.23 s | 5.028 s | 3.175 s | 14.66 s |
| Test 5 | 10M Random (Increasing) | 3.966 s | 4.041 s | 2.204 s | 11.67 s |
| Test 6 | 10M Random (Decreasing) | 9.659 s | 4.663 s | 2.382 s | 22.79 s |
Note: All metrics collected on a Ryzen 5 7640HS CPU paired with an NVIDIA 4060 GPU (though the GPU is unlikely to be relevant for heap performance). RAM speed and configuration could also influence results.
-
Test 1 & 2 (General Random Ops):
Quaternary Heap: Achieved the best time because its array-based (std::vector) implementation provides excellent cache locality (data is contiguous in memory). The 4-arity leads to a shallower tree compared to a binary heap, reducing the average number of swaps/comparisons needed duringpush(shift-up) andpop(shift-down). Its simulated small-to-largemeldinvolves efficient vector iteration andpushoperations.Priority Queue: Performed well because it relies on the highly optimizedstd::priority_queue, which uses a cache-friendlystd::vectorinternally for its binary heap. The standard library implementation likely has minimal overhead. Its simulatedmeldalso benefits from the efficient underlyingpush/pop.Pairing Heap: Was slower due to its node-based (std::shared_ptr) structure, leading to pointer chasing and poor cache locality. While itspushandmeldhave good amortized bounds, the multi-passpopoperation, involving potentially many merges and list manipulations, has higher constant factors and suffers from the cache effects of scattered nodes.shared_ptroverhead also contributes.Randomized Heap: Was slowest primarily due to its node-based (std::shared_ptr) nature (poor cache locality, pointer chasing, allocation overhead). Its core recursivemergefunction, used by all modifying operations, adds function call overhead and relies on randomization (rng()calls), which doesn't guarantee optimal structure and adds computational cost.
-
Test 3 (No Meld Ops):
Quaternary Heap/Priority Queue: Remained fastest/second because their corepush/popefficiency stems from their cache-friendly array-based implementations, which are unaffected by removingmeld.Randomized Heap: Showed significant improvement because removing explicitmeldcalls drastically reduced the total number of times its relatively expensive recursivemergefunction was invoked. Its basicpush(onemerge) andpop(onemergeof children) are less costly in isolation than when combined with frequent explicitmelds.Pairing Heap: Performed slightly worse possibly because the specific mix of onlypush/popoperations might have led to structural states where its complex multi-passpophit less favorable scenarios more often compared to the structure resulting from a mix includingmeld.
-
Test 4 (Push-Heavy):
Quaternary Heap/Priority Queue: Maintained their lead because their array-basedpush(sift-up) is inherently efficient due to good cache locality and contiguous memory access.Pairing Heap: Slowed down because eachpushinvolves node allocation (make_shared) and an O(1) amortizedmeld. While individually fast, the high frequency increases allocation overhead and might build structural complexity ("amortized debt") that makes occasionalpopoperations more expensive.Randomized Heap: Slowed down significantly as eachpushrequires node allocation (make_shared) and a call to the recursivemergefunction. The high frequency highlights the overhead of both memory management and themergeoperation itself.
-
Test 5 (Increasing Keys):
Quaternary Heap/Priority Queue: Remained efficient because pushing increasing keys often results in best-case sift-up behavior (few or no swaps needed) in their array structures.Pairing Heap: Performed relatively well because pushing an increasing key often results in the new node becoming the root immediately via the simple O(1)meld. This creates a potentially simpler structure temporarily, possibly delaying the cost of the multi-passpop.Randomized Heap: Showed little improvement because its performance is dominated by the overhead of the randomizedmergealgorithm and pointer chasing, which are less sensitive to input key order compared to deterministic sift-based heaps.
-
Test 6 (Decreasing Keys):
Quaternary Heap/Priority Queue: Remained robust because their array-based sift operations handle this pattern efficiently, even if it requires more swaps during sift-up than increasing keys. Cache locality remains a major advantage.Pairing Heap: Slowed down notably because inserting decreasing keys can create degenerate structures (e.g., long chains of nodes linked via theptr[0]child), which makes the multi-pass merge duringpopless efficient and approach worst-case behavior.Randomized Heap: Performed very poorly. While randomization aims to avoid worst-case behavior from input order, this specific pattern combined with the recursivemergemight lead to particularly bad cache performance or deeper recursion stacks due to the resulting node structure and pointer chasing.
Based on these tests and implementation analysis:
Quaternary Heapexcels due to cache-friendly array storage and shallow tree depth. Its performance advantage stems from leveraging memory hierarchy effectively and reducing the path length for operations, even when simulatingmeld.Priority Queueis strong because it uses a highly optimized standard library implementation based on similar principles (array-based binary heap).Pairing Heap's performance varies. Its O(1)meld/amortizedpushis offset by a complexpopand the inherent costs of node-based structures (pointer chasing, poor cache locality, allocation overhead).Randomized Heapsuffers from node-based overheads and the cost of its recursive, randomizedmergeoperation. While theoretically sound, the practical implementation costs (pointers, randomization, recursion) make it slower in these tests.
For general-purpose performance where cache efficiency is key, array-based heaps (Quaternary Heap, Priority Queue) demonstrate significant advantages. Node-based heaps (Pairing Heap, Randomized Heap) pay a performance penalty for their flexibility due to memory indirection and allocation overheads, though Pairing Heap offers a theoretically fast meld.
For heap structures that do not natively support an efficient meld (merge) operation (Priority Queue, Quaternary Heap in these implementations), the benchmarks simulated this operation using the small-to-large merging technique whenever a meld operation was required by the test scenario.
This involves:
- Identifying the smaller and the larger of the two heaps to be melded (based on
.size()or internal vector size). - Iteratively extracting elements from the smaller heap (using
popforPriorityQueue, or iterating the vector forQuaternary Heap) and inserting them into the larger heap (usingpush).
Complexity Consideration: This simulation adds overhead. However, the small-to-large approach guarantees that any single element is moved (pushed into another heap during a meld) at most O(log N) times throughout the entire benchmark run (where N is the total number of elements inserted). This provides an amortized bound on the total cost of all simulated meld operations. Heaps with native meld (Pairing Heap, Randomized Heap) use their built-in merge logic directly.