Skip to content

Commit d1e235c

Browse files
committed
Improve example solution of circular-buffer
The trait bounds `Default + Clone` are semantically incorrect. A proper circular buffer needs to handle all kinds of elements. The reason these bounds are here is because it allows to avoid `unsafe` while enjoying the most efficient memory layout. However, there is another performance downside: The implementations of `Default` and `Clone` may be expensive (e.g. cause heap allocations.) As came up in this discussion, there are other weird side effects, for example for `Rc` which has a non-standard implementation of `Clone`: #1652 The approach I chose here get's rid of the trait bounds and wraps every element in an `Option`. In the worst case, this may lead to twice the memory consumption. (e.g. `Option<u64>` takes 16 bytes because of alignment) This approach is semantically correct, but not the most performant. The most performant solution would be to use `unsafe`. I'd like to avoid that in example solutions.
1 parent 321824b commit d1e235c

1 file changed

Lines changed: 24 additions & 26 deletions

File tree

  • exercises/practice/circular-buffer/.meta

exercises/practice/circular-buffer/.meta/example.rs

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@ pub enum Error {
44
FullBuffer,
55
}
66

7-
pub struct CircularBuffer<T: Default + Clone> {
8-
buffer: Vec<T>,
9-
size: usize,
7+
pub struct CircularBuffer<T> {
8+
/// Using Option leads to less efficient memory layout, but
9+
/// it allows us to avoid using `unsafe` to handle uninitialized
10+
/// mempory ourselves.
11+
data: Vec<Option<T>>,
1012
start: usize,
1113
end: usize,
1214
}
1315

14-
impl<T: Default + Clone> CircularBuffer<T> {
15-
// this circular buffer keeps an unallocated slot between the start and the end
16-
// when the buffer is full.
17-
pub fn new(size: usize) -> CircularBuffer<T> {
18-
CircularBuffer {
19-
buffer: vec![T::default(); size + 1],
20-
size: size + 1,
16+
impl<T> CircularBuffer<T> {
17+
pub fn new(capacity: usize) -> Self {
18+
let mut data = Vec::with_capacity(capacity);
19+
data.resize_with(capacity, || None);
20+
Self {
21+
data,
2122
start: 0,
2223
end: 0,
2324
}
@@ -27,8 +28,9 @@ impl<T: Default + Clone> CircularBuffer<T> {
2728
if self.is_empty() {
2829
return Err(Error::EmptyBuffer);
2930
}
30-
31-
let v = self.buffer[self.start].clone();
31+
let v = self.data[self.start]
32+
.take()
33+
.expect("should not read 'uninitialized' memory");
3234
self.advance_start();
3335
Ok(v)
3436
}
@@ -37,18 +39,16 @@ impl<T: Default + Clone> CircularBuffer<T> {
3739
if self.is_full() {
3840
return Err(Error::FullBuffer);
3941
}
40-
41-
self.buffer[self.end] = byte;
42+
self.data[self.end] = Some(byte);
4243
self.advance_end();
4344
Ok(())
4445
}
4546

4647
pub fn overwrite(&mut self, byte: T) {
47-
if self.is_full() {
48+
self.data[self.end] = Some(byte);
49+
if self.start == self.end {
4850
self.advance_start();
4951
}
50-
51-
self.buffer[self.end] = byte;
5252
self.advance_end();
5353
}
5454

@@ -57,24 +57,22 @@ impl<T: Default + Clone> CircularBuffer<T> {
5757
self.end = 0;
5858

5959
// Clear any values in the buffer
60-
for element in self.buffer.iter_mut() {
61-
std::mem::take(element);
62-
}
60+
self.data.fill_with(|| None);
6361
}
6462

65-
pub fn is_empty(&self) -> bool {
66-
self.start == self.end
63+
fn is_empty(&self) -> bool {
64+
self.start == self.end && self.data[self.start].is_none()
6765
}
6866

69-
pub fn is_full(&self) -> bool {
70-
(self.end + 1) % self.size == self.start
67+
fn is_full(&self) -> bool {
68+
self.start == self.end && self.data[self.start].is_some()
7169
}
7270

7371
fn advance_start(&mut self) {
74-
self.start = (self.start + 1) % self.size;
72+
self.start = (self.start + 1) % self.data.len();
7573
}
7674

7775
fn advance_end(&mut self) {
78-
self.end = (self.end + 1) % self.size;
76+
self.end = (self.end + 1) % self.data.len();
7977
}
8078
}

0 commit comments

Comments
 (0)