This is an archive of the discontinued LLVM Phabricator instance.

[IR] Add writable attribute
ClosedPublic

Authored by nikic on Aug 16 2023, 6:58 AM.

Details

Summary

This adds a writable attribute, which in conjunction with derferenceable(N) states that a spurious store of N bytes is introduced on function entry. This implies that this many bytes are writable without trapping or introducing data races. See https://llvm.org/docs/Atomics.html#optimization-outside-atomic for why the second point is important.

This attribute can be added to sret arguments. I believe Rust will also be able to use it for by-value (moved) arguments. Rust likely won't be able to use it for &mut arguments (tree borrows does not appear to allow spurious stores).

In this patch the new attribute is only used by LICM scalar promotion. However, the actual motivation for this is to fix a correctness issue in call slot optimization, which needs this attribute to avoid optimization regressions.

Followup to the discussion on D157499.

Diff Detail

Event Timeline

nikic created this revision.Aug 16 2023, 6:58 AM
nikic requested review of this revision.Aug 16 2023, 6:58 AM
Herald added a project: Restricted Project. · View Herald TranscriptAug 16 2023, 6:58 AM
nikic updated this revision to Diff 551021.Aug 17 2023, 12:10 AM

Forgot to git add one file.

For that reason the writable attribute is specified to perform a spurious store of dereferenceable bytes on function entry.

That makes a lot of sense; FWIW I also don't know any better way to specify the regular dereferenceable attribute (i.e., as spurious reads). In particular those accesses are relevant for noalias. See here for me exploring this in the Rust context.

For this particular attribute however, just to be sure: it is still the responsibility of the optimization to ensure that the spuriously written value does not cause wrong results elsewhere, right? In other words, an empty function with a pointer argument that is 4-byte writeable can *not* just get a spurious store of 0, since someone might actually look at that memory after the function returns and they would be able to see the difference?

nikic added a comment.Aug 17 2023, 1:13 AM

For that reason the writable attribute is specified to perform a spurious store of dereferenceable bytes on function entry.

That makes a lot of sense; FWIW I also don't know any better way to specify the regular dereferenceable attribute (i.e., as spurious reads). In particular those accesses are relevant for noalias. See here for me exploring this in the Rust context.

For this particular attribute however, just to be sure: it is still the responsibility of the optimization to ensure that the spuriously written value does not cause wrong results elsewhere, right? In other words, an empty function with a pointer argument that is 4-byte writeable can *not* just get a spurious store of 0, since someone might actually look at that memory after the function returns and they would be able to see the difference?

That's right. The attribute always allows you to introduce *p = *p on function entry (in conjunction with dereferenceable), but introducing *p = 0 would require you to prove that this does not change observable behavior (e.g. because you're moving up a later *p = 0 store and there are no reads/unwinds in between). Another caveat is that once you move away from function entry, you also need to ensure that the pointer does not escape to a different thread.

RalfJung added a comment.EditedAug 17 2023, 1:51 AM

introduce *p = *p on function entry

I'm not sure about that. If some but not all of the bytes at *p were poison, then after this *p = *p they will all be poison (since *p will turn a single-byte poison into a whole-value poison).

If there was a "bytes" type that would preserve per-byte poison then this might be possible, but with the whole-value poison of the integer types I don't think this works.

nikic added a comment.Aug 17 2023, 2:11 AM

introduce *p = *p on function entry

I'm not sure about that. If some but not all of the bytes at *p were poison, then after this *p = *p they will all be poison (since *p will turn a single-byte poison into a whole-value poison).

The notation here was a bit too abstract. It would be more precise to say that you can llvm.memcpy(p, p, dereferenceable(p)).

The poison propagation issue is a very general one -- and indeed, it does also appear when introducing spurious stores. It's a well known problem that we currently don't have a good answer to. (The byte type will likely be part of that answer.)

Makes sense, thanks!

can you explain the data race part more in the description?

nikic added a comment.Aug 17 2023, 9:22 AM

can you explain the data race part more in the description?

This section from the atomic optimization guide describes the data race issue in some more detail: https://llvm.org/docs/Atomics.html#optimization-outside-atomic

khei4 added a subscriber: khei4.Aug 17 2023, 9:44 PM
efriedma added inline comments.Aug 18 2023, 10:35 AM
llvm/test/Transforms/LICM/scalar-promote.ll
889

I'm not sure this transform is actually legal without dereferenceable(4)? The way "writable" is specified, no "deferenceable" means no phantom store, so this is introducing a race.

nikic added inline comments.Aug 18 2023, 11:14 AM
llvm/test/Transforms/LICM/scalar-promote.ll
889

sret(%T) implies dereferenceable(sizeof(%T)). I should probably tweak the wording to not talk about dereferenceable(N) specifically but attributes implying dereferenceability in general.

(And we should really drop the type argument from sret and make people specify align and dereferenceable instead...)

efriedma added inline comments.Aug 18 2023, 11:53 AM
llvm/test/Transforms/LICM/scalar-promote.ll
889

Is that actually the way the compiler is reasoning about it? Can you add a negative test showing the sret is actually necessary here?

nikic updated this revision to Diff 551996.Aug 21 2023, 5:49 AM

Pivot the definition to only be meaningful in conjunction with dereferenceable (or attributes implying it). Just the writability property without the data-race freedom doesn't seem particularly useful, and we only get the data race freedom with dereferenceable, so it's probably simpler to just couple these directly. (It would be nice to not have the coupling, but I don't really see a way to specify that operationally.)

Adjust the use in LICM to explicitly check for dereferenceability if required.

nikic added inline comments.Aug 21 2023, 5:52 AM
llvm/test/Transforms/LICM/scalar-promote.ll
889

Yes, you were right and this did not reason based on explicit dereferenceability. I've added a test with an insufficient dereferenceable annotation and adjusted the implementation. I've also adjusted the LangRef wording to tie it to explicit dereferenceability.

I think there's a natural connection to dereferenceable here: "dereferenceable(N)" loads N bytes on entry, "deferenceable(N) writable" loads and stores N bytes on entry. So gluing them together makes sense to me.

llvm/docs/LangRef.rst
1537

I think I'd suggest dropping the "or another attribute" part; it makes it harder to reason about the semantics on the caller side.

I think this is fine. I still think we might just want to add this as a modifier in dereferenceable (if it is tied anyway): dereferenceable(writeable: N).
Again, the motivation stems also from the fact that we could then make dereferenceable_globally another modifier: dereferenceable(globally[, writeable]: N).

nikic added inline comments.Aug 21 2023, 11:04 AM
llvm/docs/LangRef.rst
1537

My motivation for this is to allow reusing standard dereferenceability APIs. Otherwise we would have to thread an extra option through APIs like isDereferenceablePointer() that restricts it to only dereferenceable and excludes the other attributes.

efriedma added inline comments.Aug 21 2023, 4:17 PM
llvm/docs/LangRef.rst
1537

Value::getPointerDereferenceableBytes() isn't that complicated; we can write out the other cases it handles here, I guess. I'm more worried about it being open-ended.

To be more specific about what I'm worried about, the exact number of bytes written is relevant for a couple of reasons. One, alias analysis in the caller: ideally, the caller should be able to understand which bytes are modified (although I guess that might require attributes that don't currently exist). And two, any transform that modifies a relevant attribute needs to worry about whether the transform invalidates the "writable" attribute.

nikic added a comment.Aug 22 2023, 7:45 AM

Something I'm concerned about is the interaction between writable and readonly, as well as memory(argmem: read). The way this is specified right now, writable readonly is immediate UB. And I think that's semantically correct.

However, I think it would be a good idea to make things like writable readonly a verifier error instead. This will make sure that passes like FuncAttrs remove the writable attribute when inferring readonly (which this patch currently fails to do). I assume we prefer having readonly over writable, as in the cases where we can infer readonly we wouldn't be interested in inserting spurious stores anyway.

nikic updated this revision to Diff 552373.Aug 22 2023, 8:14 AM

Forbid writable + readonly and similar combinations.

nikic added inline comments.Aug 22 2023, 8:17 AM
llvm/docs/LangRef.rst
1537

I've added an explicit list of the attributes.

I think in terms of inference the case to be concerned about is when dereferenceable with a larger byte range is inferred (maybe Attributor does that, haven't checked). The rest are ABI attributes and we shouldn't infer those.

How would the caller use writable for alias analysis?

efriedma added inline comments.Aug 22 2023, 10:18 AM
llvm/docs/LangRef.rst
1537

The presence of a non-atomic write allows other non-atomic writes to be introduced; LICM could use that in theory. Maybe too niche to really be useful.

At some point, we could add an attribute on calls to indicate the maximum number of bytes written through an argument; if we have that, making sure we don't write past the limit would become more important.

1550

Does anything actually use sret to drive analysis like this? I thought we eliminated most of the usage.

nikic added inline comments.Aug 22 2023, 11:39 AM
llvm/docs/LangRef.rst
1550

It happens here: https://github.com/llvm/llvm-project/blob/f105ed11ae44b382f1c2cf9f83cba313ad0b8d7f/llvm/lib/IR/Value.cpp#L857-L863 sret is one of the types part of getPointeeInMemoryValueType().

Worth noting that clang doesn't currently emit dereferenceable for sret arguments, it relies on the implied dereferenceability.

I'm not really comfortable reviewing the attributor stuff, but the rest looks fine to me.

nikic added inline comments.Aug 22 2023, 1:23 PM
llvm/lib/Transforms/IPO/AttributorAttributes.cpp
8038

@jdoerfert What would be the correct way to remove an attribute from all parameters here?

In practice just the handling above seems to be enough, but it would be cleaner to explicitly remove writable if we infer readonly here.

jdoerfert added inline comments.Aug 22 2023, 2:05 PM
llvm/docs/LangRef.rst
1537

maximum number of bytes

We never landed objectsize, which is similar, and 'maxwrite' would also be good, esp. now that we can shrink allocations and track content of memory.

llvm/lib/Transforms/IPO/AttributorAttributes.cpp
7849

one implies the other. Maybe use a precompiled set, like AttrKinds above, the call to remove is not free.

8038

You can just add it to AttrKinds that is passed to removeAttrs, no?

nikic added inline comments.Aug 22 2023, 2:29 PM
llvm/lib/Transforms/IPO/AttributorAttributes.cpp
8038

The problem is that Memory is a function attribute, while Writable is a parameter attribute, so I think the getIRPosition() would be the wrong one for it?

jdoerfert added inline comments.Aug 22 2023, 6:38 PM
llvm/lib/Transforms/IPO/AttributorAttributes.cpp
8038

Oh, then u need to iterate over all arguments I guess. Not that we forbid readnone on the function and writeonly on an argument right now.

nikic updated this revision to Diff 552646.Aug 23 2023, 3:32 AM
nikic edited the summary of this revision. (Show Details)

Drop conflicting attribute in attributor.

fhahn added inline comments.Sep 25 2023, 2:52 AM
llvm/docs/LangRef.rst
1536

Can/should this be enforced by the verifier? If yes, might be good to strengthen the wording from meaningful to something like must be used in conjunction

nikic added inline comments.Sep 25 2023, 3:06 AM
llvm/docs/LangRef.rst
1536

I don't think we should verify this bit. Based on past experience, attribute dependencies enforced by the verifier tend to be fairly annoying, so I think we should avoid them when possible. E.g. if we verify this, then ArgPromotion would have to drop the writable attribute when it drops sret -- which seems like an unnecessary code change.

The only reason I added the extra verification for writable + readonly etc is that in that case it's very easy to cause a miscompile if you aren't careful, so the verification seems better than silent UB. Here on the other hand the failure mode is an ignored attribute.

goldstein.w.n added inline comments.
llvm/lib/Transforms/Scalar/LICM.cpp
2177

nit: Given that the writeable attribute is only meaningful in conjunction with isDeferenceablePointer, maybe that check should just be part of the isWritableObject function? Then you can drop the ExplicitlyDeferenceableOnly bool. Just in general seems easier on the user to have the function do the checks of whether the writable attribute is meaningful for the object rather than the otherway around.

In general though I don't see any issues with the code, but am in no way qualified to approve this patch.

nikic added inline comments.Oct 10 2023, 2:06 AM
llvm/lib/Transforms/Scalar/LICM.cpp
2177

My thinking here was that some callers will have already performed the isDereferenceablePointer check anyway, so we can avoid doing it again in isWritableObject. In particular performCallSlotOptzn() already checks isDereferenceableAndAlignedPointer().

nikic updated this revision to Diff 557862.Oct 24 2023, 3:14 AM

Rebase & ping

aeubanks added inline comments.Oct 24 2023, 9:58 AM
llvm/docs/LangRef.rst
1540–1541

is this part necessary? or can we just defer to the next paragraph for what this attribute allows?

nikic added inline comments.Oct 24 2023, 12:18 PM
llvm/docs/LangRef.rst
1540–1541

I think so. This is the part specifying the actual operational semantics, the rest is just implications. This gives a general answer to some non-obvious questions, such as "if I have two aliasing noalias arguments that are only read inside the function, but one of them is marked writable, is this UB?" to which the answer would be "yes, because writable implies a write on entry, which makes aliasing noalias UB".

This revision is now accepted and ready to land.Oct 31 2023, 12:09 PM
This revision was landed with ongoing or failed builds.Nov 1 2023, 2:46 AM
This revision was automatically updated to reflect the committed changes.