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

slices: fix ZST slice iterators making up pointers; debug_assert alignment in from_raw_parts #52206

Merged
merged 9 commits into from
Aug 2, 2018

Conversation

RalfJung
Copy link
Member

@RalfJung RalfJung commented Jul 10, 2018

This fixes the problem that we are fabricating pointers out of thin air. I also managed to share more code between the mutable and shared iterators, while reducing the amount of macros.

I am not sure how useful it really is to add a debug_assert! in libcore. Everybody gets a release version of that anyway, right? Is there at least a CI job that runs the test suite with a debug version?

Fixes #42789

@rust-highfive
Copy link
Collaborator

r? @shepmaster

(rust_highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 10, 2018
@@ -581,7 +607,7 @@ impl<T> [T] {
pub fn iter(&self) -> Iter<T> {
unsafe {
let p = if mem::size_of::<T>() == 0 {
1 as *const _
NonNull::dangling().as_ptr() as *const _
Copy link
Member

Choose a reason for hiding this comment

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

This makes me think that Iter::ptr should be a NonNull<T> instead of a *const T...

Copy link
Member Author

@RalfJung RalfJung Jul 10, 2018

Choose a reason for hiding this comment

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

It could be. Same for end. (Though I have plans for #42789 that would need tweaking if end were to become a NonNull.)

Do you want me to change that in this PR?

Copy link
Member

Choose a reason for hiding this comment

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

I have no strong feelings about it. If it's easy and it's mostly lines that were changing anyway, might as well; if it's annoying or makes the PR a bunch longer, then probably not worth doing right now. (end not being NonNull is also totally fine by me.)

Copy link
Member Author

@RalfJung RalfJung Jul 11, 2018

Choose a reason for hiding this comment

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

It's annoying because tons of code here does raw pointer operations, so there'd be a lot of to_ptr and new_unchecked and so on.

@rust-highfive
Copy link
Collaborator

The job x86_64-gnu-llvm-5.0 of your PR failed on Travis (raw log). Through arcane magic we have determined that the following fragments from the build log may contain information about the problem.

Click to expand the log.

[00:03:59] travis_fold:start:tidy
travis_time:start:tidy
tidy check
[00:03:59] tidy error: /checkout/src/libcore/tests/slice.rs:389: TODO is deprecated; use FIXME
[00:03:59] tidy error: /checkout/src/libcore/tests/slice.rs:416: TODO is deprecated; use FIXME
[00:04:01] some tidy checks failed
[00:04:01] 
[00:04:01] 
[00:04:01] command did not execute successfully: "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0-tools-bin/tidy" "/checkout/src" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0/bin/cargo" "--no-vendor" "--quiet"
[00:04:01] 
[00:04:01] 
[00:04:01] failed to run: /checkout/obj/build/bootstrap/debug/bootstrap test src/tools/tidy
[00:04:01] Build completed unsuccessfully in 0:00:50
[00:04:01] Build completed unsuccessfully in 0:00:50
[00:04:01] make: *** [tidy] Error 1
[00:04:01] Makefile:79: recipe for target 'tidy' failed

The command "stamp sh -x -c "$RUN_SCRIPT"" exited with 2.
travis_time:start:1216931d
$ date && (curl -fs --head https://google.com | grep ^Date: | sed 's/Date: //g' || true)
---
travis_time:end:055475b8:start=1531206865919441469,finish=1531206865925966084,duration=6524615
travis_fold:end:after_failure.3
travis_fold:start:after_failure.4
travis_time:start:1d7bdf28
$ head -30 ./obj/build/x86_64-unknown-linux-gnu/native/asan/build/lib/asan/clang_rt.asan-dynamic-i386.vers || true
head: cannot open ‘./obj/build/x86_64-unknown-linux-gnu/native/asan/build/lib/asan/clang_rt.asan-dynamic-i386.vers’ for reading: No such file or directory
travis_fold:end:after_failure.4
travis_fold:start:after_failure.5
travis_time:start:1898c4c7
$ dmesg | grep -i kill

I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact @TimNN. (Feature Requests)

@RalfJung
Copy link
Member Author

I should add that -- given that ZST slices seem to have zero test coverage before my change -- I am somewhat worried I broke something somewhere...

@@ -80,7 +80,7 @@ macro_rules! slice_offset {
($ptr:expr, $by:expr) => {{
let ptr = $ptr;
if size_from_ptr(ptr) == 0 {
(ptr as *mut i8).wrapping_offset($by) as _
(ptr as *mut i8).wrapping_offset($by.wrapping_mul(align_from_ptr(ptr) as isize)) as _
Copy link
Member

Choose a reason for hiding this comment

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

Is there enough address space to do this, given the capacities claimed by RawVec? I can apparently make a usize::MAX-length Vec of a align-8 ZST: https://play.rust-lang.org/?gist=c0b3d266bfbd34448d97e00f1408c946&version=stable&mode=release&edition=2015

Should the following change to !0 / mem::align_of::<T>()?

pub fn cap(&self) -> usize {
if mem::size_of::<T>() == 0 {
!0
} else {
self.cap
}
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, good point.

Is it enough to change that in raw_vec or are there other ways to create huge slices?

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this problem disappear if we fix #42789 by using start as a real non-fabricated pointer and end as a counter? I assume that's the plan you mentioned, so if we're likely doing that anyway, maybe avoid some churn in user visible behavior by going straight to that solution.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes it does. In fact I am working on that next step already. I thought it would be easier for reviewers etc. to split this into two PRs, but if you prefer I can certainly finish that up and add it here (or make a new PR because there are going to be drastically more changes).

@RalfJung RalfJung changed the title slices: fix alignment for ZST slices; debug_assert alignment in from_raw_parts slices: fix ZST slice iterators making up pointers; debug_assert alignment in from_raw_parts Jul 11, 2018
@RalfJung
Copy link
Member Author

RalfJung commented Jul 11, 2018

All right, I added "no longer make up pointers" to this PR. Instead of just putting the length into end, I put ptr+len into end so that ptr == end is uniformly the condition for the iterator being empty. That saves us a bunch of if mem::size_of::<T> == 0.

This is based on @bluss's work in #46223. In that PR, quite a few benchmarks were done to make sure this does not regress performance. How would I run these benchmarks?

// iterator, this works for both ZST and non-ZST.
// For a ZST we would usually do `self.end = self.ptr`, but since
// we will not give out an address any more after this there is no
// way to observe the difference.
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't I still get an address from .as_slice().as_ptr()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm... okay it is observable but only as a raw pointer, not as a reference. So there's no soundness problem.

I can still change it, if you prefer.

Copy link
Member

Choose a reason for hiding this comment

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

Up to you; the comment tweak is probably fine since I'm not sure why anyone would be looking at the address of a post-iteration slice anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, then I'd prefer to keep it this way.

#[inline(always)]
unsafe fn post_inc_start(&mut self, offset: isize) -> * $raw_mut T {
if mem::size_of::<T>() == 0 {
self.end = (self.end as isize).wrapping_sub(offset) as * $raw_mut T;
Copy link
Member

Choose a reason for hiding this comment

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

wrapping_sub looks like a typo in a method named inc?

Copy link
Contributor

Choose a reason for hiding this comment

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

I thought so too at first, but no, for ZST slices the start pointer should not change as the iterator advances, and this wrapping_sub is decrementing the length. However, since it's apparently somewhat subtle, there should be a comment spelling this out.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah in fact I used wrapping_add at first and wondered why everything exploded. ;)

I'll add a comment.

@RalfJung
Copy link
Member Author

Should I be doing benchmarks before we land this? How would I go about that?

@shepmaster
Copy link
Member

r? @alexcrichton

@shepmaster
Copy link
Member

I think that checking the performance of slices would be a great idea, seeing as how fundamental they are. An accidental extra conditional or missed inline seems like it could have a big impact. I'm not sure if a regular perf run would be enough to feel good about it as I don't really know what that test suite looks like.

@RalfJung
Copy link
Member Author

RalfJung commented Jul 16, 2018

I've not done benchmarking like it was done for #46223 before (or any kind of benchmarking with Rust ever, really), so I'd need some pointers for how to do that.

EDIT: Hm, seems like they just ran some crate's bench suite? But how does that get the old/new numbers...?

@Mark-Simulacrum
Copy link
Member

Generally speaking you can probably write up some #[bench] tests and compare against last nightly if you don't want to rebuild. We can also run perf.rlo, but that's more opaque since any difference here is likely quite minor.

@RalfJung
Copy link
Member Author

I don't have any experience coming up with bench tests, is there something I should be on the watch for?

I noticed there is some stuff in libcore/benches/slice.rs, but not much. That produces output like

test slice::binary_search_l1                               ... bench:          55 ns/iter (+/- 2)
test slice::binary_search_l1_with_dups                     ... bench:          44 ns/iter (+/- 1)
test slice::binary_search_l2                               ... bench:          74 ns/iter (+/- 3)
test slice::binary_search_l2_with_dups                     ... bench:          74 ns/iter (+/- 3)
test slice::binary_search_l3                               ... bench:         162 ns/iter (+/- 11)
test slice::binary_search_l3_with_dups                     ... bench:         160 ns/iter (+/- 11)

How do I get these tables with a "+/- %" column?

@shepmaster
Copy link
Member

How do I get these tables with a "+/- %" column?

Probably via cargo-benchcmp

@alexcrichton
Copy link
Member

@bors: try

Can't hurt at least to have a try build!

@bors
Copy link
Contributor

bors commented Jul 16, 2018

⌛ Trying commit f567aea with merge 3e9fa0ba8d51c2a5fe7cf53be75b7e4bf1370bd4...

// For a ZST we would usually do `self.end = self.ptr`, but since
// we will not give out an reference any more after this there is no
// way to observe the difference except for raw pointers.
self.ptr = self.end;
Copy link
Member

Choose a reason for hiding this comment

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

Previously in next there's an assume(!self.end.is_null()) but only if T has a nonzero size. That implies to me that if T is a ZST it's possible for self.end to be null (I think wraparound?). In this case, this is setting self.ptr to null, right? Wouldn't that break the in-memory representation of Option<&[T]> as well as the first assume above in the call to next?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh good point, I don't think we can rule that out.

I guess I'll hope for the optimizer to remove the conditional here as well, then. :)

assume(!ptr.is_null());

let end = if mem::size_of::<T>() == 0 {
(ptr as usize).wrapping_add(self.len()) as *mut _
Copy link
Member

Choose a reason for hiding this comment

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

Oh-so-long-ago the casting here between integers and pointers was found to inhibit optimizations when it comes to ZSTs, so would it be possible to use a similar strategy as that PR to avoid the pointer<->int conversions here?

Copy link
Member Author

@RalfJung RalfJung Jul 18, 2018

Choose a reason for hiding this comment

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

I just tried that, and it makes the tests fail...?!?

EDIT: Ah, it still increments in multiples of size:of::<T>, which the docs don't really say...

@bors
Copy link
Contributor

bors commented Jul 16, 2018

☀️ Test successful - status-travis
State: approved= try=True

@alexcrichton
Copy link
Member

@rust-timer build 3e9fa0ba8d51c2a5fe7cf53be75b7e4bf1370bd4

@rust-timer
Copy link
Collaborator

Success: Queued 3e9fa0ba8d51c2a5fe7cf53be75b7e4bf1370bd4 with parent 3d5753f, comparison URL.

@bors bors added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 1, 2018
@RalfJung
Copy link
Member Author

RalfJung commented Aug 1, 2018

Heh, looks like there's actual code that which creates unaligned slices? This is in the rg test suite.

Should I back out the debug_assert! part of this PR? it's not really related anyway. I want to follow up on this but would prefer to land the rest first.

@RalfJung
Copy link
Member Author

RalfJung commented Aug 1, 2018

@bors r=alexcrichton

@bors
Copy link
Contributor

bors commented Aug 1, 2018

📌 Commit 9fcf2c9 has been approved by alexcrichton

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 1, 2018
@bors
Copy link
Contributor

bors commented Aug 2, 2018

⌛ Testing commit 9fcf2c9 with merge 1d9405f...

bors added a commit that referenced this pull request Aug 2, 2018
slices: fix ZST slice iterators making up pointers; debug_assert alignment in from_raw_parts

This fixes the problem that we are fabricating pointers out of thin air. I also managed to share more code between the mutable and shared iterators, while reducing the amount of macros.

I am not sure how useful it really is to add a `debug_assert!` in libcore. Everybody gets a release version of that anyway, right? Is there at least a CI job that runs the test suite with a debug version?

Fixes #42789
@bors
Copy link
Contributor

bors commented Aug 2, 2018

☀️ Test successful - status-appveyor, status-travis
Approved by: alexcrichton
Pushing 1d9405f to master...

@bors bors merged commit 9fcf2c9 into rust-lang:master Aug 2, 2018
@gnzlbg
Copy link
Contributor

gnzlbg commented Aug 2, 2018

Is there at least a CI job that runs the test suite with a debug version?

So what's the answer to this? If there isn't there should be one.

@kennytm
Copy link
Member

kennytm commented Aug 2, 2018

We could run some tests (up to 30 minutes) in the x86_64-gnu-debug job.

@RalfJung
Copy link
Member Author

RalfJung commented Aug 2, 2018

Give that I got a CI failure when I tried to land this with the debug_assert!, something is tested with debug-assertions enabled. ripgrep, at least. I'd assume then that this also runs the usual libcore, libstd, run-pass tests?

@kennytm
Copy link
Member

kennytm commented Aug 3, 2018

Ah right. We do test with debug_assertions enabled, but they are disabled in dist.

rust/src/ci/run.sh

Lines 55 to 76 in 40e4b6e

if [ "$DEPLOY$DEPLOY_ALT" != "" ]; then
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --release-channel=$RUST_RELEASE_CHANNEL"
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --enable-llvm-static-stdcpp"
if [ "$NO_LLVM_ASSERTIONS" = "1" ]; then
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --disable-llvm-assertions"
elif [ "$DEPLOY_ALT" != "" ]; then
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --enable-llvm-assertions"
fi
else
# We almost always want debug assertions enabled, but sometimes this takes too
# long for too little benefit, so we just turn them off.
if [ "$NO_DEBUG_ASSERTIONS" = "" ]; then
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --enable-debug-assertions"
fi
# In general we always want to run tests with LLVM assertions enabled, but not
# all platforms currently support that, so we have an option to disable.
if [ "$NO_LLVM_ASSERTIONS" = "" ]; then
RUST_CONFIGURE_ARGS="$RUST_CONFIGURE_ARGS --enable-llvm-assertions"
fi
fi
. The x86_64-gnu-debug job enables debug symbols in additional to debug assertions.

@RalfJung RalfJung deleted the zst-slices branch August 16, 2018 10:53
@nnethercote
Copy link
Contributor

This turned out to be a clear win on a few benchmarks, of up to 3.9%:
https://perf.rust-lang.org/compare.html?start=97085f9fb0736b322dc216db3655da780b4d8041&end=1d9405fb6caa5eac18e5a28685e4f30dcbde6d45&stat=instructions:u

Nice job!

@RalfJung
Copy link
Member Author

Thanks! No idea how that happened, though. ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Iterators over slices of ZST behave strange