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

new UniformInt bound checks #592

Open
TheIronBorn opened this issue Aug 21, 2018 · 15 comments
Open

new UniformInt bound checks #592

TheIronBorn opened this issue Aug 21, 2018 · 15 comments

Comments

@TheIronBorn
Copy link
Collaborator

TheIronBorn commented Aug 21, 2018

The previous version of UniformInt allowed the compiler to remove bounds checks. It can't do the same for our new widening multiply method.

You can verify with RUSTFLAGS='--emit=asm' cargo build --release:

extern crate rand;
use rand::{XorShiftRng, Rng};

pub fn foo(rng: &mut XorShiftRng) -> usize {
    let arr = [1; 77];
    // `rand = "0.4"`, no bounds checking
    // `rand = "0.5"`, bounds checking
    arr[rng.gen_range(0, arr.len())]
}

Is there something we could do to fix this? A change in UniformInt? Some sort of compiler hint? Or would this require a smarter compiler?

If we can't fix it, perhaps we should document this behavior. We should consider ways to mitigate the problem, such as using unchecked indexing in library code like shuffle, choose, etc.

@dhardy
Copy link
Member

dhardy commented Aug 21, 2018

I guess this is a side-effect of the way compiler optimisations work; presumably the optimiser knows that x % y < y for positive y and non-negative x. In theory it shouldn't be hard to prove that x * y / X < y where y is positive and x < X, but the wmul implementation is potentially more convoluted (though probably not in common cases).

The new code is here and here. @alexcrichton any ideas, or know who better to ask?

@TheIronBorn
Copy link
Collaborator Author

There is some work on a standard u32/u64::wide_mul. Perhaps that could help.

@TheIronBorn
Copy link
Collaborator Author

An example which needs no rand code:

pub fn bar(rand: u64) -> usize {
    let arr = [1; 3];
    let mul = rand as u128 * arr.len() as u128;
    let hi = (mul >> 64) as u64;
    arr[hi as usize]
}

@alexcrichton
Copy link
Contributor

@dhardy oh the best thing to do for something like this is to try and get a minimal example. With that it's relatively easy to inspect the LLVM IR and deduce why LLVM can't perform the optimization, and that will likely also lead to a solution one way or another. (I'm not too familiar here with the specifics, unfortunately)

@TheIronBorn
Copy link
Collaborator Author

TheIronBorn commented Aug 21, 2018

Seems the only problem is proving the x * y / X != y case (only when the range is 3)

%1 = zext i32 %rand to i64
%2 = mul nuw nsw i64 %1, 3
%3 = lshr i64 %2, 32
%4 = icmp eq i64 %3, 3

from

pub fn bar(rand: u32, arr: [u8; 3]) -> u8 {
    let mul = rand as u64 * arr.len() as u64;
    let hi = (mul >> 32) as u32;
    arr[hi as usize]
}

https://play.rust-lang.org/?gist=da0698f29ba0d0c28c5adec4d6717a18&version=nightly&mode=release

@TheIronBorn
Copy link
Collaborator Author

>> 33 has no bounds checking

@dhardy
Copy link
Member

dhardy commented Aug 22, 2018

>> 33 has no bounds checking

Only when it's known that arr.len() > 0 it seems?

In any case you have a valid minimal example (arr: &[u8; 3]), though I don't know whether this will generalise to the case where widening multiply has to emulate the operation due to lack of a larger type. But as long as LLVM considers u128 multiplication a simple operation, this won't matter on most platforms.

@TheIronBorn
Copy link
Collaborator Author

The widening multiply bounding technique is useful for a lot of problems, so it's likely to be used a lot in the future. It would be good if this problem were handled automatically.

Is there any chance of an upstream change in Rust/LLVM?

@TheIronBorn
Copy link
Collaborator Author

Paper describing the technique btw: https://arxiv.org/pdf/1805.10941.pdf#page=3

@TheIronBorn
Copy link
Collaborator Author

TheIronBorn commented Aug 22, 2018

arr.len() as f64 * 0.5 works (when constant) which is similar to the technique: (range * rand / 2^32 ~= range * rand between 0 and 1).

Constant widening multiply works as well.

@TheIronBorn
Copy link
Collaborator Author

Does anyone know where I should start asking about an upstream change?

@dhardy
Copy link
Member

dhardy commented Oct 12, 2018

Sorry, no. Perhaps @nikomatsakis has some idea how a proof of bounds can be added to widening multiply?

@vks
Copy link
Collaborator

vks commented Aug 24, 2021

Will this be fixed by #1154?

@TheIronBorn
Copy link
Collaborator Author

That new method still uses the widening multiply method so I doubt it will affect this.

@dhardy
Copy link
Member

dhardy commented Nov 13, 2024

The widening_mul methods are still unstable (rust-lang/rust#85532).

Presumably we could use std::hint::assert_unchecked to solve this issue. Since this would add unsafe code with potential for UB (if there are bugs in our widening-mul code), I require proof of significant benefit before accepting such a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants