-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Proposal: represent bit pointers at runtime #16271
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
Comments
Simplification suggestion:
|
Hm, interesting idea. I kinda like it, but my main issue with it is that the switch from "normal, well-defined layout, pointer" to "bit pointer, undefined layout" is non-obvious - it's just when |
Well, likewise I don't see the reason of why they would have to be so fundamentally different. Pointers are just numbers, depending on alignment they need more or fewer backing bits of information. |
It could be that, and that's essentially the representation I proposed, but the point is that's still very different to a normal pointer. Pointers have a fixed size, determined by the architecture; the CPU directly dereferences them; and, arguably most importantly, they can be passed over ABI. Bit pointers violate all of these properties: they're necessarily larger than normal pointers, require nontrivial logic to dereference, and cannot be passed over ABI. |
Probably not, I don't really think there's a use case for reinterpreting pointer values in memory in Zig. The reason pointers having a well-defined layout is important is simply that they can be passed over ABI. Bit pointers can't be passed over ABI, so there's no need for them to have a well-defined layout; and having undefined layout allows an implementation to change its representation down the line if something else turns out more efficient, plus allows for some size optimizations (related: #104). |
Well, okay, I don't want to keep unnecessarily dragging out this 1-on-1 thread without new suggestions. |
Note that the important part about the host size for backends is not what that size actually is, but rather where does there exist a known-size, known-alignment block of dereferenceable memory that necessarily contains the bit field. Given an arbitrarily large but suitably aligned block, the backend can calculate the optimal offset and load/store size with a still-known alignment to use to actually perform the operation. It may not be obvious with the current set of functional backends, but losing this alignment information will have performance implications on other targets.
This optimization is not valid without knowing the size and alignment of the packed struct, since it could just as easily be the last field of a packed struct at the end of a page of memory, which could fault if you try to access all 5 bytes starting at a runtime bit offset of 0. While I do not disagree with any of the following points:
I do not personally draw the conclusion that the current feature needs to be removed, just that the feature could use some work and runtime bit pointers sound cool. |
Just wanted to add - probably obvious, but since it hasn't been explicitly spelled out yet: |
I just clocked that I forgot to reply to @jacobly0's comment here - that's my bad. Thanks for explaining why host size matters - I hadn't considered this case. So, without a host size, every bit operation would require a conditional branch to determine how many bytes to load - that's definitely not great. One option here would be to add runtime bit-pointers on top of the current system; for instance, with It's worth noting, however, that direct usage of bit-pointers in user code pretty much never happens - as I mentioned, without runtime bit-pointers, they're actually not a particularly useful concept, since the pointer type kinda locks you into an exact field. The reason they're absolutely necessary is basically for assignments to work - |
FWIW, it's not clear to me how |
If we can't, there's also the option to make volatile (accesses via) bit pointers a compile error. (Though maybe that was the obvious intent / implied already.) |
Yeah, I'm saying it should probably be disallowed. Calling it out since the proposal specifically mentions |
I've been working a lot with pointers in Sema recently, and have become fairly confident that our abstraction of "bit pointers" as it stands today is highly flawed. This is a proposal to fundamentally change them.
Background
Currently, bit-pointers work by being a component of the pointer type. The type
*align(1:6:8) T
contains a pointer to an 8-byte "host" value, which itself has 1-byte alignment, and which contains a value of typeT
at an offset of 6 bits.This setup somewhat simplifies code generation, since we don't need to worry about accessing at arbitrary bit offsets, and bit-pointers are given the same in-memory representation as any other pointer type. However, this comes at the cost of confusion. The whole point of a runtime pointer is that we don't need to know anything about the location of its target at compile-time; that information is not encoded in the type. (Well, okay, there's alignment, but you can always use
align(1)
if you'd like to be able to refer to any address). Bit pointers in their current form violate this rule by effectively encoding part of the address in the type.Proposal
I propose that we eliminate the
*align(a:o:b) T
form from the language, and instead introduce a new "bit pointer" type which contains a bit offset in the pointer value. I'll briefly discuss syntax later, but for now, let's just say$T
is a bit-pointer toT
.const
,volatile
, andaddrspace
are all valid on bit-pointers, butalign
is not, with bit-pointers all being considered to have no alignment requirement (align(1)
).[$]T
(a bit many-pointer) can also be introduced; technically a slice-like type also could, but it's less clear how to write that with a new sigil, and besides, it seems to me unnecessary complexity for something that will essentially never be useful (if you do need it, just use a struct of[$]T
andusize
). Note that[$c]T
wouldn't make any sense, since bit-pointers can't interop with C in the first place.@intFromPtr
and@ptrFromInt
are not valid operations to convert to and from bit pointers (supercedes #2677).@alignCast
is also invalid on bit pointers, but@ptrCast
,@constCast
,@volatileCast
, and@addrSpaceCast
are allowed. You cannot in any defined way cast from a bit-pointer to a standard (byte-aligned) pointer(*), but you can coerce in the other direction (this coercion is always safe; it's akin to lowering pointer alignment). Note also that this solves an existing footgun where@ptrCast
on a bit-pointer can easily result in pointing to bogus memory; another example of how storing the bit offset as a part of the type leads to trouble.(*): if desired, we could allow
@ptrCast
to do this, asserting that the bit offset is 0, but I doubt there's really any situation where it's practically useful.Justification
While this change may complicate compiler implementation, I believe it significantly simplies the language, and makes it more useful. In my opinion, bit pointers as they stand today are one of the most confusing parts of Zig; they're poorly documented, buggy, rare to come by, have a confusing syntax (it's not immediately clear how they relate to pointer alignment!), and make pointers unexpectedly non-interchangable. The more "opaque" bit-pointer system proposed here is a big simplification: the language-level construct can be simply defined as that "
$T
is a pointer to aT
which may not be byte-aligned", and any details of the representation become implementation details (assuming bit-pointers have undefined layout, which seems to me a reasonable choice).There are no complexities from ABI interop, since this is not an ABI type. That means we also don't really lose anything from bit-pointers having undefined layout.
Also note that pointers currently have a fourth
align
field: the "vector index". This is intended to deal with vectors having undefined layout (which they currently don't but hey, y'know!). This proposal would definitively eliminate the need for the vector index. Here's something interesting: the vector index is permitted to be a generic value called "runtime" (this is the type resulting from e.g.&vec[runtime_index]
), but there is no memory to actually store the runtime-known index, so these types generally give garbage results when used right now. If we want&vec[runtime_index]
to be permitted, these pointers must store this extra information, at which point we essentially have this proposal implemented, just limited to a small subset of types!Syntax
The
$T
syntax here isn't a concrete proposal (in fact I don't think I really like it much), it was purely written here so I had something to work with. I did consider the syntax*bit T
, but this reservesbit
as a keyword, so IMHO is a non-starter. One option would be*align(0) T
, although this may be a bit confusing since we've effectively rejected the notion ofalign(0)
in other contexts in the past (#3802), and it makes0
a special case since it is the onlyalign
which changes pointer representation. I'm not 100% sure where to go with this; bikeshedding is encouraged.Implementation
On the level of semantic analysis, this change, while broad, is fairly simple: allow operations working with pointers to take
$T
where it makes sense, and to return$T
where it makes sense. In terms of runtime representation, I propose that these pointers simply store an additional "bit offset" in the range 0 to 7 alongside the normal byte address. This does have the unfortunate effect of doubling the pointer size (so on 64-bit platforms we pay the cost of 64 bits for just 3 bits of information!), but the rarity of these pointers, and particularly of storing them, makes this (in my eyes) a non-issue.Note that this representation removes the concept of a "host size" from bit pointers. I don't believe this raises any problems; this is used today essentially tell backends how to load these pointers, but backends can freely make whatever choice they want. In fact, using the host size naively can lead to inefficient code; if loading a 4-byte value from a 64-byte packed struct, we know for a fact that we need to load at most 5 bytes of memory, which will probably fit in a register for easy shifting!
The text was updated successfully, but these errors were encountered: