This will let us start moving away from hard-coded attributes in
MemoryBuiltins.cpp and put the knowledge about various attribute
functions in the compilers that emit those calls where it probably
belongs.
Details
Diff Detail
- Repository
- rG LLVM Github Monorepo
Event Timeline
Might it make more sense to implement this as an argument attribute? Would be a bit simpler I think, and I don't really see a disadvantage.
If you do want this to be a function int attribute, then the parameter reference should be zero-based, not one-based, for consistency with allocsize.
I don't have any idea. @reames any opinion? I confess I'm not sure what this means for me.
If you do want this to be a function int attribute, then the parameter reference should be zero-based, not one-based, for consistency with allocsize.
I can't do it zero-based, because there's no way to store a zero in an int attribute. I can go to the work of adding getters and setters everywhere and then pretend it's 0-based for the public API, but internally you'll fail assertions if you try to carry 0 as the payload of an int attr. If you want the getters and setters let me know, I guess? This will matter for one other attribute we need too (mustfree(N) - to indicate a pointer will be freed).
Done.
I don't see anyway with argument align attributes to express the alignment of the return value of the call. Specifically, the fact this is trying to express is "the returned pointer has at least the alignment of the integer param X".
If you do want this to be a function int attribute, then the parameter reference should be zero-based, not one-based, for consistency with allocsize.
I can't do it zero-based, because there's no way to store a zero in an int attribute. I can go to the work of adding getters and setters everywhere and then pretend it's 0-based for the public API, but internally you'll fail assertions if you try to carry 0 as the payload of an int attr. If you want the getters and setters let me know, I guess? This will matter for one other attribute we need too (mustfree(N) - to indicate a pointer will be freed).
Er, this seems strongly suspect to me. I have not looked carefully, but I'm pretty sure we do use zero based indexing in allocsize. You should be able to parallel that structure. (You don't need the argument packing bit, but at the same time it does no harm to reuse if it simplifies the code.)
You need tests for this. Both IR, and bitcode parsing.
I would suggest focusing this change on the mechanics of round tripping the new attribute and defining it. We can defer the use to a separate patch.
llvm/docs/LangRef.rst | ||
---|---|---|
1554 | This definition needs expanded. Questions:
See the alllocsize for example text. | |
llvm/lib/Analysis/MemoryBuiltins.cpp | ||
227 ↗ | (On Diff #402904) | Nope, would be same place we handle allocsize today. |
339 ↗ | (On Diff #402904) | To parallel the allocsize path, we need builtin knowledge to overrule attributed knowledge. |
allocsize is different because it has two parameters, one optional. It's not possible for both to be set to zero, so the payload it carries will _never_ be zero. It does use zero-based indexing, but we'll have to do custom getters/settings across the board like allocsize if we want to use zero-based indexing in both allocalign and mustfree. If that's the preference, I'm happy to do it, but it does seem like a fair amount of extra code for only marginal benefit.
To clarify, what I mean is:
; Instead of declare i8* @aligned_alloc(i64, i64) allocalign(1) ; You do declare i8* @aligned_alloc(i64, i64 allocalign)
That is, the allocalign attribute indicates that the value provided to this parameter will the alignment of the return value. The semantics would be the same as with the return attribute, you just don't need to deal with integer attributes.
If we do want to use the function attribute, I think it should be possible to support proper zero values for integer attributes.
That is, the allocalign attribute indicates that the value provided to this parameter will the alignment of the return value. The semantics would be the same as with the return attribute, you just don't need to deal with integer attributes.
Using a parameter attribute instead of a function attribute with a parameter number seems quite reasonable. Let's go with that.
If we do want to use the function attribute, I think it should be possible to support proper zero values for integer attributes.
I don't think this would be straightforward. The attribute system has the assumption that "unset == 0" in many parts of its API.
llvm/docs/LangRef.rst | ||
---|---|---|
1554 | I don't think the allocsize description is a great example of clarity... For allocsize, I'd want to know how it relates to -- or differs from -- placing dereferenceable_or_null(value-of-EltSizeParam) or dereferenceable_or_null(value-of-EltSizeParam * value-of-NumEltsParam) on the function return. Other than that the latter is only expressible when the values are constants, of course. It is not at all clear from the wording, but it's treated completely differently internally right now. (Maybe it shouldn't actually be? Dunno! The current description doesn't seem to suggest any of the semantics we've actually applied to it!) Anyways, for allocalign, I think we can probably do better. E.g, maybe we can say "This attribute is equivalent to placing align(value-of-allocalign-parameter) on the function return, except that the value is not required to be a constant, and <something about non-power-of-2 values>." -- and actually have the code treat it that way, as well. So, what _should_ the semantics be if the parameter is not a power-of-2?
Looks like we're currently inconsistent in the handling at the moment for the hardcoded functions. AttributorAttributes.cpp seems to be assuming power-of-2 (and tries to create an invalid alloca instruction if it's not), while InstCombineCalls.cpp ignores the specified alignment if it's not a power-of-2. (Am I the only one surprised that "Attributor" can make very-much non-attribute IR changes like doing heap-to-stack!) I think I'd vote for "ignore". Units: Yea, clearly this is "alignment in bytes", but that should be specified. | |
llvm/lib/Analysis/MemoryBuiltins.cpp | ||
339 ↗ | (On Diff #402904) | Why does allocsize work that way? I'd generally expect an attribute to take precedence if specified? |
llvm/lib/Analysis/MemoryBuiltins.cpp | ||
---|---|---|
339 ↗ | (On Diff #402904) | I'm new to compilers at all and LLVM in particular, but I really would have expected an attribute to overrule builtin knowledge too, to the point that I didn't check how allocsize was set up. For now I'm making it consistent, but it does seem backwards. Also, the "use the attribute" part of this has moved to a new change. |
If the existing infrastructure truly doesn't allow zero as a value for an attribute as an expressability level, then it's over specialized for existing attributes, and we should generalize.
With @nikic's clarification, I now understand what he meant, and agree that looks reasonable as well.
llvm/lib/Analysis/MemoryBuiltins.cpp | ||
---|---|---|
339 ↗ | (On Diff #402904) | I have no idea why the code is structured the current way, but having the two *differ* seems like a recipe for confusion and disaster. If you want to change the way the existing attribute is handled, you could do the research and work through that. If you want to leave the order the same for the two, and add a TODO for latter work on reversing it, I'm also fine. Note that order doesn't matter if the baseline knowledge is removed as we seem to be working towards. |
llvm/docs/LangRef.rst | ||
---|---|---|
1552 | This shouldn't be in the "function attributes" section. | |
1553 | "The function parameter marked with this attribute is [...]" maybe? The current phrasing reads odd to me. | |
1554 | I would probably go for UB here, in the interest of allowing handling of non-constant alignments by creating assumes. LangRef is not clear on this, but I expect that an align operand bundle assume with a non-power-of-two value would be UB. No strong preference though. |
This patch looks mechanically fine, we just need to get the LangRef wording finalized.
I did have a thought when reading over this. Is there anything specific here to an allocation function? Could we generalize this into a generic "retalign" parameter attribute which generalizes align(N) on the return value and allows non-constant alignments to be described?
I'm not sure that generalization is actually useful, it just seems like we have something which is almost non-allocation specific. If we could think of examples where non-constant alignments are useful, then it seems a shame to leave it specialized...
llvm/docs/LangRef.rst | ||
---|---|---|
1383 | I can't make heads or tails of the current wording. I'd suggest something along the lines of: This attribute indicates that the alignment of the newly allocated object returned by this allocation function can be assumed to be greater than the runtime value of the marked parameter. That is, it's similar to putting`align(N)` on the return value of the function but allows the alignment to be a runtime value. Runtime alignment values which aren't a power of 2 are undefined behavior. |
So, sorry for not noticing this sooner, but...I just realized we actually already have a way to spell something very much like this already, with llvm.assume.
%ret = call i8* @aligned_alloc(i64 %0, i64 %1) call void @llvm.assume(i1 true) [ "align"(i8* %ret, i64 %0) ]
Maybe we _shouldn't_ be adding an attribute for this, given the above, and rather just insert llvm.assume in appropriate places? @jdoerfert maybe has an opinion here?
llvm/docs/LangRef.rst | ||
---|---|---|
1554 | On second look, this must NOT be UB. Per C2x aligned_alloc: We cannot turn a guarantee of returning nullptr into UB. We must be prepared to accept any alignment at runtime, even if we ignore non-power-of-2 ones. |
I agree that the actual semantics here should be independent of whether the function is also, in some sense, considered an "allocator". I'd be fine still calling it allocalign in that case (similar to how allocsize is a general statement that the number of bytes is dereferenceable_or_null). retalign also sounds reasonable though.
llvm/docs/LangRef.rst | ||
---|---|---|
1554 | Good point. In that case I'm fine with saying that non-power-of-two alignment is ignored. Alternatively we could specify that ptrtoint(ret) % param == 0, which would be the non-power-of-two generalization (and trivially holds for the null pointer), but I don't think there is anything useful we could do with that, and it's probably best to stick with the well-defined notion of alignment (don't want to bring question like "what about non-integral pointers?" into this.) |
After thinking about this a bit more, I think I've actually convinced myself that introducing the attribute is the right thing to do after all. We'll need to make sure to document this well, of course...
The semantics of llvm.assume is only that the return value must be aligned _at least_ to the given amount. If you see a @llvm.assume(i1 true) [ "align"(i8* %ret, i64 2) ] that means you know that %ret is aligned to at least 2-bytes. However, "an alignment assumption which is true at this point" is insufficient to be able to correctly do heap->stack conversion of an allocation call. For that, we must know the alignment which was required of that allocation call.
It's semantically correct to lower the alignment value on -- or entirely discard -- an llvm.assume "align" value, but it is NOT valid to use a lower alignment than was requested for an allocation call.
I think, thus, we can say that "allocalign" implies the llvm.assume "align", in terms of what a caller can assume about the return value's alignment, but also has the additional semantics of indicating the requested alignment of the allocation, which the compiler may use if it decides to elide an allocation. (This also implies that generalizing this attribute to "retalign" is not desirable nor necessary.)
I can't make heads or tails of the current wording. I'd suggest something along the lines of:
This attribute indicates that the alignment of the newly allocated object returned by this allocation function can be assumed to be greater than the runtime value of the marked parameter.
That is, it's similar to putting`align(N)` on the return value of the function but allows the alignment to be a runtime value.
Runtime alignment values which aren't a power of 2 are undefined behavior.