Skip to content

Commit

Permalink
Merge #335
Browse files Browse the repository at this point in the history
335: Powerset iterator adaptor r=jswrenn a=willcrozi

Implements a [powerset](https://en.wikipedia.org/wiki/Power_set) iterator adaptor that iterates over all subsets of the input iterator's elements. Returns vectors representing these subsets. Internally uses `Combinations` of increasing length.

I've taken the strategy of using a 'position' field that acts both as a means to to detect the special case of the first element and also allows optimal `size_hint()` implementation.

Additionally there is a commit to improve performance that alters `Combinations` implementation slightly. I've added Combinations benchmark as a stand-alone commit to allow checking for performance regressions. `Powerset` performance after this commit improves some cases (with small sizes of `n`) by 10-30%

This is my first attempt at a Rust contribution, happy to put in whatever discussion/work to get this merged. Cheers!

Co-authored-by: Will Crozier <willcrozi@gmail.com>
  • Loading branch information
bors[bot] and willcrozi committed Dec 7, 2020
2 parents 9958c45 + 83c0f04 commit 3489aeb
Show file tree
Hide file tree
Showing 11 changed files with 416 additions and 36 deletions.
8 changes: 8 additions & 0 deletions Cargo.toml
Expand Up @@ -67,3 +67,11 @@ harness = false
[[bench]]
name = "bench1"
harness = false

[[bench]]
name = "combinations"
harness = false

[[bench]]
name = "powerset"
harness = false
125 changes: 125 additions & 0 deletions benches/combinations.rs
@@ -0,0 +1,125 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use itertools::Itertools;

// approximate 100_000 iterations for each combination
const N1: usize = 100_000;
const N2: usize = 448;
const N3: usize = 86;
const N4: usize = 41;
const N14: usize = 21;

fn comb_for1(c: &mut Criterion) {
c.bench_function("comb for1", move |b| {
b.iter(|| {
for i in 0..N1 {
black_box(vec![i]);
}
})
});
}

fn comb_for2(c: &mut Criterion) {
c.bench_function("comb for2", move |b| {
b.iter(|| {
for i in 0..N2 {
for j in (i + 1)..N2 {
black_box(vec![i, j]);
}
}
})
});
}

fn comb_for3(c: &mut Criterion) {
c.bench_function("comb for3", move |b| {
b.iter(|| {
for i in 0..N3 {
for j in (i + 1)..N3 {
for k in (j + 1)..N3 {
black_box(vec![i, j, k]);
}
}
}
})
});
}

fn comb_for4(c: &mut Criterion) {
c.bench_function("comb for4", move |b| {
b.iter(|| {
for i in 0..N4 {
for j in (i + 1)..N4 {
for k in (j + 1)..N4 {
for l in (k + 1)..N4 {
black_box(vec![i, j, k, l]);
}
}
}
}
})
});
}

fn comb_c1(c: &mut Criterion) {
c.bench_function("comb c1", move |b| {
b.iter(|| {
for combo in (0..N1).combinations(1) {
black_box(combo);
}
})
});
}

fn comb_c2(c: &mut Criterion) {
c.bench_function("comb c2", move |b| {
b.iter(|| {
for combo in (0..N2).combinations(2) {
black_box(combo);
}
})
});
}

fn comb_c3(c: &mut Criterion) {
c.bench_function("comb c3", move |b| {
b.iter(|| {
for combo in (0..N3).combinations(3) {
black_box(combo);
}
})
});
}

fn comb_c4(c: &mut Criterion) {
c.bench_function("comb c4", move |b| {
b.iter(|| {
for combo in (0..N4).combinations(4) {
black_box(combo);
}
})
});
}

fn comb_c14(c: &mut Criterion) {
c.bench_function("comb c14", move |b| {
b.iter(|| {
for combo in (0..N14).combinations(14) {
black_box(combo);
}
})
});
}

criterion_group!(
benches,
comb_for1,
comb_for2,
comb_for3,
comb_for4,
comb_c1,
comb_c2,
comb_c3,
comb_c4,
comb_c14,
);
criterion_main!(benches);
44 changes: 44 additions & 0 deletions benches/powerset.rs
@@ -0,0 +1,44 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use itertools::Itertools;

// Keep aggregate generated elements the same, regardless of powerset length.
const TOTAL_ELEMENTS: usize = 1 << 12;
const fn calc_iters(n: usize) -> usize {
TOTAL_ELEMENTS / (1 << n)
}

fn powerset_n(c: &mut Criterion, n: usize) {
let id = format!("powerset {}", n);
c.bench_function(id.as_str(), move |b| {
b.iter(|| {
for _ in 0..calc_iters(n) {
for elt in (0..n).powerset() {
black_box(elt);
}
}
})
});
}

fn powerset_0(c: &mut Criterion) { powerset_n(c, 0); }

fn powerset_1(c: &mut Criterion) { powerset_n(c, 1); }

fn powerset_2(c: &mut Criterion) { powerset_n(c, 2); }

fn powerset_4(c: &mut Criterion) { powerset_n(c, 4); }

fn powerset_8(c: &mut Criterion) { powerset_n(c, 8); }

fn powerset_12(c: &mut Criterion) { powerset_n(c, 12); }

criterion_group!(
benches,
powerset_0,
powerset_1,
powerset_2,
powerset_4,
powerset_8,
powerset_12,
);
criterion_main!(benches);
48 changes: 24 additions & 24 deletions benches/tuple_combinations.rs
Expand Up @@ -7,8 +7,8 @@ const N2: usize = 448;
const N3: usize = 86;
const N4: usize = 41;

fn comb_for1(c: &mut Criterion) {
c.bench_function("comb for1", move |b| {
fn tuple_comb_for1(c: &mut Criterion) {
c.bench_function("tuple comb for1", move |b| {
b.iter(|| {
for i in 0..N1 {
black_box(i);
Expand All @@ -17,8 +17,8 @@ fn comb_for1(c: &mut Criterion) {
});
}

fn comb_for2(c: &mut Criterion) {
c.bench_function("comb for2", move |b| {
fn tuple_comb_for2(c: &mut Criterion) {
c.bench_function("tuple comb for2", move |b| {
b.iter(|| {
for i in 0..N2 {
for j in (i + 1)..N2 {
Expand All @@ -29,8 +29,8 @@ fn comb_for2(c: &mut Criterion) {
});
}

fn comb_for3(c: &mut Criterion) {
c.bench_function("comb for3", move |b| {
fn tuple_comb_for3(c: &mut Criterion) {
c.bench_function("tuple comb for3", move |b| {
b.iter(|| {
for i in 0..N3 {
for j in (i + 1)..N3 {
Expand All @@ -43,8 +43,8 @@ fn comb_for3(c: &mut Criterion) {
});
}

fn comb_for4(c: &mut Criterion) {
c.bench_function("comb for4", move |b| {
fn tuple_comb_for4(c: &mut Criterion) {
c.bench_function("tuple comb for4", move |b| {
b.iter(|| {
for i in 0..N4 {
for j in (i + 1)..N4 {
Expand All @@ -59,8 +59,8 @@ fn comb_for4(c: &mut Criterion) {
});
}

fn comb_c1(c: &mut Criterion) {
c.bench_function("comb c1", move |b| {
fn tuple_comb_c1(c: &mut Criterion) {
c.bench_function("tuple comb c1", move |b| {
b.iter(|| {
for (i,) in (0..N1).tuple_combinations() {
black_box(i);
Expand All @@ -69,8 +69,8 @@ fn comb_c1(c: &mut Criterion) {
});
}

fn comb_c2(c: &mut Criterion) {
c.bench_function("comb c2", move |b| {
fn tuple_comb_c2(c: &mut Criterion) {
c.bench_function("tuple comb c2", move |b| {
b.iter(|| {
for (i, j) in (0..N2).tuple_combinations() {
black_box(i + j);
Expand All @@ -79,8 +79,8 @@ fn comb_c2(c: &mut Criterion) {
});
}

fn comb_c3(c: &mut Criterion) {
c.bench_function("comb c3", move |b| {
fn tuple_comb_c3(c: &mut Criterion) {
c.bench_function("tuple comb c3", move |b| {
b.iter(|| {
for (i, j, k) in (0..N3).tuple_combinations() {
black_box(i + j + k);
Expand All @@ -89,8 +89,8 @@ fn comb_c3(c: &mut Criterion) {
});
}

fn comb_c4(c: &mut Criterion) {
c.bench_function("comb c4", move |b| {
fn tuple_comb_c4(c: &mut Criterion) {
c.bench_function("tuple comb c4", move |b| {
b.iter(|| {
for (i, j, k, l) in (0..N4).tuple_combinations() {
black_box(i + j + k + l);
Expand All @@ -101,13 +101,13 @@ fn comb_c4(c: &mut Criterion) {

criterion_group!(
benches,
comb_for1,
comb_for2,
comb_for3,
comb_for4,
comb_c1,
comb_c2,
comb_c3,
comb_c4,
tuple_comb_for1,
tuple_comb_for2,
tuple_comb_for3,
tuple_comb_for4,
tuple_comb_c1,
tuple_comb_c2,
tuple_comb_c3,
tuple_comb_c4,
);
criterion_main!(benches);
50 changes: 42 additions & 8 deletions src/combinations.rs
Expand Up @@ -31,13 +31,8 @@ impl<I> fmt::Debug for Combinations<I>
pub fn combinations<I>(iter: I, k: usize) -> Combinations<I>
where I: Iterator
{
let mut pool: LazyBuffer<I> = LazyBuffer::new(iter);

for _ in 0..k {
if !pool.get_next() {
break;
}
}
let mut pool = LazyBuffer::new(iter);
pool.prefill(k);

Combinations {
indices: (0..k).collect(),
Expand All @@ -46,14 +41,53 @@ pub fn combinations<I>(iter: I, k: usize) -> Combinations<I>
}
}

impl<I: Iterator> Combinations<I> {
/// Returns the length of a combination produced by this iterator.
#[inline]
pub fn k(&self) -> usize { self.indices.len() }

/// Returns the (current) length of the pool from which combination elements are
/// selected. This value can change between invocations of [`next`].
///
/// [`next`]: #method.next
#[inline]
pub fn n(&self) -> usize { self.pool.len() }

/// Returns a reference to the source iterator.
#[inline]
pub(crate) fn src(&self) -> &I { &self.pool.it }

/// Resets this `Combinations` back to an initial state for combinations of length
/// `k` over the same pool data source. If `k` is larger than the current length
/// of the data pool an attempt is made to prefill the pool so that it holds `k`
/// elements.
pub(crate) fn reset(&mut self, k: usize) {
self.first = true;

if k < self.indices.len() {
self.indices.truncate(k);
for i in 0..k {
self.indices[i] = i;
}

} else {
for i in 0..self.indices.len() {
self.indices[i] = i;
}
self.indices.extend(self.indices.len()..k);
self.pool.prefill(k);
}
}
}

impl<I> Iterator for Combinations<I>
where I: Iterator,
I::Item: Clone
{
type Item = Vec<I::Item>;
fn next(&mut self) -> Option<Self::Item> {
if self.first {
if self.pool.is_done() {
if self.k() > self.n() {
return None;
}
self.first = false;
Expand Down
15 changes: 11 additions & 4 deletions src/lazy_buffer.rs
Expand Up @@ -24,10 +24,6 @@ where
self.buffer.len()
}

pub fn is_done(&self) -> bool {
self.done
}

pub fn get_next(&mut self) -> bool {
if self.done {
return false;
Expand All @@ -44,6 +40,17 @@ where
}
}
}

pub fn prefill(&mut self, len: usize) {
let buffer_len = self.buffer.len();

if !self.done && len > buffer_len {
let delta = len - buffer_len;

self.buffer.extend(self.it.by_ref().take(delta));
self.done = self.buffer.len() < len;
}
}
}

impl<I, J> Index<J> for LazyBuffer<I>
Expand Down

0 comments on commit 3489aeb

Please sign in to comment.