-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: ForbiddenValues
trait to enable more optimizations
#2888
Conversation
RFC: ForbiddenValue trait to enable more optimizations
Revert "RFC: ForbiddenValue trait to enable more optimizations"
ForbiddenValue
trait to enable more optimizations
I'm no compiler implementation expert, but this RFC feels pretty light in the details department. It seems to focus mostly on a surface syntax of how this would be used, but doesn't give any time to how the implementation using that information would happen. |
This looks like it only permits 1 forbidden value, which is rather limiting. Note: If you're type has any padding bytes (which most structs do), you can get #[repr(u8)]
enum ZeroPadding {
Padding = 0
} For enums are much harder to layout optimize, usually we can merge the discriminants together, this is what allows |
@shepmaster I know nothing about rustc, but here's my best guess about the implementation:
@KrishnaSannasi The value of this RFC is for those cases where it's not convenient to use an enum as a part of your type. For example: pub struct S {
x: u32,
} If legal values for the field |
So, similar to the currently unstable + internal |
@KrishnaSannasi I wasn't aware of those attributes, so thank you for bringing them to my attention. The error message when |
Personally, I'd rather those internals be exposed as some sort of bounded integer types than something like this. |
That is much less easily usable to create type system guarantees. |
I don't think "bounded integer types" and "arbitrary niche specification" are exclusive features. Bounded integer types are a concrete usage, potentially with a different surface syntax, but could be implemented using arbitrary niches. In my head, I view it akin to how |
That's a pretty good analogy, honestly. The main reason why I mention bounded integer types is because it requires actual language support to allow coercing integer literals into bounded types, and allowing casting properly. Niche specialization can technically be done today in a very hacky way with enums, although I personally wouldn't mind requiring that someone make an enum to allow niches like disjoint ranges (e.g. a byte whose values can only be 0-10 or 20-100). Having to explicitly define 0-254 enum variants to get these niches today is an enormous hack, and ranges at least ease the pain with minimal changes from a user perspective. |
It would be extremely powerful if we could utilise Alternatively, and with less reliance on Edit: As a potential upside of doing if {const B: bool = self.is_forbidden(); B} {
unsafe { core::hint::unreachable_unchecked() };
} Then Edit2: A second benefit could be that Edit3: Potential downside is that figuring out how many potential forbidden variants there are for enum layout optimisations is potentially too complicated with |
How about a forbidden bit? I have seen it in http://troubles.md/abusing-rustc/ |
Is this useful as long as repr(Rust) has unstable representation? |
@elichai Good question. As in my boolean function proposition, it would be good to just use |
Changed the `ForbiddenValue` trait (renamed to `ForbiddenValues`) so that it is able to define multiple forbidden values.
@RustyYato I changed this RFC to allow multiple forbidden values. Also, added an example of a |
ForbiddenValue
trait to enable more optimizationsForbiddenValues
trait to enable more optimizations
text/0000-forbidden-value-trait.md
Outdated
unsafe impl ForbiddenValues<[[u8; 4]; 3], [u8; 4]> for FastFloat { | ||
const FORBIDDEN_VALUES: [[u8; 4]; 3] = unsafe { | ||
[ | ||
mem::transmute(f32::NAN), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that f32
has about 16.8 million different NaN
values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem. You just write a procedural macro to produce those 16.8 million lines of code and buy a supercomputer to compile it 😜
I admit that your suggestion for a trait with a const fn is_forbidden(&self) -> bool
method would be strictly better than this RFC - it would make it (relatively) easy to specify large ranges of forbidden values. But I do wonder about its compiler implementation - without knowing much at all, I would imagine that the compiler would have to call value.is_forbidden()
for each possible bit pattern of value
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would the compiler have to call value.is_forbidden()
for each possible bit pattern?
If the compiler "needs" 5 values for a variety of optimizations, it would just have to find 5 forbidden values and use those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would always have a worst case of going through every value, though. Which isn't scalable past a few bits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To eliminate dead code, all rustc
needs to do is insert and constant-fold the mentioned unreachable_unchecked
snippet. This is something it can do today.
Finding holes for compactly encoding enum
variants, however, is more tricky.
text/0000-forbidden-value-trait.md
Outdated
mem::transmute(f32::NAN), | ||
mem::transmute(f32::INFINITY), | ||
mem::transmute(f32::NEG_INFINITY), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we have a range of forbidden values? For example if we were to have u8
that is able to store 55 numbers at most, then all the rest of the numbers as forbidden values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You would have to add all those 201 (2^8-55) forbidden values to the FORBIDDEN_VALUES
array as [u8; 1]
values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems extremely suboptimal. Why not just accept ranges? Individual values can be done as ranges too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a struct that is packed and uses 30 bits. For obvious reasons it's stored as a 32 bit integer.
If we used const fn(T) -> bool
or ranges, this would be trivial to write. With the proposal, I'd have to list out over three billion values. Obviously that's neither feasible nor fast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that a list is infeasible, but my point is that a predicate is also infeasible. Imagine that the forbidden values are in the higher bits, and that the compiler simply does a sweep of the values in order to find the forbidden ones. It'll still have to go through almost anything.
And if your argument is that the compiler should be able to look at the function and determine what it's doing, 1. the halting problem and 2. ranges are easier and way less jank :p
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slice of ranges? I think that would solve pretty much every situation, as a value can (of course) be represented as a range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@clarfon Do you mean ranges as in something like the following?
unsafe trait ForbiddenValues<A, B>
where
A: FixedSizeArray<(B, B)>,
B: FixedSizeArray<u8>,
{
const FORBIDDEN_VALUES: A;
}
Where B
is now interpreted as a fixed-size unsigned integer type with an arbitrary width, and the tuple (B, B)
represents the inclusive start and end of the range of forbidden values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using a list of both ranges and bit masks to define forbidden values is probably the best solution, since it handles nearly all cases and doesn't have the halting problem and similar issues you'd run into with defining it using a const fn
.
For the bit mask, it would match all values v
that pass (v & a) == b
where a
and b
are values of some sort (byte arrays? integers?).
As an example of why ranges alone are insufficient:
What if you have a type like:
enum E<T> {
A(T),
B(T),
C(T),
D(T),
}
type E2<'a'> = E<&'a u32>;
where you want Rust to pack the enum tag for E2
into the 2 low bits of the pointer since the pointer is guaranteed to be some value with the two low bits are zeros due to references guaranteed alignment and u32
having an alignment of 4? Rust currently doesn't pack the enum that way, but could. This kind of bit packing is used extensively throughout LLVM's codebase (implemented using custom C++ code), so is not that uncommon.
If you were to enumerate the forbidden values of a custom T
type using ranges, you'd get something like:
const FORBIDDEN_VALUES: [...] = [
0x0..=0x3, // include 0 for null
0x5..=0x7, // repeats every 4 values
0x9..=0xB,
0xD..=0xF,
0x11..=0x13,
0x15..=0x17,
0x19..=0x1B,
0x1D..=0x1F,
... // another *billion* lines for 32-bit pointers
];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like the following to allow forbidding values both by ranges and by a bit mask?
unsafe trait ForbiddenValues<R, B>
where
R: FixedSizeArray<RangeInclusive<B>>,
B: FixedSizeArray<u8>,
{
const FORBIDDEN_RANGES: R;
const FORBIDDEN_BITS: B;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For bitmasks, just a single integer is insufficient, you'd need something like:
struct BitMask<T> {
mask: T,
value: T,
}
impl<T> BitMask<T> {
fn matches<'a>(&'a self, value: T) -> bool
where
T: BitAnd<&'a T, Output = T> + PartialEq,
{
(value & &self.mask) == self.value
}
}
unsafe trait ForbiddenValues<B>
where
B: MemoryBits,
{
const FORBIDDEN_RANGES: &'static [RangeInclusive<B>];
const FORBIDDEN_BIT_MASKS: &'static [BitMask<B>];
}
This allows you to represent things such as an integer where top 4 and bottom 12 bits can't be 0x3
and 0xCAB
by including a bit mask BitMask { mask: 0xF000_0FFFu32, value: 0x3000_0CABu32 }
assuming u32
implements MemoryBits
.
I wonder if rust currently pack struct of bools into just a single |
It only now, much later, occurred to me that your solution here probably kind of is what @Ixrec meant by implementing exotic layout guarantees with a bunch of bitshift operations (because I had no idea what he meant by that). With the exception that your example uses So, if my options are to use |
Sorry for the confusion. @burdges and @shepmaster's comments together are pretty much exactly what I had in mind with that comment. I thought this was well-known since that's basically how all bitflags/bitfield crates work, and those are by far the most prominent use case for the sort of thing this RFC is about. |
@lxrec so this means one should not use the |
There isn't really such a thing as a "more optimized In general there will always be lots of crates offering variations on stuff in |
There are parallels between bitfield access and As an aside, we'd save more memory and better improve cache locality with NRVO ala #2884 because crates can easily burn considerable stack space now |
I admit I don't know what a First of all, as we can see from the code snippet below, the compiler doesn't optimize the size of In order to optimize the size of This is clearly a tradeoff between size (of #[repr(u8)]
enum U8LessThan2 {
_0, _1,
}
#[repr(u8)]
enum U8LessThan4 {
_0, _1, _2, _3,
}
enum Bar {
A(U8LessThan2),
B(U8LessThan2),
C(U8LessThan4),
}
const _: () = assert!(mem::size_of::<Bar>() == 2); |
So, I can't actually find any good examples -- would someone be able to provide a link to some example of what the weird unsafe bitshifting looks like, as a way of creating forbidden values? |
@clarfon Is https://github.com/maciejhirsz/beef/blob/856988b38cafa8c8f8f44f6f0923c717ce3f518b/src/lean.rs#L24-L29 considered weird enough? |
AFAIK, that example uses a |
Is forbidden values applied to existing |
There have been multiple comments here that mention bitfields in various contexts, and I haven't understood why people are talking about bitfields. What do bitfields have to do with this RFC? I've been thinking that it's me who doesn't understand something here because that happens a lot, but now I'm beginning to think that maybe some people have misunderstood what this RFC is trying to accomplish. And that would be clearly my fault. So, I'll now try to explain better what this RFC is all about:
Give the compiler the ability to use the forbidden value(s) of any applicable (opted-in) user-defined type in order to represent data-less enum variants.
There are some types that should/must never hold certain (forbidden) value(s). Here are some examples of types with forbidden values that the compiler knows about and uses to represent data-less enum variants today:
std::{
boxed::Box,
num::{NonZeroI8, NonZeroU8, NonZeroI16, /* ... */},
ptr::NonNull,
}
enum UnitaryEnum {
A, B, C, D,
} (I believe that list is not exhaustive) Enum types can have both data-less variants and variants with data attached to them. Here's an example: enum EnumType<T> {
VariantWithData(T),
VariantWithoutData1,
VariantWithoutData2,
} If you instantiate an Currently there are only two ways a user-defined type |
(edit: added additional explanation and example usage)
The reason several people have mentioned bitfields and bitmasks is because some kinds of forbidden values are not one or a few ranges of values, instead, (like
We would like to be able to express those kinds of forbidden values for our own custom types. Now that I think about it, both kinds of forbidden value specifications could be combined into a single kind: // replace as necessary to avoid depending on const generics if that causes problems:
struct ForbiddenValue<const N: usize> {
mask: [u8; N],
low: [u8; N],
high: [u8; N],
}
impl<const N: usize> ForbiddenValue<N> {
const fn is_forbidden(&self, value: &[u8; N]) -> bool {
let mut cmp_low = Ordering::Equal;
let mut cmp_high = Ordering::Equal;
// would need to switch for into a while loop since const for doesn't currently work
for i in 0..N {
// compare in little endian order -- could switch to big endian by reversing iteration order
// or match native endian
let masked_byte = self.mask[i] & value[i];
match masked_byte.cmp(&self.low[i]) {
Ordering::Equal => {}
v => cmp_low = v,
}
match masked_byte.cmp(&self.high[i]) {
Ordering::Equal => {}
v => cmp_high = v,
}
}
cmp_low != Ordering::Less && cmp_high != Ordering::Greater
}
}
trait ForbiddenValues<const N: usize> {
const FORBIDDEN_VALUES: &'static [ForbiddenValue<N>];
} Example usage: #[repr(transparent)]
struct MyFakeReferenceType(u32);
// assumes little endian; 4 is size of u32
impl ForbiddenValues<4> for MyFakeReferenceType {
const FORBIDDEN_VALUES: &'static [ForbiddenValue<4>] = &[
ForbiddenValue { mask: [0xFF; 4], low: [0; 4], high: [0; 4] }, // exclude value 0
// exclude values where (value & 3) >= 1 && (value & 3) <= 3
ForbiddenValue { mask: [3, 0, 0, 0], low: [1, 0, 0, 0], high: [3, 0, 0, 0] },
];
} |
I changed the design (in the RFC) a bit by removing the generic parameters because they would allow the trait to be implemented multiple times for a specific type. Then it occurred to me, that it is already possible with the current design to define a large number of forbidden values easily by using a unsafe trait ForbiddenValues {
type Forbidden: FixedSizeArray<[u8; mem::size_of::<Self>()]>;
const FORBIDDEN: Self::Forbidden;
}
unsafe impl ForbiddenValues for f32 {
type Forbidden = [[u8; 4]; 50];
const FORBIDDEN: [[u8; 4]; 50] = {
let mut forbidden = [[0u8; 4]; 50];
let mut item = 1u32;
let mut i = 0;
while i < 50 {
forbidden[i] = unsafe { mem::transmute(item) };
item += 128;
i += 1;
}
forbidden
};
} And I think it's unlikely that the compiler would need a huge number of forbidden values to perform its optimizations, so a reasonable number like 50 would probably suffice. Another thing to note is that I believe that these (potentially large) |
There are some optimizations that could be done in the future that would need large numbers of forbidden values, such as packing values into bitfields, so, to prevent getting stuck with an API that is really unsuited for those use cases, I think we should plan ahead by using an API that can succinctly express all forbidden values, especially including those that require bitmasks to describe succinctly (such as the type |
@programmerjake Could you elaborate a bit on what you mean by that? And I'm going to anticipate your answer now, and comment a bit on it beforehand. Tell me if I'm wrong, but this is what I assume you meant by that: There will be some bitfield-type that gives the user the impression (by doing bitwise operations behind the scenes) that one can store types of arbitrary bit widths inside it. In order to store a value of type Another point I want to make, or rather suggest, is that we should assume that there will be a bounded integer type in the standard library at some point, and that the compiler will implicitly know about all the forbidden values of those types just like it does today with unitary enums. Such bounded integer types will remove a lot of the use cases for the |
@programmerjake I'd like to point out that the design you described cannot express all forbidden values. For example: // Contract:
// For each value `f` of type `Foo`, `f.start <= f.end`
struct Foo {
start: u32,
end: u32,
} The iterator based design and the predicate based design are the two designs that can express all forbidden values (not succinctly though, and neither one of them can, practically speaking, express forbidden bits). The predicate based design may not be feasible from compiler implementation's point of view. And the iterator based design has the caveat that it may not be able to use pub trait AlternativeIterator {
type Item;
const LEN: usize;
fn next(&mut self) -> Option<Self::Item>;
} The current design is simple & not robust, and the iterator based design is complicated & robust. I think they represent the two extremes of the design space. In order to navigate that space, it would be good to get the compiler to tell us statistics about the maximum and the average number of forbidden values it needs to perform the optimizations in crates.io. |
Just to add perspective: I'm not just interested in more efficient |
@Evrey Can you elaborate on what you mean by that? I'm pretty sure "statically" means "at compile-time". But other than that, I just don't understand what you mean.
I take "exhaustively" to mean that you need to be able to express all invalid states. You also use the word "conveniently" - do you consider the external iterator based design convenient enough? |
@Evrey Note that the trait is |
We'll need compiler plugins for formal verification tools like refinement types because rustc performance would suffer if random crates started using those tools everywhere. I'm kinda curious just how much performance hit we already take from generic-array, which winds up being an unecessary dependency of 500+ crates. I think https://internals.rust-lang.org/t/pre-rfc-prefer-all-zeros-niche-for-niche-variant-optimization/12076 has a "wiser" conversation about the topics around this RFC. |
@tommit One example I repeatedly brought up is expressing As for the iterator: It still passes around byte buffers, which have the big endianess issue. And iterating over 16.8 million @jhpratt Well, yes, like with @burdges Very true. But on the other hand, whoever uses refinement types (and I see this RFC as an opportunity for pretty much »poor man's refinement types«) or even full formal verification of the code is already okay with long compile times. |
Yeah, it makes an assumption about the data because you said it could, basically. I think we're sort of saying the same thing. |
text/0000-forbidden-value-trait.md
Outdated
```rust | ||
*(&raw const t as *const [u8; size_of::<T>()]) != f | ||
``` | ||
2. `T` must not be a type that has forbidden values the compiler already knows about (e.g. `char`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e.g.
char
How about other types, are there any other types that the compiler already knows about? Is it possible to lists them out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how relevant this is now that the issue's closed, but https://doc.rust-lang.org/std/#primitives is a list of primitive types in the language. Which of these counts as "having forbidden values" depends on what you mean by that. For example, "uninitialized" is a forbidden / insta-UB-causing value for all of them, and str
has to be valid UTF-8 within safe code but unsafe code can violate that without triggering UB, and I don't know if you'd want to include library types like NonZero*
, and so on and so forth. But if we try to just ignore all of this crippling scope vagueness, I think bool
is the only other primitive type that is "like char
" insofar as it clearly has to be implemented as one of the integer types with certain values excluded (in bool
's case, every value except 0 or 1).
Rendered