Skip to content

Eliminate bounds checking in slice::Windows - #77617

Merged
bors merged 2 commits into
rust-lang:masterfrom
AnthonyMikh:slice_windows_no_bounds_checking
Oct 7, 2020
Merged

Eliminate bounds checking in slice::Windows#77617
bors merged 2 commits into
rust-lang:masterfrom
AnthonyMikh:slice_windows_no_bounds_checking

Conversation

@AnthonyMikh

@AnthonyMikhAnthonyMikh commented Oct 6, 2020

Copy link
Copy Markdown
Contributor

This is how <core::slice::Windows as Iterator>::next looks right now:

fnnext(&mutself) -> Option<&'a[T]>{ifself.size > self.v.len(){None}else{let ret = Some(&self.v[..self.size]);self.v = &self.v[1..];
ret
}}

The line with self.v = &self.v[1..]; relies on assumption that self.v is definitely not empty at this point. Else branch is taken when self.size <= self.v.len(), so self.v can be empty if self.size is zero. In practice, since Windows is never created directly but rather trough [T]::windows which panics when size is zero, self.size is never zero. However, the compiler doesn't know about this check, so it keeps the code which checks bounds and panics.

Using NonZeroUsize lets the compiler know about this invariant and reliably eliminate bounds checking without unsafe on -O2. Here is assembly of Windows<'a, u32>::next before and after this change (goldbolt):

Before
example::next:
push rax
mov rcx, qword ptr [rdi + 8]
mov rdx, qword ptr [rdi + 16]
cmp rdx, rcx
jbe .LBB0_2
xor eax, eax
pop rcx
ret
.LBB0_2:
test rcx, rcx
je .LBB0_5
mov rax, qword ptr [rdi]
mov rsi, rax
add rsi, 4
add rcx, -1
mov qword ptr [rdi], rsi
mov qword ptr [rdi + 8], rcx
pop rcx
ret
.LBB0_5:
lea rdx, [rip + .L__unnamed_1]
mov edi, 1
xor esi, esi
call qword ptr [rip + core::slice::slice_index_order_fail@GOTPCREL]
ud2
.L__unnamed_2:
.ascii "./example.rs"
.L__unnamed_1:
.quad .L__unnamed_2
.asciz "\f\000\000\000\000\000\000\000\016\000\000\000\027\000\000"
After
example::next:
mov rcx, qword ptr [rdi + 8]
mov rdx, qword ptr [rdi + 16]
cmp rdx, rcx
jbe .LBB0_2
xor eax, eax
ret
.LBB0_2:
mov rax, qword ptr [rdi]
lea rsi, [rax + 4]
add rcx, -1
mov qword ptr [rdi], rsi
mov qword ptr [rdi + 8], rcx
ret

Note the lack of call to core::slice::slice_index_order_fail in second snippet.

Possible reasons not to merge this PR:

  • this changes the error message on panic in [T]::windows. However, AFAIK this messages are not covered by backwards compatibility policy.

@rust-highfive

Copy link
Copy Markdown
Contributor

r? @Mark-Simulacrum

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

@rust-highfiverust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Oct 6, 2020
lcnr
lcnr approved these changes Oct 6, 2020
@lcnr

lcnr commented Oct 6, 2020

Copy link
Copy Markdown
Contributor

Would you mind adding a codegen test which checks that windows().next() is unable to panic?

@lcnr

lcnr commented Oct 6, 2020

Copy link
Copy Markdown
Contributor

I expect that you need something like this: https://github.com/bugadani/rust/blob/1d157ce797dddcee16a577796199b1144b4f7f34/src/test/codegen/enum-bounds-check-issue-13926.rs (potentially without min-llvm-version, depending on how stable this optimization is)

@jyn514jyn514 added I-slow Issue: Problems and improvements with respect to performance of generated code. T-libs-api [DEPRECATED; DO NOT USE] labels Oct 6, 2020
@AnthonyMikh

Copy link
Copy Markdown
ContributorAuthor

Would you mind adding a codegen test which checks that windows().next() is unable to panic?

@lcnr I can add it. However, I see no way to run codegen check. How can I do it?

@lcnr

lcnr commented Oct 6, 2020

Copy link
Copy Markdown
Contributor

./x.py test src/test/codegen/name_of_your_test.rs --stage 0 should work afaik

@AnthonyMikh

Copy link
Copy Markdown
ContributorAuthor

@lcnr I've added test but I am not sure if it is correct. Or should I test Windows::next directly?

@lcnr

lcnr commented Oct 6, 2020

Copy link
Copy Markdown
Contributor

seems good to me. Does this test fail without your changes?

@AnthonyMikh

Copy link
Copy Markdown
ContributorAuthor

seems good to me. Does this test fail without your changes?

It... Passes without my changes.

Apparently I don't know how to write codegen tests

@lcnr

lcnr commented Oct 6, 2020

Copy link
Copy Markdown
Contributor

Looks like you need slice_start_index_len_failhttps://rust.godbolt.org/z/eb3jfo

@AnthonyMikh
AnthonyMikhforce-pushed the slice_windows_no_bounds_checking branch from 0f52d1b to b5096c1CompareOctober 6, 2020 18:34
@AnthonyMikh

Copy link
Copy Markdown
ContributorAuthor

@lcnr without my changes this codegen test fails on --stage 1 but passes with my changes. Thanks for directing.

@AnthonyMikh

Copy link
Copy Markdown
ContributorAuthor

BTW I only now realised that my rather mechanical changes in other methods seem also to eliminate bounds check in next_back, last and nth_back. Should I make codegen tests for them as well?

@AnthonyMikh
AnthonyMikhforce-pushed the slice_windows_no_bounds_checking branch from b5096c1 to a8f098bCompareOctober 6, 2020 18:47
@lcnr

lcnr commented Oct 6, 2020

Copy link
Copy Markdown
Contributor

hmm, it probably won't hurt to use explicitly extend the test to also add functions for these methods 🤷 feel free to do so

r? @lcnr
otherwise r=me

@AnthonyMikh
AnthonyMikhforce-pushed the slice_windows_no_bounds_checking branch from a8f098b to 61fca88CompareOctober 7, 2020 09:58
@AnthonyMikh

Copy link
Copy Markdown
ContributorAuthor

@lcnr I've added codegen tests for Windows methods which benefit from my changes. PR is ready to merge.

@lcnr

lcnr commented Oct 7, 2020

Copy link
Copy Markdown
Contributor

Looking at the emitted code on https://godbolt.org/z/T716cx

It looks to me like next calls core::slice::index::slice_start_index_len_fail on the currently nightly and next_back calls core::slice::index::slice_end_index_len_fail rn.
Does next and next_back alone fail with the current nightly?

In general this looks to me like these tests might not be as useful as I thought, considering that method renames make them somewhat useless. Does it work to instead test with the following?

// CHECK-NOT: panic// CHECK-NOT: fail

@AnthonyMikh
AnthonyMikhforce-pushed the slice_windows_no_bounds_checking branch from 61fca88 to e699e83CompareOctober 7, 2020 13:17
@AnthonyMikh

Copy link
Copy Markdown
ContributorAuthor

Indeed, it correctly checks with panic and fail as well.

@lcnr

lcnr commented Oct 7, 2020

Copy link
Copy Markdown
Contributor

@bors r+

@bors

bors commented Oct 7, 2020

Copy link
Copy Markdown
Collaborator

📌 Commit e699e83 has been approved by lcnr

@borsbors 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 Oct 7, 2020
#[inline]
pub fn windows(&self, size: usize) -> Windows<'_, T> {
assert_ne!(size, 0);
let size = NonZeroUsize::new(size).expect("size is zero");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have guideline about what the phrase used in expect call ?

Suggested change
let size = NonZeroUsize::new(size).expect("size is zero");
let size = NonZeroUsize::new(size).expect("`size` cannot be zero");

@AnthonyMikhAnthonyMikhOct 7, 2020

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I am not sure, but before my change panic message was assertion failed: size != 0.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is bike-shredding so don't worry much about this.

@AnthonyMikh

AnthonyMikh commented Oct 7, 2020

Copy link
Copy Markdown
ContributorAuthor

BTW while inspecting the assembly on order to determine which changed methods benefit from my changes I checked nth_back, and I was surprised to see the panicking functions still remain in resulting binary even with -C opt-level=3. After my changes nth_back looks like so:

// impl DoubleEndedIteator ...pubfnnth_back(&mutself,n:usize) -> Option<&'a[T]>{let(end, overflow) = self.v.len().overflowing_sub(n);if end < self.size.get() || overflow {self.v = &[];None}else{let ret = &self.v[end - self.size.get()..end];self.v = &self.v[..end - 1];Some(ret)}}

Here else branch is taken when both conditions in if are false. !overflow means that n <= self.v.len() and thus end <= self.v.len(). !(end < self.size.get()) means end >= self.size.get() (so end - self.size.get() is greater or equal to zero and doesn't underflow) and, since self.size is NonZeroUsize, by transitivity we get end > 0. These two invariants guarantees that both slicings of slice.v are performed in-bounds.

The compiler understands that end > 0 -- in fact, putting assert!(end > 0) in the beginning of else branch doesn't change assembly code at all. However, it fails to understand the first invariant -- putting assert!(end <= self.v.len()) generates assembly with panic-related code. Calling unsafe {core::intrinsics::assume(end <= self.v.len()); }doesn't elide panic-related code (though it merges two code paths to them). I believe that it can be called a compiler bug since both invariants can be established via data-flow analysis. Should I file an issue for this?

@scottmcm

Copy link
Copy Markdown
Member

TBH I was surprised that NonZeroUSize was working for this, since it works reliably for layout optimizations but often doesn't work for code optimization, see #49572

@bors

bors commented Oct 7, 2020

Copy link
Copy Markdown
Collaborator

⌛ Testing commit e699e83 with merge 28928c7...

@bors

bors commented Oct 7, 2020

Copy link
Copy Markdown
Collaborator

☀️ Test successful - checks-actions, checks-azure
Approved by: lcnr
Pushing 28928c7 to master...

@borsbors added the merged-by-bors This PR was explicitly merged by bors. label Oct 7, 2020
@bors
bors merged commit 28928c7 into rust-lang:masterOct 7, 2020
@rustbotrustbot added this to the 1.49.0 milestone Oct 7, 2020
@AnthonyMikh
AnthonyMikh deleted the slice_windows_no_bounds_checking branch October 8, 2020 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-slowIssue: Problems and improvements with respect to performance of generated code.merged-by-borsThis PR was explicitly merged by bors.S-waiting-on-borsStatus: Waiting on bors to run and complete tests. Bors will change the label on completion.T-libs-api[DEPRECATED; DO NOT USE]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@AnthonyMikh@rust-highfive@lcnr@bors@scottmcm@tesuji@Mark-Simulacrum@jyn514@rustbot