This is an archive of the discontinued LLVM Phabricator instance.

[BasicAliasAnalysis] Switch from isMallocOrCallocLikeFn to onlyAccessesInaccessibleMemory
AcceptedPublic

Authored by Bryce-MW on Jan 12 2022, 7:27 PM.

Details

Summary

Allocation functions should be marked correctly with onlyAccessesInaccessibleMemory so this change can be made (assuming the comment was accurate). In the future, this could be expanded to check inaccessiblememorargmemonly as well but that would have to ensure that the args don't alias with the memory location in question.

Diff Detail

Event Timeline

Bryce-MW created this revision.Jan 12 2022, 7:27 PM
Bryce-MW requested review of this revision.Jan 12 2022, 7:27 PM
Herald added a project: Restricted Project. · View Herald TranscriptJan 12 2022, 7:27 PM
Bryce-MW updated this revision to Diff 399542.Jan 12 2022, 9:00 PM
  • malloc is inaccessiblememonly
nikic requested changes to this revision.Jan 13 2022, 12:11 AM
nikic added a reviewer: reames.
nikic added a subscriber: nikic.

Does anything fail if you simply remove this code? We should already be handling inaccessiblememonly generically in AA, there's no need to special-case it here again.

Something I want to note is that we currently do not set inaccessiblememonly for new in clang (we only set noalias), so maybe that should be changed first?

This revision now requires changes to proceed.Jan 13 2022, 12:11 AM
Bryce-MW updated this revision to Diff 399743.Jan 13 2022, 11:45 AM
  • Try removing the code

I'll see what happens when removing that section. It does sound like that should be added to clang. I'll take a look at that before this is landed. I don't know enough about the standards to say if op new is required to only access inaccessible memory but that sounds reasonable since I think op new is supposed to not be observable.

I'll see what happens when removing that section. It does sound like that should be added to clang. I'll take a look at that before this is landed. I don't know enough about the standards to say if op new is required to only access inaccessible memory but that sounds reasonable since I think op new is supposed to not be observable.

Er, the discussion here has gone a bit off base. A couple of points:

  1. The existing code does *NOT* handle operator new. It very specifically uses the accessor which does not include opnewlike.
  2. The reason for this is that calls to the replaceable global allocator in C++ are *not* removable (i.e. are observable) *unless* they come from an new expression. (See the comment in isAllocRemovable for pointers to relevant sections of the spec.) Since we don't model this distinction, we must treat all as non-removable.

So, no, please do *not* mark opnew as inaccessiblememonly in clang. That would be wrong.

Thanks, like I said, I'm not super knowledgeable about the standards (especially for C++, I'm mostly a C person). One thing I will note is that isMallocOrCallocLikeFn does return true for op new as far as I can tell.
The test it does is

(FnData->AllocTy & MallocOrCallocLike) != FnData->AllocTy // returns true when they type doesn't match and false when it does

MallocOrCallocLike = MallocLike | CallocLike | AlignedAllocLike
MallocLike = 1<<1 | OpNewLike
OpNewLike = 1<<0
CallocLike = 1<<3
AlignedAllocLike = 1<<2
so MallocOrCallocLike = 0b1111
an OpNewLike function will be tagged OpNewLike = 1

so the test becomes:
(1 & 0b1111) != 1
(1) != 1
false

If it were true then getAllocationDataForFunction would return None causing isMallocOrCallocLikeFn to return false. But it returns false, so as long as the arguments match the check, then getAllocationDataForFunction will return some data making isMallocOrCallocLikeFn return true. This also means that for similar reasons, isAllocRemovable is going to return true for op new but it sounds like you are saying that it should not.

Er, you're correct, but WTF? We consider an operator new "malloc like"? That's simply wrong on so many levels.

Well, then the original code is buggy.

Well, then the original code is buggy.

To clarify, buggy in a way you are not obligated to fix. If you want to go ahead and change how we encode the bug, and the clang folks accept that, I have no objection.

Based on the comments on AllocType, it seems that the original difference between OpNewLike and MallocLike was whether it returned null. In that context, it's reasonable that something that never returns null can be used in the place of something that could return null. It's obviously used for more than just that now so this has probably caused a few places to be incorrect.

It seems like this patch of just removing the condition entirely should at least make this part of the code correct even though it does change the functionality slightly.

Er, you're correct, but WTF? We consider an operator new "malloc like"? That's simply wrong on so many levels.

Well, then the original code is buggy.

Non throwing version of new (which may return null) is malloc like.

Er, you're correct, but WTF? We consider an operator new "malloc like"? That's simply wrong on so many levels.

Well, then the original code is buggy.

Non throwing version of new (which may return null) is malloc like.

Please see the C++ specification wording in expr.new paragraph 13.

Calls to the replaceable global allocator are not removable unless coming from a new expression. I don't believe we have enough information in IR today to tell if they're coming from new expressions or not.

Er, you're correct, but WTF? We consider an operator new "malloc like"? That's simply wrong on so many levels.

Well, then the original code is buggy.

Non throwing version of new (which may return null) is malloc like.

Please see the C++ specification wording in expr.new paragraph 13.

Calls to the replaceable global allocator are not removable unless coming from a new expression. I don't believe we have enough information in IR today to tell if they're coming from new expressions or not.

Yeah I know and we have many related PRs. Everything related to C++ “new” is a mess.

cd36b29 adjusts the code to make the fact we treat opnew like malloc slightly more explicit. This doesn't change any semantics, it just makes it easier to notice this odd case.

Er, you're correct, but WTF? We consider an operator new "malloc like"? That's simply wrong on so many levels.

Well, then the original code is buggy.

Non throwing version of new (which may return null) is malloc like.

Please see the C++ specification wording in expr.new paragraph 13.

Calls to the replaceable global allocator are not removable unless coming from a new expression. I don't believe we have enough information in IR today to tell if they're coming from new expressions or not.

Yeah I know and we have many related PRs. Everything related to C++ “new” is a mess.

Agreed. Strongly.

nikic accepted this revision.Jan 14 2022, 12:41 AM

LGTM

This revision is now accepted and ready to land.Jan 14 2022, 12:41 AM
This revision was automatically updated to reflect the committed changes.
nikic added a comment.Jan 14 2022, 2:26 PM

It looks like this had some very significant codegen impact, including a 13% code size regression on 7zip: https://llvm-compile-time-tracker.com/compare.php?from=cd97aaee5fef87215eebbd17a31099fe4437ce1f&to=1f2cfc4fdc1eefb2c5f562c77a5fe7e916bbf670&stat=size-text

We should probably revert this until that is investigated. I suspect that it is due to the mentioned issue with new, but I didn't expect impact to be this large.

I've reverted it. That's quite significant. I'm not sure what we should do here. The current situation is incorrect but as we've seen fixing it causes significant regression. I'm guessing even if op new can't technically be considered inaccessiblememonly in the general case, it probably is commonly inaccessiblememonly which is why this worked in the past. I'm not familiar enough with clang to know if it could say add the annotation if linking against a library that is known to have op new be inaccessiblememonly.

Bryce-MW reopened this revision.Jan 14 2022, 3:22 PM
This revision is now accepted and ready to land.Jan 14 2022, 3:22 PM
nikic added a comment.Jan 14 2022, 3:34 PM

Yes, this is a tricky problem. See also https://github.com/llvm/llvm-project/issues/48366 for some relevant discussion, though in different context. We have a pretty fundamental problem when it comes to handling of replaceable allocator functions in LLVM. Setting inaccessiblememonly (and arguably also noalias) on them is only strictly correct if the allocator implementation (including any state it may use) is not visible to LLVM.

That's worse than I thought since it appears LLVM can't know if the allocator was replaced until link time it seems

My current thought on how this could be done is to add some sort of optimization flag to clang, say -fstrict-op-new. If off (-fno-strict-op-new), it would mark op new as inaccessiblememonly and possibly removable if we ever add a way to mark that. When they would enable that flag would be up to them. I would say disabled unless -Ofast but maybe because of the regressions we've seen, it should be disabled at -O3 or something. I can't see any easier way to solve this issue unless we basically leave this optimization to LTO where we would know if op new was globally replaced or not.

My current thought on how this could be done is to add some sort of optimization flag to clang, say -fstrict-op-new. If off (-fno-strict-op-new), it would mark op new as inaccessiblememonly and possibly removable if we ever add a way to mark that. When they would enable that flag would be up to them. I would say disabled unless -Ofast but maybe because of the regressions we've seen, it should be disabled at -O3 or something. I can't see any easier way to solve this issue unless we basically leave this optimization to LTO where we would know if op new was globally replaced or not.

We do already have something like this in the form of -fassume-sane-operator-new (enabled by default I believe). This is what currently gates the noalias addition. I don't know whether gating inaccessiblememonly under that as well would be acceptable, as we don't seem to have any documentation on what "sane" requires in that context.

If I can manage to find my way around the clang source code, I could put in a patch and see what people say about it?

I'm skimming the BasicAA code, and I don't see where we handle the isNoAliasCall case inside of getModRefInfo(CB, Loc). Is it possible the effect here was because this was the only form of handling we had for noalias results?

It looks like we have support for escape based reasoning, and identified object based reasoning, but I don't see anywhere that we directly check for noalias in an analogous manner.

Another possibility: We appear to not be annotating calloc with inaccessiblememonly.

nikic added a comment.Jan 17 2022, 9:27 AM

I'm skimming the BasicAA code, and I don't see where we handle the isNoAliasCall case inside of getModRefInfo(CB, Loc). Is it possible the effect here was because this was the only form of handling we had for noalias results?

