-
Notifications
You must be signed in to change notification settings - Fork 13.2k
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
Missed opportunities to eliminate bounds checks #35981
Comments
A reduced test case would help here. |
The thread has a simple test case. Is this not sufficient? Maybe the author of the thread could write something here. I linked this bug report in the thread. |
It's not clear from the thread exactly which bounds checks are not being eliminated that are eliminated by Go. A reduced test case would ideally point out a single array access that has a bounds check in Rust but not in Go. |
There are many examples in the linked thread, and a quick read doesn't make it obvious where there are bounds checks to eliminate. Clearly there are optimization opportunities here, but this thread needs reduced examples with a clear description of which bounds checks are not being removed. |
Disclaimer: I know very little Rust and even less Go. I'm somewhat familiar with LLVM, though, so I took a look, comparing the original Go results at http://www.tapirgames.com/blog/golang-1.7-bce to the Rust as given in the forum post. I'm using Note that the Rust translations do not seem to be quite equivalent: the Go code seems to perform no-op accesses on array slices whereas the Rust versions increment the array values. This complicates the generated code heavily, so to simplify the analysis I replaced increment ops like I'll go through the functions in order. f1, f2, f3, f4
f5Here's the discrepancy. The Go code: func f5(s []int) {
for i := range s {
_ = s[i]
_ = s[i:len(s)]
_ = s[:i+1]
}
} The Rust translation from the forum post, with increment operations removed and fn f5(s: &mut [isize]) {
for i in 0 .. s.len() {
s[i];
for j in i .. s.len() { s[j]; }
for j in 0 .. i + 1 { s[j]; }
}
} Looking at the output of Constructing what I believe to be equivalent C we can see whether this is a #include <stdlib.h>
void f5(long* s, size_t len) {
for (size_t i = 0; i < len; i++) {
for (size_t j = 0; j <= i + 1; j++) {
if (j >= len) {
abort();
}
}
}
} The LLVM IR output by Going back to the original Go, we see that it's constructing array slices instead of doing individual element access like the Rust translation. So we could write a Rust version of the function that loops over slices instead: fn f5(s: &mut [isize]) {
for i in 0 .. s.len() {
s[i];
let len = s.len();
for x in s[i .. len].iter_mut() { *x; }
for x in s[0 .. i + 1].iter_mut() { *x; }
}
} No bounds checks are emitted for the above code, but there's still a no-op loop that is not eliminated. I'm not sure why that happens but I'm sure it's tied with the bounds check issues. Going even closer to the original Go: fn f5(s: &mut [isize]) {
for i in 0 .. s.len() {
s[i];
let len = s.len();
&s[i .. len];
&s[0 .. i + 1];
}
} The bounds check is back: LLVM doesn't realize that f6Codegen is practically identical to f7, f8, f9Same results as Go, no problems here. ConclusionsI made some incorrect conclusions here, please see my comment below for better ones. LLVM bug 810 is not strongly related to this and my examples aren't entirely relevant. In the end I think this boils down to the now over ten years old LLVM bug 810. (There may be more specific bugs for these kinds of use cases related to array bounds checks but I didn't find any.) LLVM simply doesn't have the smarts required to perform reasoning like "if #include <stdlib.h>
void check(unsigned int i, unsigned int len) {
if (i < len) {
if (i > (int)len - 1) {
abort();
}
}
} Note that if the inner condition is changed to the equivalent fn obtuse(s: &mut [isize]) {
if s.len() >= 2 {
for i in 0 .. s.len() {
if i < s.len() - 2 {
&s[0 .. i + 2];
}
}
}
} Just like this C code does: #include <stdlib.h>
void check(unsigned int i, unsigned int len) {
if (len >= 2) {
if (i < len - 2) {
if ((int)i + 2 > len) {
abort();
}
}
}
} For what it's worth, GCC 6.2.1 can't eliminate the checks in any of the above C snippets either, so this doesn't seem like a major deficiency in LLVM. In the end, kudos to the Go folks for beating industry standard C compilers, but this seems like LLVM's problem, not Rust's. |
So, does it make any sense to extend the old LLVM bug report? |
No, I don't think it does. I thought about this some more and did my research more thoroughly and came to the conclusion that while a fix for the old bug 810 might be helpful it's not really pertinent to this case. I'll edit my comment. Instead, this is a combo of two things:
For the general case such as my more obtuse example where I used |
I appreciate your efforts! – Thank you, very much! |
@eddyb I wonder if the root cause of this issue is related to the one of #13018. Real world code sometimes cannot just use the slice iterator etc and must instead rely on using ranges of indices, it would be nice to be able to fix this, though I suspect this is a pretty difficult task. Godbolt playground with Cc @rust-lang/wg-codegen |
Perhaps miri could help here to more generally find where bounds checks can be removed? I don't know much about how, but perhaps if it could figure out the way to push up the index as much as it can then it can interpret it and figure out whether it's still out of bounds in that situation? |
Looks like there's no bounds checks on f5 either anymore (since 1.45): https://rust.godbolt.org/z/f3aja5 With that, I think we can call this resolved, and other bounds check elimination failures should be reported separately. |
A user reported a code example that shows that in Go 1.7 there is bounds check elimination and in rust not.
https://users.rust-lang.org/t/bounds-check-elimination-in-go-1-7/7008
The text was updated successfully, but these errors were encountered: