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

slice::split_mut does not elide bound checks in release mode #86313

Closed
daniel5151 opened this issue Jun 14, 2021 · 4 comments · Fixed by #99223
Closed

slice::split_mut does not elide bound checks in release mode #86313

daniel5151 opened this issue Jun 14, 2021 · 4 comments · Fixed by #99223
Assignees
Labels
A-codegen Area: Code generation A-slice Area: `[T]` C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@daniel5151
Copy link

daniel5151 commented Jun 14, 2021

As part of an ongoing effort to eliminate all panics from one of my no_std projects, I discovered that slice::split_mut does not elide bounds checks in release mode, whereas slice::split does.

This issue also affects slice::splitn_mut.

The following examples illustrate this issue:

pub fn split(s: &mut [u8]) {
    for s in s.split(|b| *b == b' ') {
        black_box(s);
    }
}

// stable block_box pulled from
// https://docs.rs/bencher/0.1.5/src/bencher/lib.rs.html#590-596
pub fn black_box<T>(dummy: T) -> T {
    unsafe {
        let ret = core::ptr::read_volatile(&dummy);
        core::mem::forget(dummy);
        ret
    }
}

The generated asm for split elides all bounds checks:

playground::split:
	sub	rsp, 16

.LBB0_1:
	test	rsi, rsi
	je	.LBB0_2
	lea	rax, [rdi + 1]
	xor	ecx, ecx

.LBB0_4:
	cmp	byte ptr [rax - 1], 32
	je	.LBB0_7
	add	rcx, 1
	add	rax, 1
	cmp	rsi, rcx
	jne	.LBB0_4
	xor	edx, edx
	mov	rax, rdi
	mov	rcx, rsi
	test	rdi, rdi
	jne	.LBB0_9
	jmp	.LBB0_10

.LBB0_2:
	xor	esi, esi
	mov	rax, rdi
	xor	edx, edx
	xor	ecx, ecx
	test	rdi, rdi
	jne	.LBB0_9
	jmp	.LBB0_10

.LBB0_7:
	mov	rdx, rcx
	not	rdx
	add	rsi, rdx
	mov	dl, 1
	test	rdi, rdi
	je	.LBB0_10

.LBB0_9:
	mov	qword ptr [rsp], rdi
	mov	qword ptr [rsp + 8], rcx
	mov	rcx, qword ptr [rsp + 8]
	mov	rcx, qword ptr [rsp]
	mov	rdi, rax
	test	dl, dl
	jne	.LBB0_1

.LBB0_10:
	add	rsp, 16
	ret

Unfortunately, the bounds check is reintroduced when split_mut is used instead:

pub fn split_mut(s: &mut [u8]) {
    for s in s.split_mut(|b| *b == b' ') {
        black_box(s);
    }
}
playground::split_mut:
	sub	rsp, 24
	lea	r8, [rip + .L__unnamed_1]
	jmp	.LBB0_1

.LBB0_2:
	xor	eax, eax
	mov	rcx, r8
	xor	r9d, r9d
	xor	esi, esi

.LBB0_9:
	mov	qword ptr [rsp + 8], rdi
	mov	qword ptr [rsp + 16], rsi
	mov	rdx, qword ptr [rsp + 16]
	mov	rdx, qword ptr [rsp + 8]
	mov	rsi, rax
	mov	rdi, rcx
	test	r9b, r9b
	je	.LBB0_10

.LBB0_1:
	test	rsi, rsi
	je	.LBB0_2
	lea	rcx, [rdi + 1]
	xor	edx, edx

.LBB0_4:
	cmp	byte ptr [rcx - 1], 32
	je	.LBB0_7
	add	rdx, 1
	add	rcx, 1
	cmp	rsi, rdx
	jne	.LBB0_4
	xor	eax, eax
	mov	rcx, r8
	xor	r9d, r9d
	jmp	.LBB0_9

.LBB0_7:
	cmp	rsi, rdx
	je	.LBB0_11
	mov	rax, rdx
	not	rax
	add	rax, rsi
	mov	r9b, 1
	mov	rsi, rdx
	jmp	.LBB0_9

.LBB0_10:
	add	rsp, 24
	ret

.LBB0_11:
	lea	rdx, [rip + .L__unnamed_2]
	mov	edi, 1
	xor	esi, esi
	call	qword ptr [rip + core::slice::index::slice_start_index_len_fail@GOTPCREL]
	ud2

