Skip to content

Commit 547f2ba

Browse files
committed
Auto merge of rust-lang#86988 - thomcc:chunky-splitz-says-no-checking, r=the8472
Carefully remove bounds checks from some chunk iterator functions So, I was writing code that requires the equivalent of `rchunks(N).rev()` (which isn't the same as forward `chunks(N)` — in particular, if the buffer length is not a multiple of `N`, I must handle the "remainder" first). I happened to look at the codegen output of the function (I was actually interested in whether or not a nested loop was being unrolled — it was), and noticed that in the outer `rchunks(n).rev()` loop, LLVM seemed to be unable to remove the bounds checks from the iteration: https://rust.godbolt.org/z/Tnz4MYY8f (this panic was from the split_at in `RChunks::next_back`). After doing some experimentation, it seems all of the `next_back` in the non-exact chunk iterators have the issue: (`Chunks::next_back`, `RChunks::next_back`, `ChunksMut::next_back`, and `RChunksMut::next_back`)... Even worse, the forward `rchunks` iterators sometimes have the issue as well (... but only sometimes). For example https://rust.godbolt.org/z/oGhbqv53r has bounds checks, but if I uncomment the loop body, it manages to remove the check (which is bizarre, since I'd expect the opposite...). I suspect it's highly dependent on the surrounding code, so I decided to remove the bounds checks from them anyway. Overall, this change includes: - All `next_back` functions on the non-`Exact` iterators (e.g. `R?Chunks(Mut)?`). - All `next` functions on the non-exact rchunks iterators (e.g. `RChunks(Mut)?`). I wasn't able to catch any of the other chunk iterators failing to remove the bounds checks (I checked iterations over `r?chunks(_exact)?(_mut)?` with constant chunk sizes under `-O3`, `-Os`, and `-Oz`), which makes sense, since these were the cases where it was harder to prove the bounds check correct to remove... In fact, it took quite a bit of thinking to convince myself that using unchecked_ here was valid — so I'm not really surprised that LLVM had trouble (although compilers are slightly better at this sort of reasoning than humans). A consequence of that is the fact that the `// SAFETY` comment for these are... kinda long... --- I didn't do this for, or even think about it for, any of the other iteration methods; just `next` and `next_back` (where it mattered). If this PR is accepted, I'll file a follow up for someone (possibly me) to look at the others later (in particular, `nth`/`nth_back` looked like they had similar logic), but I wanted to do this now, as IMO `next`/`next_back` are the most important here, since they're what gets used by the iteration protocol. --- Note: While I don't expect this to impact performance directly, the panic is a side effect, which would otherwise not exist in these loops. That is, this could prevent the compiler from being able to move/remove/otherwise rework a loop over these iterators (as an example, it could not delete the code for a loop whose body computes a value which doesn't get used). Also, some like to be able to have confidence this code has no panicking branches in the optimized code, and "no bounds checks" is kinda part of the selling point of Rust's iterators anyway.
2 parents 93e8201 + 9c62455 commit 547f2ba

File tree

2 files changed

+137
-7
lines changed

2 files changed

+137
-7
lines changed

library/core/src/slice/iter.rs

+35-7
Original file line numberDiff line numberDiff line change
@@ -1476,7 +1476,21 @@ impl<'a, T> DoubleEndedIterator for Chunks<'a, T> {
14761476
} else {
14771477
let remainder = self.v.len() % self.chunk_size;
14781478
let chunksz = if remainder != 0 { remainder } else { self.chunk_size };
1479-
let (fst, snd) = self.v.split_at(self.v.len() - chunksz);
1479+
// SAFETY: split_at_unchecked requires the argument be less than or
1480+
// equal to the length. This is guaranteed, but subtle: `chunksz`
1481+
// will always either be `self.v.len() % self.chunk_size`, which
1482+
// will always evaluate to strictly less than `self.v.len()` (or
1483+
// panic, in the case that `self.chunk_size` is zero), or it can be
1484+
// `self.chunk_size`, in the case that the length is exactly
1485+
// divisible by the chunk size.
1486+
//
1487+
// While it seems like using `self.chunk_size` in this case could
1488+
// lead to a value greater than `self.v.len()`, it cannot: if
1489+
// `self.chunk_size` were greater than `self.v.len()`, then
1490+
// `self.v.len() % self.chunk_size` would return nonzero (note that
1491+
// in this branch of the `if`, we already know that `self.v` is
1492+
// non-empty).
1493+
let (fst, snd) = unsafe { self.v.split_at_unchecked(self.v.len() - chunksz) };
14801494
self.v = fst;
14811495
Some(snd)
14821496
}
@@ -1641,7 +1655,8 @@ impl<'a, T> DoubleEndedIterator for ChunksMut<'a, T> {
16411655
let sz = if remainder != 0 { remainder } else { self.chunk_size };
16421656
let tmp = mem::replace(&mut self.v, &mut []);
16431657
let tmp_len = tmp.len();
1644-
let (head, tail) = tmp.split_at_mut(tmp_len - sz);
1658+
// SAFETY: Similar to `Chunks::next_back`
1659+
let (head, tail) = unsafe { tmp.split_at_mut_unchecked(tmp_len - sz) };
16451660
self.v = head;
16461661
Some(tail)
16471662
}
@@ -2410,8 +2425,14 @@ impl<'a, T> Iterator for RChunks<'a, T> {
24102425
if self.v.is_empty() {
24112426
None
24122427
} else {
2413-
let chunksz = cmp::min(self.v.len(), self.chunk_size);
2414-
let (fst, snd) = self.v.split_at(self.v.len() - chunksz);
2428+
let len = self.v.len();
2429+
let chunksz = cmp::min(len, self.chunk_size);
2430+
// SAFETY: split_at_unchecked just requires the argument be less
2431+
// than the length. This could only happen if the expression `len -
2432+
// chunksz` overflows. This could only happen if `chunksz > len`,
2433+
// which is impossible as we initialize it as the `min` of `len` and
2434+
// `self.chunk_size`.
2435+
let (fst, snd) = unsafe { self.v.split_at_unchecked(len - chunksz) };
24152436
self.v = fst;
24162437
Some(snd)
24172438
}
@@ -2485,7 +2506,8 @@ impl<'a, T> DoubleEndedIterator for RChunks<'a, T> {
24852506
} else {
24862507
let remainder = self.v.len() % self.chunk_size;
24872508
let chunksz = if remainder != 0 { remainder } else { self.chunk_size };
2488-
let (fst, snd) = self.v.split_at(chunksz);
2509+
// SAFETY: similar to Chunks::next_back
2510+
let (fst, snd) = unsafe { self.v.split_at_unchecked(chunksz) };
24892511
self.v = snd;
24902512
Some(fst)
24912513
}
@@ -2571,7 +2593,12 @@ impl<'a, T> Iterator for RChunksMut<'a, T> {
25712593
let sz = cmp::min(self.v.len(), self.chunk_size);
25722594
let tmp = mem::replace(&mut self.v, &mut []);
25732595
let tmp_len = tmp.len();
2574-
let (head, tail) = tmp.split_at_mut(tmp_len - sz);
2596+
// SAFETY: split_at_mut_unchecked just requires the argument be less
2597+
// than the length. This could only happen if the expression
2598+
// `tmp_len - sz` overflows. This could only happen if `sz >
2599+
// tmp_len`, which is impossible as we initialize it as the `min` of
2600+
// `self.v.len()` (e.g. `tmp_len`) and `self.chunk_size`.
2601+
let (head, tail) = unsafe { tmp.split_at_mut_unchecked(tmp_len - sz) };
25752602
self.v = head;
25762603
Some(tail)
25772604
}
@@ -2649,7 +2676,8 @@ impl<'a, T> DoubleEndedIterator for RChunksMut<'a, T> {
26492676
let remainder = self.v.len() % self.chunk_size;
26502677
let sz = if remainder != 0 { remainder } else { self.chunk_size };
26512678
let tmp = mem::replace(&mut self.v, &mut []);
2652-
let (head, tail) = tmp.split_at_mut(sz);
2679+
// SAFETY: Similar to `Chunks::next_back`
2680+
let (head, tail) = unsafe { tmp.split_at_mut_unchecked(sz) };
26532681
self.v = tail;
26542682
Some(head)
26552683
}

library/core/tests/slice.rs

+102
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,40 @@ fn test_chunks_nth() {
251251
assert_eq!(c2.next(), None);
252252
}
253253

254+
#[test]
255+
fn test_chunks_next() {
256+
let v = [0, 1, 2, 3, 4, 5];
257+
let mut c = v.chunks(2);
258+
assert_eq!(c.next().unwrap(), &[0, 1]);
259+
assert_eq!(c.next().unwrap(), &[2, 3]);
260+
assert_eq!(c.next().unwrap(), &[4, 5]);
261+
assert_eq!(c.next(), None);
262+
263+
let v = [0, 1, 2, 3, 4, 5, 6, 7];
264+
let mut c = v.chunks(3);
265+
assert_eq!(c.next().unwrap(), &[0, 1, 2]);
266+
assert_eq!(c.next().unwrap(), &[3, 4, 5]);
267+
assert_eq!(c.next().unwrap(), &[6, 7]);
268+
assert_eq!(c.next(), None);
269+
}
270+
271+
#[test]
272+
fn test_chunks_next_back() {
273+
let v = [0, 1, 2, 3, 4, 5];
274+
let mut c = v.chunks(2);
275+
assert_eq!(c.next_back().unwrap(), &[4, 5]);
276+
assert_eq!(c.next_back().unwrap(), &[2, 3]);
277+
assert_eq!(c.next_back().unwrap(), &[0, 1]);
278+
assert_eq!(c.next_back(), None);
279+
280+
let v = [0, 1, 2, 3, 4, 5, 6, 7];
281+
let mut c = v.chunks(3);
282+
assert_eq!(c.next_back().unwrap(), &[6, 7]);
283+
assert_eq!(c.next_back().unwrap(), &[3, 4, 5]);
284+
assert_eq!(c.next_back().unwrap(), &[0, 1, 2]);
285+
assert_eq!(c.next_back(), None);
286+
}
287+
254288
#[test]
255289
fn test_chunks_nth_back() {
256290
let v: &[i32] = &[0, 1, 2, 3, 4, 5];
@@ -809,6 +843,40 @@ fn test_rchunks_nth_back() {
809843
assert_eq!(c2.next_back(), None);
810844
}
811845

846+
#[test]
847+
fn test_rchunks_next() {
848+
let v = [0, 1, 2, 3, 4, 5];
849+
let mut c = v.rchunks(2);
850+
assert_eq!(c.next().unwrap(), &[4, 5]);
851+
assert_eq!(c.next().unwrap(), &[2, 3]);
852+
assert_eq!(c.next().unwrap(), &[0, 1]);
853+
assert_eq!(c.next(), None);
854+
855+
let v = [0, 1, 2, 3, 4, 5, 6, 7];
856+
let mut c = v.rchunks(3);
857+
assert_eq!(c.next().unwrap(), &[5, 6, 7]);
858+
assert_eq!(c.next().unwrap(), &[2, 3, 4]);
859+
assert_eq!(c.next().unwrap(), &[0, 1]);
860+
assert_eq!(c.next(), None);
861+
}
862+
863+
#[test]
864+
fn test_rchunks_next_back() {
865+
let v = [0, 1, 2, 3, 4, 5];
866+
let mut c = v.rchunks(2);
867+
assert_eq!(c.next_back().unwrap(), &[0, 1]);
868+
assert_eq!(c.next_back().unwrap(), &[2, 3]);
869+
assert_eq!(c.next_back().unwrap(), &[4, 5]);
870+
assert_eq!(c.next_back(), None);
871+
872+
let v = [0, 1, 2, 3, 4, 5, 6, 7];
873+
let mut c = v.rchunks(3);
874+
assert_eq!(c.next_back().unwrap(), &[0, 1]);
875+
assert_eq!(c.next_back().unwrap(), &[2, 3, 4]);
876+
assert_eq!(c.next_back().unwrap(), &[5, 6, 7]);
877+
assert_eq!(c.next_back(), None);
878+
}
879+
812880
#[test]
813881
fn test_rchunks_last() {
814882
let v: &[i32] = &[0, 1, 2, 3, 4, 5];
@@ -874,6 +942,40 @@ fn test_rchunks_mut_nth_back() {
874942
assert_eq!(c2.next_back(), None);
875943
}
876944

945+
#[test]
946+
fn test_rchunks_mut_next() {
947+
let mut v = [0, 1, 2, 3, 4, 5];
948+
let mut c = v.rchunks_mut(2);
949+
assert_eq!(c.next().unwrap(), &mut [4, 5]);
950+
assert_eq!(c.next().unwrap(), &mut [2, 3]);
951+
assert_eq!(c.next().unwrap(), &mut [0, 1]);
952+
assert_eq!(c.next(), None);
953+
954+
let mut v = [0, 1, 2, 3, 4, 5, 6, 7];
955+
let mut c = v.rchunks_mut(3);
956+
assert_eq!(c.next().unwrap(), &mut [5, 6, 7]);
957+
assert_eq!(c.next().unwrap(), &mut [2, 3, 4]);
958+
assert_eq!(c.next().unwrap(), &mut [0, 1]);
959+
assert_eq!(c.next(), None);
960+
}
961+
962+
#[test]
963+
fn test_rchunks_mut_next_back() {
964+
let mut v = [0, 1, 2, 3, 4, 5];
965+
let mut c = v.rchunks_mut(2);
966+
assert_eq!(c.next_back().unwrap(), &mut [0, 1]);
967+
assert_eq!(c.next_back().unwrap(), &mut [2, 3]);
968+
assert_eq!(c.next_back().unwrap(), &mut [4, 5]);
969+
assert_eq!(c.next_back(), None);
970+
971+
let mut v = [0, 1, 2, 3, 4, 5, 6, 7];
972+
let mut c = v.rchunks_mut(3);
973+
assert_eq!(c.next_back().unwrap(), &mut [0, 1]);
974+
assert_eq!(c.next_back().unwrap(), &mut [2, 3, 4]);
975+
assert_eq!(c.next_back().unwrap(), &mut [5, 6, 7]);
976+
assert_eq!(c.next_back(), None);
977+
}
978+
877979
#[test]
878980
fn test_rchunks_mut_last() {
879981
let v: &mut [i32] = &mut [0, 1, 2, 3, 4, 5];

0 commit comments

Comments
 (0)