-
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
More Exotic Enum Layout Optimizations #1230
Comments
👍 to any of these, even if just as an experiment to see if it would be worth it in practice. |
rust issue: rust-lang/rust#5977 |
Be wary of optimizations that are incompatible with pointers to the payload. For example
|
|
|
A general-purpose "ForbiddenValues" wrapper as you described in rust-lang/rust#27573 would be nice, but how exactly would you tell the compiler which values are forbidden? Even just for primitive types it's difficult, considering that as yet the type system has no support for variadics or constant parameters. Supporting compound types is even trickier. To be fully general, you have to be able to describe any possible subset of any possible compound type, and do so in a concise way which the compiler can easily reason about, and do it all without relying on any particular layout choice. Short of using a compiler plugin, I have no idea how you do that. Basically: maybe it's worth stabilizing simple cases like |
It seems like one could also null the first field and use the second as a tag, allowing 2^64 (or 2^32) variants. |
@rkjnsn Oh yeah, great point! |
Another idea: Make use of padding bytes in nested enums: enum A {
Aa(u32, B),
Ab(u32, B),
}
enum B {
Ba(u32),
Bb(u32)
} Today, this leads to this kind of memory layout:
Now, we can't just combine those two tag ranges and padding areas into one because we need to support interior references. But what we can do is store the
The idea here being that a This would require that the compiler ensures that padding in a enum doesn't get overwritten with junk bytes though, which might be tricky or inefficient in combination with mutable references to such an enum (You couldn't just overwrite all bytes on reassignment). |
You could also use the bit0/1 in pointers witch are always zero under some aligment conditions like |
@Naicode That may be more space efficient, but it's less time efficient because of the bit shifting and masking required. Rust probably shouldn't make that kind of change automatically. It's a neat idea, though. Reminds me of Ruby's internal |
It's true that such bit-fiddling slows down the program at runtime and therefor should never be applied implicitly. If more exotic layout optimisations are implemented it might still be possible to opt-in to such optimisations on per-struct-basis e.g. a (e.g. for usage of rust in embedded environment with small sized RAM, or a enum's allocated in massive amounts on the heap). |
Another idea: compress nested enum variant ids (thanks Mutabah over IRC!).
This currently requires space in B for the variant id of A, but one could assign BA+A1 = 0, BA+A2 = 1, B1 = 2, B2 = 3, and both save space and maintain the ability to take a reference to the stored A (because its payload may presumably be stored immediately following its variant id [which is also B's] as usual). |
@soltanmm Those kind of optimizations are not generally possible, because 'A' must have the same layout regardless of whether it is used within another enum (otherwise references to the 'A' within 'BA' would have a different layout from normal references to 'A'. Furthermore, you can't adjust the layout of all 'A's in advance because 'B' might be in a different crate. What you can do is treat the determinant in 'A' as a restricted value (either 0 or 1 for A1/A2) in which case the 'B' is free to use the disallowed values as IDs for the other variants (B1, B2) but that kind of optimization has already been covered in this issue. |
@huonw EDIT: I just realized I might have interpreted that incorrectly. @Diggsey I believe the optimizations discussed most similar were using forbidden values in payloads or placing determinants in padding, as opposed to outright summing the determinants in the same location. Having the determinant be a value for which forbidden values might be used wasn't mentioned, and this has slightly different space-time trade-offs to @Kimundi's example with the determinants being split into different sequences of bit positions. Arguably, though, everything in this discussion is some variant of, "Use forbidden values to encode variants," so as long as everyone's kicking the horse I guess I concede that to @Diggsey. :-) |
Would it be possible to enable NonZero for the common Rust enum? Like enum Status {
Alive, // discriminant would start at 1 instead of 0
Suspect,
Dead,
}
size_of::<Status>() == size_of::<Option<Status>>() |
@arthurprs No, that's not a good choice, first off you can't depend on knowing whether |
The general optimization scales better, but @arthurprs's suggestion seems like it would technically work fine. You don't need to depend on knowing about |
Rust rely so much on these patterns that any gain would be huge.
Can't it be the usual 2 discriminants + Status? |
If you manually wrote |
@SimonSapin It feels like this would be on the order of struct field reordering in terms of the kinds of weird bugs it'd end up with and the effort required, with not nearly as much impact. A more general optimization here would be to realize that the refcount of |
(FWIW the refcount is not stored in |
Yeah, you're right. My bad. I still think my point stands, though. |
So in rust-lang/rust#45225 (comment) I mentioned some hard cases, but @trentj on IRC brought it up again and I've realized there's a simpler way to look at things, that is not type-based, as enum E {
A(ALeft..., Niche, ARight...),
B(B...),
C(C...)
} can be seen as a generalization of the case where The challenge is to fit EDIT: now over at rust-lang/rust#46213. |
One possible optimisation that hasn't been mentioned yet: NaN tagging. It's impossible to implement with current Rust types, however; floating-point types treat all possible NaN bit patterns as valid. Doing this would require adding I've actually got something written up on this topic, I might submit it as an RFC soon... |
@fstirlitz With |
@eddyb Will it be possible to use const generics to disallow NaNs with certain payloads, but not others? You'd have to have And even if sufficiently expressive const generics do arrive, it will be beneficial to have a canonical form of this feature in the standard library. |
I'm not talking about CTFE to compute the layout, but a wrapper type with two integer parameters expressing a range of values that the inner type can't use but optimizations can. |
You mean, like... // possibly wrapped in an opaque struct;
// could probably be generic over float types
// by means of associated consts and types,
// but using f64 for readability
enum NaNaN64 {
Positive(IntInRange<u64, 0x0000000000000000, 0x7ff0000000000000>),
Negative(IntInRange<u64, 0x8000000000000000, 0xfff0000000000000>),
}
impl From<NaNaN64> for f64 { /* f64::from_bits */ }
/* other impls */ That's... not especially convenient, is it? Plus, without compiler support it won't be able to additionally take advantage of LLVM's fast-math annotations. |
More like: struct NaNaN64 {
float: WithInvalidRange<WithInvalidRange<f64,
0x7ff0000000000000, 0x7fffffffffffffff>,
0xfff0000000000000, 0xffffffffffffffff>
} But internally we can't represent the two ranges anyway for now, so for NaN-boxing you'd have to choose only one of them, in the near future. It's much more straightforward when everything is offsets and bitpatterns and ranges than "types". |
I'm pretty sure that a safe "forbidden NaN" float type would undermine its own benefits with NaN-masking-cmov's (or worse) everywhere. |
Note: If someone is willing to mentor me on this bug, I'm interested in tackling it. |
@gankro How are we going to track that most of the optimizations mentioned this have been implemented, and the various tricks required to make any further changes? |
@eddyb are any of the optimizations in the OP not implemented? It looks like at least most are, and if so we might want to turn this into a metabug that points at sub-issues for the remainder, or just close it. |
@gankro: re NaNs, you mean checking after arithmetic operations whether the result was NaN? Maybe. On the other hand, such types could at least implement |
@eddyb Could |
Yeah, I only figured out how to later. Also, it requires |
@SimonSapin another alternative: since capacity > len, I've actually started writing such crate for fun. Same holds for |
But the compiler doesn’t know about |
Yes. The compiler will probably never be able to do the optimization using |
Since a part of the original desired optimizations have been implemented in rust-lang/rust#45225, and most of the remaining ones have various trade-offs that need to be explored by RFC for each category separately (with the exception of rust-lang/rust#46213), I'm going to close this central issue. |
Mark enums with non-zero discriminant as non-zero cc rust-lang/rfcs#1230 r? @eddyb
There are several things we currently don't do that we could. It's not clear that these would have practical effects for any real program (or wouldn't negatively affect real programs by making llvm super confused), so all I can say is that these would be super cool.
Use Undefined Values in Other Primitives
Use Multiple Invalid-Value Fields
(&u8, &u8)
can be the same size asOption<Option<(&u8, &u8)>>
Support More than a Single Bit
Use all other fields as free bits when there exists another forbidden field
(&u8, u64)
supports 2^64 variants via the encoding(0, x)
Use the Fact that Void is Statically Unreachable
enum Void { }
cannot be instantiated, so any variant the containsVoid
is statically unreachable, and therefore can be removed. SoOption<Void>
can only beNone
, and therefore can be zero-sized.Support Enums that have More than One Non-C-Like Variant
Can be represented without any tag as
(&u8, *const u8)
via the following packing:Treat any True Discriminant Tags as a Type with Undefined Values
Option<Option<u32>>
can be "only" 8 bytes if the second Option stores its discriminant in the first Option's u32 tag.The text was updated successfully, but these errors were encountered: