Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upMore Exotic Enum Layout Optimizations #1230
Comments
This comment has been minimized.
This comment has been minimized.
|
|
This comment has been minimized.
This comment has been minimized.
bluss
commented
Aug 3, 2015
|
rust issue: rust-lang/rust#5977 |
This comment has been minimized.
This comment has been minimized.
bluss
commented
Aug 3, 2015
|
Be wary of optimizations that are incompatible with pointers to the payload. For example
|
This comment has been minimized.
This comment has been minimized.
|
|
This comment has been minimized.
This comment has been minimized.
bluss
commented
Aug 3, 2015
|
|
This was referenced Aug 10, 2015
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
It seems like one could also null the first field and use the second as a tag, allowing 2^64 (or 2^32) variants. |
This comment has been minimized.
This comment has been minimized.
|
@rkjnsn Oh yeah, great point! |
This comment has been minimized.
This comment has been minimized.
|
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). |
This comment has been minimized.
This comment has been minimized.
rustonaut
commented
Aug 28, 2015
|
You could also use the bit0/1 in pointers witch are always zero under some aligment conditions like |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
rustonaut
commented
Aug 28, 2015
|
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). |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
soltanmm
commented
Jan 13, 2016
|
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). |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
soltanmm
commented
Jan 13, 2016
|
@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. :-) |
rphmeier
referenced this issue
Jan 27, 2016
Open
Optimize Option<f64> and friends with nanboxing #31236
tbu-
referenced this issue
Jun 28, 2016
Closed
discriminant_value intrinsic -- tracking issue for 639 #24263
jonas-schievink
referenced this issue
Jul 21, 2016
Closed
Enums not being stored as compactly as possible #34961
nrc
added
the
T-compiler
label
Aug 25, 2016
This comment has been minimized.
This comment has been minimized.
arthurprs
commented
Oct 14, 2016
•
|
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>>() |
This comment has been minimized.
This comment has been minimized.
|
@arthurprs No, that's not a good choice, first off you can't depend on knowing whether |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
arthurprs
commented
Oct 14, 2016
|
Rust rely so much on these patterns that any gain would be huge.
Can't it be the usual 2 discriminants + Status? |
This comment has been minimized.
This comment has been minimized.
|
If you manually wrote |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 4, 2017
|
Nice, thanks! I just feel like playing a bit, so I'm gonna try to clean it up a bit. |
This comment has been minimized.
This comment has been minimized.
camlorn
commented
Jul 9, 2017
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
(FWIW the refcount is not stored in |
This comment has been minimized.
This comment has been minimized.
camlorn
commented
Jul 13, 2017
|
Yeah, you're right. My bad. I still think my point stands, though. |
oli-obk
referenced this issue
Jul 27, 2017
Closed
Extend the "nullable pointer optimization" to two different size variants #43507
kennytm
referenced this issue
Jul 29, 2017
Closed
enum discriminant should be picked to make enum `NonZero` #43537
eddyb
marked this as
a duplicate of
rust-lang/rust#43537
Jul 29, 2017
mrhota
referenced this issue
Oct 13, 2017
Merged
Refactor type memory layouts and ABIs, to be more general and easier to optimize. #45225
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
fstirlitz
commented
Nov 19, 2017
|
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... |
This comment has been minimized.
This comment has been minimized.
|
@fstirlitz With |
This comment has been minimized.
This comment has been minimized.
fstirlitz
commented
Nov 20, 2017
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
fstirlitz
commented
Nov 20, 2017
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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". |
This comment has been minimized.
This comment has been minimized.
|
I'm pretty sure that a safe "forbidden NaN" float type would undermine its own benefits with NaN-masking-cmov's (or worse) everywhere. |
This comment has been minimized.
This comment has been minimized.
Yoric
commented
Nov 20, 2017
|
Note: If someone is willing to mentor me on this bug, I'm interested in tackling it. |
This comment has been minimized.
This comment has been minimized.
|
@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? |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
fstirlitz
commented
Nov 20, 2017
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
@eddyb Could |
This comment has been minimized.
This comment has been minimized.
|
Yeah, I only figured out how to later. Also, it requires |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Nov 23, 2017
•
|
@SimonSapin another alternative: since capacity > len, I've actually started writing such crate for fun. Same holds for |
This comment has been minimized.
This comment has been minimized.
|
But the compiler doesn’t know about |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Nov 23, 2017
|
Yes. The compiler will probably never be able to do the optimization using |
eddyb
referenced this issue
Nov 23, 2017
Open
Optimize enums by niche-filling even when the smaller variants also have data. #46213
This comment has been minimized.
This comment has been minimized.
|
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. |
Gankro commentedJul 31, 2015
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 containsVoidis 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.