-
Notifications
You must be signed in to change notification settings - Fork 19
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
Rangeless Shifts #428
Comments
I think this is similar to #230. (edit: I said "the same as" first, but you want different semantics for oversized shifts) |
Yes, in particular I want the over-width left shift to be |
Why not change the behaviour of the |
Leaving aside whether Rust would have designed these operators like that pre-1.0 (which I don't think Rust should have), I don't think we should change these operators at this point, because that would make them not match the behavior of any existing CPUs, and require multiple instructions rather than one. (In some cases we could optimize away the additional instructions, but I don't think our default operator semantics should have a mismatch with CPUs.) |
Not sure if you consider nVidia GPUs an "existing CPU" but the PTX instruction set for
The wrapping behavior is true for basically every other target though (x86, arm, wasm, sparc, riscv, powerpc, mips, m68k, loongarch64, hexagon, bpf) BTW I think NVPTX's impl u64 {
pub fn wrapping_shl(self, rhs: u32) -> Self {
self << (rhs & 63)
}
pub fn clamping_shl(self, rhs: u32) -> Self { // or `clamped_shl`
if rhs < 64 { self << rhs } else { 0 }
}
} |
We discussed this ACP in today's standard library API meeting. The team members present agreed there is a compelling motivation for the standard library to provide the proposed rangeless shift behavior. Before we accept the new methods, we'd like to ask for some other naming options to be brainstormed. We were split on whether changing Commentary opposed to changing the operators:
Commentary in favor of changing the operators:
|
changing the shift operators is not backwards compatible, it changes the release-mode behavior of |
If we changed the operator, we would also need to decide how to handle negative shift values. e.g. I don't think we want rangeless (This isn't a problem for explicit methods with
How about "unbounded"? |
If you are changing the behavior of Interestingly the x86 SSE2
OTOH ARM's vector-variant
|
x86-64 will also offer this behaviour (specifically as to left-shift and unsigned right-shift) when the range is known to be <64, as you can just do the shift on a 64-bit register, then use the lower 32-bit, 16-bit, or 8-bit overlaps (though rightshift requires first zero-extending, which is either a |
Given a time machine, I'd absolutely change what Rust 1.0 did here. Good integer behaviour follows the rule from rust-lang/rust#100422 (comment) -- as though it was done in infinite precision, and you just got the low bits. That's why I think it's bad that let x = 3;
foo(x);
dbg!(x << 16); does something so drastically different depending on what the argument type of Needing a conditional instruction -- notably one that doesn't add any dependencies -- is 100% not an issue. Rust isn't an assembly language; the defaults should be the things that work properly even if it's another instruction, because the fast-but-wrong ones are better as functions, not operators.
But I agree that it's probably too late to change it now. (At least, it'd need a very long transition period, probably over multiple editions.) Spitballing names:
|
Some more options
|
We discussed this in today's @rust-lang/libs-api meeting. We ended up settling on @cuviper's suggestion of |
Marking this as accepted, because we're accepting the functionality proposed by ACP, with the names changed to |
Proposal
Problem statement
Rust presently has a few options for shifts:
val << bits
/val >> bits
does a shift bybits
with normally checked behaviour for out-of-range shifts,val.wrapping_shl(bits)
/val.wrapping_shr(bits)
likewise does a shift bybits
, but wraps bits to the width of the type.(There are also
rotates_
which likewise wrap at width boundary).This has a problem because the wrapping behaviour here is not the intuitive one (as it wraps the shift quantity, not the shift result). Operations may perform a shift with an undetermined quantity that may exceed the width of the type, e.g. to produce a bitmask of possible values.
Motivating examples or use cases
Producing an
n-bit
mask of an integer type up ton
bits:Determining which bit to check for a carry-out of a wide operation
Both of these functions are ones I'm currently using in an architecture emulator which does native integer operations of varying sizes up to 64-bit.
Solution sketch
The names of the methods are subject to bikeshed, I don't have a good name for them.
Formally, the behaviour of both of these methods involves evaluating the shift on a type with infinite width (or concretely, a width of
u32::MAX + 1
), then wrapping the result modulo(2 ^ Self::WIDTH)
.Practically this means that:
a.rangeless_shl(bits)
, ifbits < Self::WIDTH
, the result is exactlya << bits
. Otherwise the result is0
a.rangeless_shr(bits)
, ifbits < Self::WIDTH
, the result is exactlya >> bits
. Otherwise the result depends on the signedness:unsigned
types, the result is0
like forwrapping_shl
, which corresponds with the logical right shift of the value by the full width of the type,signed
types, the result is either0
or-1
depending on whether the value is signed or unsigned, which corresponds to an arithmetic right shift of the value by the full width of the type.The result does not differ when the shift quantity is exactly
Self::WIDTH
or is any value that exceeds it.Alternatives
There are a few ways to implement the rangeless shifts concretely:
self.checked_{shl,shr}(bits).unwrap_or(exceeds_width_result)
, or, verbosely:if bits < Self::WIDTH { self (op) bits} else { exceeds_width_result }
(where
exceeds_width_result
is the appropriate value for the type and op)Both versions are verbose and somewhat error prone (particularily for signed
rangeless_shr
) to write inline.This could be an external crate, but has one of two major limitations due to current language features
const fn
.The ergonomics of having a method call, and the ability to do this as
const
makes me of the opinion that it should be an inherent method of the integer types in the standard library. A standard library function could also potentially benefit from optimizations on architectures where this behaviour is the behaviour of an out-of-range shift. I do not know whether or not the versions written above will do this, on x86 the unsigned versions will optimize to acmov
however.Links and related work
What happens now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
Second, if there's a concrete solution:
The text was updated successfully, but these errors were encountered: