Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ringbuf remove option<T> #18747

Merged
merged 6 commits into from Nov 17, 2014
Merged

Ringbuf remove option<T> #18747

merged 6 commits into from Nov 17, 2014

Conversation

ghost
Copy link

@ghost ghost commented Nov 7, 2014

Fix for task in Metabug #18009 (Rebased version of #18170)

This changes much of about how RingBuf functions. lo, nelts are replaced by a more traditional head andtail. The Vec<Option<T>> is replaced by a bare pointer that is managed by the RingBuf itself. This also expects the ring buffer to always be size that is a power of 2.

This change also includes a number of new tests to cover the some areas that could be of concern with manual memory management.

The benchmarks have been reworked since the old ones were benchmarking of the Ring buffers growth rather then the actual test.

The unit test suite have been expanded, and exposed some bugs in fn get() and fn get_mut()

Benchmark

Before:

test ring_buf::tests::bench_grow_1025                      ... bench:      8919 ns/iter (+/- 87)
test ring_buf::tests::bench_iter_1000                      ... bench:       924 ns/iter (+/- 28)
test ring_buf::tests::bench_mut_iter_1000                  ... bench:       918 ns/iter (+/- 6)
test ring_buf::tests::bench_new                            ... bench:        15 ns/iter (+/- 0)
test ring_buf::tests::bench_pop_100                        ... bench:       294 ns/iter (+/- 9)
test ring_buf::tests::bench_pop_front_100                  ... bench:       948 ns/iter (+/- 32)
test ring_buf::tests::bench_push_back_100                  ... bench:       291 ns/iter (+/- 16)
test ring_buf::tests::bench_push_front_100                 ... bench:       311 ns/iter (+/- 27

After:

test ring_buf::tests::bench_grow_1025                      ... bench:      2209 ns/iter (+/- 169)
test ring_buf::tests::bench_iter_1000                      ... bench:       534 ns/iter (+/- 27)
test ring_buf::tests::bench_mut_iter_1000                  ... bench:       515 ns/iter (+/- 28)
test ring_buf::tests::bench_new                            ... bench:        11 ns/iter (+/- 0)
test ring_buf::tests::bench_pop_100                        ... bench:       170 ns/iter (+/- 5)
test ring_buf::tests::bench_pop_front_100                  ... bench:       171 ns/iter (+/- 11)
test ring_buf::tests::bench_push_back_100                  ... bench:       172 ns/iter (+/- 13)
test ring_buf::tests::bench_push_front_100                 ... bench:       158 ns/iter (+/- 12)

@rust-highfive
Copy link
Collaborator

warning Warning warning

  • These commits modify unsafe code. Please review it carefully!

@ghost
Copy link
Author

ghost commented Nov 7, 2014

ping @gankro

@Gankra
Copy link
Contributor

Gankra commented Nov 7, 2014

❤️ ❤️ ❤️

Thanks so much for persisting! Will review/trick-others-into-reviewing ASAP.

pub fn reserve_exact(&mut self, additional: uint) {
// FIXME(Gankro): this is just wrong. The ringbuf won't actually use this space
self.elts.reserve_exact(additional);
#[deprecated = "use reserve, Ringbuf can no longer be an exact size."]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right call, but we need to work out if we want to expose this level of implementation detail in the public APIs (that is, this isn't fundamental to a RingBuf). CC @aturon interested in thoughts. Applies to HashMap as well.

@thestinger
Copy link
Contributor

Using debug_assert! extensively will result in this being slower in many cases than the current ring buffer. In that case, it doesn't make sense to merge this.

@huonw
Copy link
Member

huonw commented Nov 11, 2014

Once #17665 is solved, those problems will go away. We should optimise for the language we are aiming to have, not the one we currently have. It is ridiculously easy to manually do #17665 (i.e. sed -i /debug_assert!/d ringbuf.rs) on a case-by-case basis if it is not in-place by 1.0 but much harder to do the reverse if #17665 is implemented.

@ghost
Copy link
Author

ghost commented Nov 11, 2014

@gankro both of the oom cases should be covered by csherratt@3905a39

@Gankra
Copy link
Contributor

Gankra commented Nov 11, 2014

Ah, that it is! (I was going off of clobbered comments)

Just need to figure out if we want the debug_asserts, then. I leave you in the strong, comforting, arms of @huonw then.

@ghost
Copy link
Author

ghost commented Nov 11, 2014

I added some extra debug_asserts. I was careful in where I placed them to avoid doing the same check twice. The only state that is invalid for the head and tail pointer to be in is if they are greater then or equal to the capacity of ring buffer. Since wrap_index is used in every case to modify the head and tail pointer placing an out of bound check in it will check that assertion everywhere.

The other suspect code is the logic in reserve. So I added a few extra asserts.

Performance wise the extra checks are not free. The largest changes are in the iteration rate.

Before

test ring_buf::tests::bench_grow_1025                      ... bench:      3078 ns/iter (+/- 43)
test ring_buf::tests::bench_iter_1000                      ... bench:       629 ns/iter (+/- 4)
test ring_buf::tests::bench_mut_iter_1000                  ... bench:       625 ns/iter (+/- 9)
test ring_buf::tests::bench_new                            ... bench:        17 ns/iter (+/- 0)
test ring_buf::tests::bench_pop_100                        ... bench:       284 ns/iter (+/- 0)
test ring_buf::tests::bench_pop_front_100                  ... bench:       235 ns/iter (+/- 0)
test ring_buf::tests::bench_push_back_100                  ... bench:       282 ns/iter (+/- 0)
test ring_buf::tests::bench_push_front_100                 ... bench:       234 ns/iter (+/- 1)

After

test ring_buf::tests::bench_grow_1025                      ... bench:      2883 ns/iter (+/- 202)
test ring_buf::tests::bench_iter_1000                      ... bench:       999 ns/iter (+/- 15)
test ring_buf::tests::bench_mut_iter_1000                  ... bench:       994 ns/iter (+/- 21)
test ring_buf::tests::bench_new                            ... bench:        17 ns/iter (+/- 0)
test ring_buf::tests::bench_pop_100                        ... bench:       223 ns/iter (+/- 1)
test ring_buf::tests::bench_pop_front_100                  ... bench:       209 ns/iter (+/- 1)
test ring_buf::tests::bench_push_back_100                  ... bench:       220 ns/iter (+/- 5)
test ring_buf::tests::bench_push_front_100                 ... bench:       215 ns/iter (+/- 1)

@mahkoh
Copy link
Contributor

mahkoh commented Nov 11, 2014

while None != deq.pop_front() {}

These benchmarks are still useless because they can be optimized away.

@mahkoh
Copy link
Contributor

mahkoh commented Nov 11, 2014

Ideally the push and pop benchmarks should handle their inverse with unsafe code so that it doesn't take any time.

@ghost
Copy link
Author

ghost commented Nov 11, 2014

@mahkoh that's not a bad idea. I'll give it a try.

self.elts[self.lo] = Some(t);
self.nelts += 1u;

self.tail = wrap_index(self.tail - 1, self.cap);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these function calls can be replaced by

self.tail -= 1;

The only place where you might have to wrap indices is in the reallocating function. By removing these operations you get measurably better performance. Furthermore, if you don't wrap indices you can reduce memory usage by up to 2x because head == tail if and only if the container is empty and therefore you don't need an extra space.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This optimisation can be done in a follow-up PR. Also, what happens here when self.tail = 0?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing that matters is head - tail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual number does matter, e.g. the self.buffer_write(tail, t) below is writing t to self.ptr.offset(tail), where self.ptr points to the start of the allocated buffer (so tail better be positive). As a random other example (that's not reallocation), wrapping also matters in get,

fn get(&self, i: uint) -> Option<&T> {
    if i < self.len() {
        unsafe { Some(&mut *self.ptr.offset((self.tail + i) as int)) }
    } else {
        None
    }
}

is incorrect if tail can be "negative".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to wrap when you actually access the memory, that's clear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So

The only place where you might have to wrap indices is in the reallocating function

was not correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

@huonw
Copy link
Member

huonw commented Nov 11, 2014

I'm happy to r+ this after feedback on #18747 (comment) (everything else can be done later).

@ghost
Copy link
Author

ghost commented Nov 14, 2014

@huonw I think I have satisfied the asserts you wanted.


#[test]
fn test_drop() {
static mut drops: uint = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General point: this could be a non-mut static storing an AtomicUint which would avoid the need for unsafety.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe modifying an non-mut static is UB even if it is an Atomic*.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the whole purpose of introducing const was to allow non-unsafe mutation of statics by building on top of the Sync built-in trait and only allowing & references to be taken.

(The compiler allows it without unsafe, which is a pretty good indication that something is not UB.)

-Adds unit tests for fn get() and fn get_mut() which are currently untested
-Adds unit tests to verify growth of the ringbuffer when reserve is called.
-Adds unit tests to confirm that dropping of items is correct
Move ringbuf to use a raw buffer instead of Option<T>
Use is_some() in clear to simplify the clear loop.
@ghost
Copy link
Author

ghost commented Nov 15, 2014

Can I get a retry? The first build failed because I got unlucky and this got hit by a breaking change #18827

bors added a commit that referenced this pull request Nov 16, 2014
Fix for task in Metabug #18009 (Rebased version of #18170)

This changes much of about how RingBuf functions. `lo`, `nelts` are replaced by a more traditional `head` and`tail`. The `Vec<Option<T>>` is replaced by a bare pointer that is managed by the `RingBuf` itself. This also expects the ring buffer to always be size that is a power of 2.

This change also includes a number of new tests to cover the some areas that could be of concern with manual memory management.

The benchmarks have been reworked since the old ones were benchmarking of the Ring buffers growth rather then the actual test.

The unit test suite have been expanded, and exposed some bugs in `fn get()` and `fn get_mut()`

## Benchmark
**Before:**
```
test ring_buf::tests::bench_grow_1025                      ... bench:      8919 ns/iter (+/- 87)
test ring_buf::tests::bench_iter_1000                      ... bench:       924 ns/iter (+/- 28)
test ring_buf::tests::bench_mut_iter_1000                  ... bench:       918 ns/iter (+/- 6)
test ring_buf::tests::bench_new                            ... bench:        15 ns/iter (+/- 0)
test ring_buf::tests::bench_pop_100                        ... bench:       294 ns/iter (+/- 9)
test ring_buf::tests::bench_pop_front_100                  ... bench:       948 ns/iter (+/- 32)
test ring_buf::tests::bench_push_back_100                  ... bench:       291 ns/iter (+/- 16)
test ring_buf::tests::bench_push_front_100                 ... bench:       311 ns/iter (+/- 27
```
**After:**
```
test ring_buf::tests::bench_grow_1025                      ... bench:      2209 ns/iter (+/- 169)
test ring_buf::tests::bench_iter_1000                      ... bench:       534 ns/iter (+/- 27)
test ring_buf::tests::bench_mut_iter_1000                  ... bench:       515 ns/iter (+/- 28)
test ring_buf::tests::bench_new                            ... bench:        11 ns/iter (+/- 0)
test ring_buf::tests::bench_pop_100                        ... bench:       170 ns/iter (+/- 5)
test ring_buf::tests::bench_pop_front_100                  ... bench:       171 ns/iter (+/- 11)
test ring_buf::tests::bench_push_back_100                  ... bench:       172 ns/iter (+/- 13)
test ring_buf::tests::bench_push_front_100                 ... bench:       158 ns/iter (+/- 12)

```
@bors bors closed this Nov 17, 2014
@bors bors merged commit 6277e3b into rust-lang:master Nov 17, 2014
@Gankra
Copy link
Contributor

Gankra commented Nov 17, 2014

Yessss eat @bors it meeeeerged!

🎉

@ghost
Copy link
Author

ghost commented Nov 17, 2014

@gankro 4th times the charm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.