Noalias calls are considered local identified objects, and are considered in any code dealing with those. For calls specifically, we don't modref local identified not-captured-before objects.

Another possibility: We appear to not be annotating calloc with inaccessiblememonly.

Good point!

I don't think the conclusion reached here makes sense. On the surface, it seems like we need to tweak inaccessiblememonly to allow writes to newly allocated returned memory. Do we have a use case for the version of inaccessiblememonly which disallows that?

From my perspective, since the return value is noalias, I feel like that memory is inaccessible until it is returned so anything done to that memory, such as clearing it, should be fine. It seems like the issue that they had was with various bailouts for inaccessible memory in llvm/lib/Transforms/Scalar/DeadStoreElimination.cpp. We do have a check for an allocation function in storeIsNoop. I don't know if that happens before the other checks but it seems at least worth testing to see whether the old issue still occurs on today's code: https://github.com/llvm/llvm-project/issues/49487. It probably wouldn't be hard to add a test for that

@Bryce-MW It's a bit more complicated than just checking existing test cases. From memory, this explains some code in both AliasAnalysis.cpp and BasicAliasAnalysis.cpp which I'd thought odd when looking through it this morning. I think the attribute change is probably worthwhile, but it will require both an RFC and a number of changes to existing code.

I can drive that. I'll wait a day or two to see if anyone remembers a strong reason for the current semantic, then work up some patches and an RFC to llvm-dev.

That sounds reasonable to me!

nikic added a comment.EditedJan 18 2022, 12:06 AM

@Bryce-MW It's a bit more complicated than just checking existing test cases. From memory, this explains some code in both AliasAnalysis.cpp and BasicAliasAnalysis.cpp which I'd thought odd when looking through it this morning. I think the attribute change is probably worthwhile, but it will require both an RFC and a number of changes to existing code.

I can drive that. I'll wait a day or two to see if anyone remembers a strong reason for the current semantic, then work up some patches and an RFC to llvm-dev.

Not sure I understand what is being suggested here. I do think that using inaccessiblememonly for calloc is fine, as the memory is inaccessible at the time of the call, and becomes accessible after it returns. calloc isn't really different from other allocation functions here.

I think the problematic interaction with DSE is the fact that MSSA does not model object lifetime, so if a new object comes into existence (via alloca, malloc ... or calloc) it will be liveOnEntry. The accepted way to handle that right now is to check that the defining access is liveOnEntry and the underlying object is an alloca or allocation.

I've put up D117543 to show that DSE would be perfectly capable of handling inaccessiblememonly calloc, it just requires a slightly different implementation.

Not sure I understand what is being suggested here. I do think that using inaccessiblememonly for calloc is fine, as the memory is inaccessible at the time of the call, and becomes accessible after it returns. calloc isn't really different from other allocation functions here.

I agree that calloc behaves like malloc in this respect. The basic question I see being posed here is whether inaccessiblememonly is legal on any allocation routine.

I was originally thinking we needed to change the definition to explicitly allow this, but it sounds like you think this was always allowed. I put up a langref change (https://reviews.llvm.org/D117571) to make that really explicit.

I think the problematic interaction with DSE is the fact that MSSA does not model object lifetime, so if a new object comes into existence (via alloca, malloc ... or calloc) it will be liveOnEntry. The accepted way to handle that right now is to check that the defining access is liveOnEntry and the underlying object is an alloca or allocation.

I've put up D117543 to show that DSE would be perfectly capable of handling inaccessiblememonly calloc, it just requires a slightly different implementation.

I think I agree. I was originally thinking we'd need AA changes for the definition point (what you call "coming into existence"), but looking at how we handle allocas, you're proposal is more consistent than mine.

Is your change ready for review? Or do you need to do something else first?

Is your change ready for review? Or do you need to do something else first?

As we seem to agree on direction, yes. Just needs to land together with the corresponding SLC change to add inaccessiblememonly to calloc.

reames added a comment.EditedJan 18 2022, 2:58 PM

Once D117543 (and the associate calloc revert) land, I think it's worth seeing if the regression here still exists.

I don't see where we tag op-new as inaccessiblememonly, so we may still have a problem, but I think the calloc case is likely to help a lot.

Edit: Even if opnew still blocks removal, we can probably narrow the check to only kick in for opnew and add an explicit comment about that. Would help to confirm our theory about it being a lack of annotation on those functions specifically at least.

I don't think op new is annotated as inaccessible memory anywhere. I'm working on a patch in D117600. I was just about to submit it for review

I don't think op new is annotated as inaccessible memory anywhere. I'm working on a patch in D117600. I was just about to submit it for review

https://github.com/llvm/llvm-project/commit/ab243efb261ba7e27f4b14e1a6fbbff15a79c0bf#diff-4e887ae3fd3d1f1ad429d92733318ce1efac179eb498d9c4c1438d3255855018