.L__unnamed_1:

.L__unnamed_3:
	.ascii	"/rustc/9bc8c42bb2f19e745a63f3445f1ac248fb015e53/library/core/src/slice/iter.rs"

.L__unnamed_2:
	.quad	.L__unnamed_3
	.asciz	"N\000\000\000\000\000\000\000z\002\000\000\037\000\000"

It seems that the following code in the SplitMut iterator is responsible for introducing the bounds check (specifically the array index on line 640):

let idx_opt = {
// work around borrowck limitations
let pred = &mut self.pred;
self.v.iter().position(|x| (*pred)(x))
};
match idx_opt {
None => self.finish(),
Some(idx) => {
let tmp = mem::replace(&mut self.v, &mut []);
let (head, tail) = tmp.split_at_mut(idx);
self.v = &mut tail[1..];
Some(head)
}
}

Meta

rustc --version --verbose:

rustc 1.52.1 (9bc8c42bb 2021-05-09)
binary: rustc
commit-hash: 9bc8c42bb2f19e745a63f3445f1ac248fb015e53
commit-date: 2021-05-09
host: x86_64-unknown-linux-gnu
release: 1.52.1
LLVM version: 12.0.0

Also tested on 1.55.0-nightly (2021-06-13 f586d79d183d144e0cbf) from the Rust playground.

@daniel5151 daniel5151 added the C-bug Category: This is a bug. label Jun 14, 2021
@leonardo-m
Copy link

Nice catch.

As part of an ongoing effort to eliminate all panics from one of my no_std projects

Oh, I'm doing the same (but in regular std code).
stdsort is full of bound tests (issue #84173).
I'm dreaming of having red squiggles under divisions/mods/rems and array/slice accesses where a bound test hasn't being removed. And built-in annotations like https://crates.io/crates/no-panic . And more :)

daniel5151 added a commit to daniel5151/gdbstub that referenced this issue Jun 19, 2021
this is something I've been meaning to do for a _long_ time now, and
after many failed attempts and false-starts, I've finally done it! if
you inspect the asm output of `example_no_std`, you'll find that there
is absolutely _zero_ panic handling machinery in the final binary!

This has also resulted in a _substantial_ drop in binary size, as those
bounds checks and panicking machinery were taking up a lot of space!

removing panicking code ended up requiring 3 different approaches:

1. Rewriting array-indexing operations to use simpler indexing, which
the compiler is able to optimize better. e.g: see the code used to index
into the `target.xml` buffer in `base.rs`
2. Adding a sprinkle of unsafe code to certain array-indexing operations
that the compiler is unsable to prove are safe. This was only done in
two places: `decode_hex_buf` and `PacketBuf`.
3. Manually re-implementing the standard library's `slice::split_mut`
and `slice::splitn_mut` methods to elide a bounds check.
  - Tracked upstream via rust-lang/rust#86313

Introducing unsafe code isn't something I take lightly, and while I've
done my best to audit and validate the unsafe code I've written, I did
end up including an optional `paranoid_unsafe` feature which gives
end-users the option to opt-out of `gdbstub`'s no-panic guarantee in
exchange for some additional peace of mind.
@inquisitivecrystal
Copy link
Contributor

inquisitivecrystal commented Jun 20, 2021

@rustbot label +A-codegen +A-slice +T-libs +T-compiler

@rustbot rustbot added A-slice Area: `[T]` T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. A-codegen Area: Code generation labels Jun 20, 2021
@saethlin
Copy link
Member

@rustbot claim

@daniel5151
Copy link
Author

Awesome, thanks for looking into this @saethlin!

workingjubilee pushed a commit to tcdi/postgrestd that referenced this issue Sep 15, 2022
Rearrange slice::split_mut to remove bounds check

Closes rust-lang/rust#86313

Turns out that all we need to do here is reorder the bounds checks to convince LLVM that all the bounds checks can be removed. It seems like LLVM just fails to propagate the original length information past the first bounds check and into the second one. With this implementation it doesn't need to, each check can be proven inbounds based on the one immediately previous.

I've gradually convinced myself that this implementation is unambiguously better based on the above logic, but maybe this is still deserving of a codegen test?

Also the mentioned borrowck limitation no longer seems to exist.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-codegen Area: Code generation A-slice Area: `[T]` C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants