Skip to content

Suboptimal performance of BinaryHeap::append #77433

Closed
@hanmertens

Description

@hanmertens

The current implementation of BinaryHeap::append uses a heuristic based on the worst-case number of comparisons to determine between two strategies:

  1. Calling self.extend(other.drain()), equivalent to pushing all elements of the other heap.
  2. Appending the vector of the other heap and rebuilding the heap from scratch.

I've done some benchmarking, and it turns out that method 1 (based on extend) is always faster for two heaps based on randomly shuffled data – on the computers I've tested it on, anyway. I've included images of the benchmarks below: first the current append strategy, then the extend strategy (method 1). In the benchmarks, two heaps are merged, one containing the number of elements on the x-axis, the other containing 100,000 minus that number (for the rightmost data points both heaps are equal in size). The red line (sometimes hiding behind the green one) is in both images that corresponding to std::collections::BinaryHeap, the other lines are not relevant to this issue (they aren't standard library types).

append
extend

From the jump in performance in the first graph, you can clearly see when the switch between the two approaches happens. Graphs are similar for smaller heaps, I didn't test larger ones. It's possible method 2 is faster under other circumstances (maybe if one of the heaps contains mostly very small/large elements?), especially if both heaps are (almost) equal in size. However, I think the benchmark is more close to the "average" real-world use case than such a case would be (I'd be happy to be proven wrong about this, though).

Simplification of the benchmark that only runs the parts relevant for this issue
Cargo.toml
[package]
name = "binary_heap_bench"
version = "0.1.0"
edition = "2018"

[dependencies]
criterion = "0.3"
rand = "0.7"

[[bench]]
name = "benchmark"
harness = false
benches/benchmark.rs
use criterion::*;
use rand::{seq::SliceRandom, thread_rng};
use std::collections::BinaryHeap;
use std::iter::FromIterator;
use std::mem;

/// Data type we want to use
type T = u32;

/// Produce shuffled vector containing values 0..n
fn random_data(n: T) -> Vec<T> {
    let mut data = Vec::from_iter(0..n);
    data.shuffle(&mut thread_rng());
    data
}

fn two_heaps<H: From<Vec<T>>>(mut vec1: Vec<T>, i: usize) -> (H, H) {
    let vec2 = vec1.split_off(i);
    (vec1.into(), vec2.into())
}

fn std_heap_append((mut heap1, mut heap2): (BinaryHeap<T>, BinaryHeap<T>)) -> BinaryHeap<T> {
    heap1.append(&mut heap2);
    heap1
}

fn std_heap_extend((mut heap1, mut heap2): (BinaryHeap<T>, BinaryHeap<T>)) -> BinaryHeap<T> {
    if heap1.len() < heap2.len() {
        mem::swap(&mut heap1, &mut heap2);
    }
    heap1.extend(heap2.drain());
    heap1
}

fn benchmark(c: &mut Criterion) {
    let mut group = c.benchmark_group("append/extend");
    let base = 100000;
    let step = 2500;
    for i in (step..=base as usize / 2).step_by(step) {
        let size = BatchSize::SmallInput;
        group.bench_function(BenchmarkId::new("append", i), |b| {
            b.iter_batched(|| two_heaps(random_data(base), i), std_heap_append, size)
        });
        group.bench_function(BenchmarkId::new("extend", i), |b| {
            b.iter_batched(|| two_heaps(random_data(base), i), std_heap_extend, size)
        });
    }
}

criterion_group!(benches, benchmark);
criterion_main!(benches);

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-collectionsArea: `std::collections`C-enhancementCategory: An issue proposing an enhancement or a PR with one.I-slowIssue: Problems and improvements with respect to performance of generated code.T-libsRelevant to the library team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions