Skip to content

Commit 130ffd3

Browse files
bors[bot]willcrozi
andauthored
Merge #335
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 <[email protected]>
2 parents 4183f2c + 83c0f04 commit 130ffd3

11 files changed

+416
-36
lines changed

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,11 @@ harness = false
6767
[[bench]]
6868
name = "bench1"
6969
harness = false
70+
71+
[[bench]]
72+
name = "combinations"
73+
harness = false
74+
75+
[[bench]]
76+
name = "powerset"
77+
harness = false

benches/combinations.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2+
use itertools::Itertools;
3+
4+
// approximate 100_000 iterations for each combination
5+
const N1: usize = 100_000;
6+
const N2: usize = 448;
7+
const N3: usize = 86;
8+
const N4: usize = 41;
9+
const N14: usize = 21;
10+
11+
fn comb_for1(c: &mut Criterion) {
12+
c.bench_function("comb for1", move |b| {
13+
b.iter(|| {
14+
for i in 0..N1 {
15+
black_box(vec![i]);
16+
}
17+
})
18+
});
19+
}
20+
21+
fn comb_for2(c: &mut Criterion) {
22+
c.bench_function("comb for2", move |b| {
23+
b.iter(|| {
24+
for i in 0..N2 {
25+
for j in (i + 1)..N2 {
26+
black_box(vec![i, j]);
27+
}
28+
}
29+
})
30+
});
31+
}
32+
33+
fn comb_for3(c: &mut Criterion) {
34+
c.bench_function("comb for3", move |b| {
35+
b.iter(|| {
36+
for i in 0..N3 {
37+
for j in (i + 1)..N3 {
38+
for k in (j + 1)..N3 {
39+
black_box(vec![i, j, k]);
40+
}
41+
}
42+
}
43+
})
44+
});
45+
}
46+
47+
fn comb_for4(c: &mut Criterion) {
48+
c.bench_function("comb for4", move |b| {
49+
b.iter(|| {
50+
for i in 0..N4 {
51+
for j in (i + 1)..N4 {
52+
for k in (j + 1)..N4 {
53+
for l in (k + 1)..N4 {
54+
black_box(vec![i, j, k, l]);
55+
}
56+
}
57+
}
58+
}
59+
})
60+
});
61+
}
62+
63+
fn comb_c1(c: &mut Criterion) {
64+
c.bench_function("comb c1", move |b| {
65+
b.iter(|| {
66+
for combo in (0..N1).combinations(1) {
67+
black_box(combo);
68+
}
69+
})
70+
});
71+
}
72+
73+
fn comb_c2(c: &mut Criterion) {
74+
c.bench_function("comb c2", move |b| {
75+
b.iter(|| {
76+
for combo in (0..N2).combinations(2) {
77+
black_box(combo);
78+
}
79+
})
80+
});
81+
}
82+
83+
fn comb_c3(c: &mut Criterion) {
84+
c.bench_function("comb c3", move |b| {
85+
b.iter(|| {
86+
for combo in (0..N3).combinations(3) {
87+
black_box(combo);
88+
}
89+
})
90+
});
91+
}
92+
93+
fn comb_c4(c: &mut Criterion) {
94+
c.bench_function("comb c4", move |b| {
95+
b.iter(|| {
96+
for combo in (0..N4).combinations(4) {
97+
black_box(combo);
98+
}
99+
})
100+
});
101+
}
102+
103+
fn comb_c14(c: &mut Criterion) {
104+
c.bench_function("comb c14", move |b| {
105+
b.iter(|| {
106+
for combo in (0..N14).combinations(14) {
107+
black_box(combo);
108+
}
109+
})
110+
});
111+
}
112+
113+
criterion_group!(
114+
benches,
115+
comb_for1,
116+
comb_for2,
117+
comb_for3,
118+
comb_for4,
119+
comb_c1,
120+
comb_c2,
121+
comb_c3,
122+
comb_c4,
123+
comb_c14,
124+
);
125+
criterion_main!(benches);

benches/powerset.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2+
use itertools::Itertools;
3+
4+
// Keep aggregate generated elements the same, regardless of powerset length.
5+
const TOTAL_ELEMENTS: usize = 1 << 12;
6+
const fn calc_iters(n: usize) -> usize {
7+
TOTAL_ELEMENTS / (1 << n)
8+
}
9+
10+
fn powerset_n(c: &mut Criterion, n: usize) {
11+
let id = format!("powerset {}", n);
12+
c.bench_function(id.as_str(), move |b| {
13+
b.iter(|| {
14+
for _ in 0..calc_iters(n) {
15+
for elt in (0..n).powerset() {
16+
black_box(elt);
17+
}
18+
}
19+
})
20+
});
21+
}
22+
23+
fn powerset_0(c: &mut Criterion) { powerset_n(c, 0); }
24+
25+
fn powerset_1(c: &mut Criterion) { powerset_n(c, 1); }
26+
27+
fn powerset_2(c: &mut Criterion) { powerset_n(c, 2); }
28+
29+
fn powerset_4(c: &mut Criterion) { powerset_n(c, 4); }
30+
31+
fn powerset_8(c: &mut Criterion) { powerset_n(c, 8); }
32+
33+
fn powerset_12(c: &mut Criterion) { powerset_n(c, 12); }
34+
35+
criterion_group!(
36+
benches,
37+
powerset_0,
38+
powerset_1,
39+
powerset_2,
40+
powerset_4,
41+
powerset_8,
42+
powerset_12,
43+
);
44+
criterion_main!(benches);

benches/tuple_combinations.rs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const N2: usize = 448;
77
const N3: usize = 86;
88
const N4: usize = 41;
99

10-
fn comb_for1(c: &mut Criterion) {
11-
c.bench_function("comb for1", move |b| {
10+
fn tuple_comb_for1(c: &mut Criterion) {
11+
c.bench_function("tuple comb for1", move |b| {
1212
b.iter(|| {
1313
for i in 0..N1 {
1414
black_box(i);
@@ -17,8 +17,8 @@ fn comb_for1(c: &mut Criterion) {
1717
});
1818
}
1919

20-
fn comb_for2(c: &mut Criterion) {
21-
c.bench_function("comb for2", move |b| {
20+
fn tuple_comb_for2(c: &mut Criterion) {
21+
c.bench_function("tuple comb for2", move |b| {
2222
b.iter(|| {
2323
for i in 0..N2 {
2424
for j in (i + 1)..N2 {
@@ -29,8 +29,8 @@ fn comb_for2(c: &mut Criterion) {
2929
});
3030
}
3131

32-
fn comb_for3(c: &mut Criterion) {
33-
c.bench_function("comb for3", move |b| {
32+
fn tuple_comb_for3(c: &mut Criterion) {
33+
c.bench_function("tuple comb for3", move |b| {
3434
b.iter(|| {
3535
for i in 0..N3 {
3636
for j in (i + 1)..N3 {
@@ -43,8 +43,8 @@ fn comb_for3(c: &mut Criterion) {
4343
});
4444
}
4545

46-
fn comb_for4(c: &mut Criterion) {
47-
c.bench_function("comb for4", move |b| {
46+
fn tuple_comb_for4(c: &mut Criterion) {
47+
c.bench_function("tuple comb for4", move |b| {
4848
b.iter(|| {
4949
for i in 0..N4 {
5050
for j in (i + 1)..N4 {
@@ -59,8 +59,8 @@ fn comb_for4(c: &mut Criterion) {
5959
});
6060
}
6161

62-
fn comb_c1(c: &mut Criterion) {
63-
c.bench_function("comb c1", move |b| {
62+
fn tuple_comb_c1(c: &mut Criterion) {
63+
c.bench_function("tuple comb c1", move |b| {
6464
b.iter(|| {
6565
for (i,) in (0..N1).tuple_combinations() {
6666
black_box(i);
@@ -69,8 +69,8 @@ fn comb_c1(c: &mut Criterion) {
6969
});
7070
}
7171

72-
fn comb_c2(c: &mut Criterion) {
73-
c.bench_function("comb c2", move |b| {
72+
fn tuple_comb_c2(c: &mut Criterion) {
73+
c.bench_function("tuple comb c2", move |b| {
7474
b.iter(|| {
7575
for (i, j) in (0..N2).tuple_combinations() {
7676
black_box(i + j);
@@ -79,8 +79,8 @@ fn comb_c2(c: &mut Criterion) {
7979
});
8080
}
8181

82-
fn comb_c3(c: &mut Criterion) {
83-
c.bench_function("comb c3", move |b| {
82+
fn tuple_comb_c3(c: &mut Criterion) {
83+
c.bench_function("tuple comb c3", move |b| {
8484
b.iter(|| {
8585
for (i, j, k) in (0..N3).tuple_combinations() {
8686
black_box(i + j + k);
@@ -89,8 +89,8 @@ fn comb_c3(c: &mut Criterion) {
8989
});
9090
}
9191

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

102102
criterion_group!(
103103
benches,
104-
comb_for1,
105-
comb_for2,
106-
comb_for3,
107-
comb_for4,
108-
comb_c1,
109-
comb_c2,
110-
comb_c3,
111-
comb_c4,
104+
tuple_comb_for1,
105+
tuple_comb_for2,
106+
tuple_comb_for3,
107+
tuple_comb_for4,
108+
tuple_comb_c1,
109+
tuple_comb_c2,
110+
tuple_comb_c3,
111+
tuple_comb_c4,
112112
);
113113
criterion_main!(benches);

src/combinations.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,8 @@ impl<I> fmt::Debug for Combinations<I>
3131
pub fn combinations<I>(iter: I, k: usize) -> Combinations<I>
3232
where I: Iterator
3333
{
34-
let mut pool: LazyBuffer<I> = LazyBuffer::new(iter);
35-
36-
for _ in 0..k {
37-
if !pool.get_next() {
38-
break;
39-
}
40-
}
34+
let mut pool = LazyBuffer::new(iter);
35+
pool.prefill(k);
4136

4237
Combinations {
4338
indices: (0..k).collect(),
@@ -46,14 +41,53 @@ pub fn combinations<I>(iter: I, k: usize) -> Combinations<I>
4641
}
4742
}
4843

44+
impl<I: Iterator> Combinations<I> {
45+
/// Returns the length of a combination produced by this iterator.
46+
#[inline]
47+
pub fn k(&self) -> usize { self.indices.len() }
48+
49+
/// Returns the (current) length of the pool from which combination elements are
50+
/// selected. This value can change between invocations of [`next`].
51+
///
52+
/// [`next`]: #method.next
53+
#[inline]
54+
pub fn n(&self) -> usize { self.pool.len() }
55+
56+
/// Returns a reference to the source iterator.
57+
#[inline]
58+
pub(crate) fn src(&self) -> &I { &self.pool.it }
59+
60+
/// Resets this `Combinations` back to an initial state for combinations of length
61+
/// `k` over the same pool data source. If `k` is larger than the current length
62+
/// of the data pool an attempt is made to prefill the pool so that it holds `k`
63+
/// elements.
64+
pub(crate) fn reset(&mut self, k: usize) {
65+
self.first = true;
66+
67+
if k < self.indices.len() {
68+
self.indices.truncate(k);
69+
for i in 0..k {
70+
self.indices[i] = i;
71+
}
72+
73+
} else {
74+
for i in 0..self.indices.len() {
75+
self.indices[i] = i;
76+
}
77+
self.indices.extend(self.indices.len()..k);
78+
self.pool.prefill(k);
79+
}
80+
}
81+
}
82+
4983
impl<I> Iterator for Combinations<I>
5084
where I: Iterator,
5185
I::Item: Clone
5286
{
5387
type Item = Vec<I::Item>;
5488
fn next(&mut self) -> Option<Self::Item> {
5589
if self.first {
56-
if self.pool.is_done() {
90+
if self.k() > self.n() {
5791
return None;
5892
}
5993
self.first = false;

src/lazy_buffer.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ where
2424
self.buffer.len()
2525
}
2626

27-
pub fn is_done(&self) -> bool {
28-
self.done
29-
}
30-
3127
pub fn get_next(&mut self) -> bool {
3228
if self.done {
3329
return false;
@@ -44,6 +40,17 @@ where
4440
}
4541
}
4642
}
43+
44+
pub fn prefill(&mut self, len: usize) {
45+
let buffer_len = self.buffer.len();
46+
47+
if !self.done && len > buffer_len {
48+
let delta = len - buffer_len;
49+
50+
self.buffer.extend(self.it.by_ref().take(delta));
51+
self.done = self.buffer.len() < len;
52+
}
53+
}
4754
}
4855

4956
impl<I, J> Index<J> for LazyBuffer<I>

0 commit comments

Comments
 (0)