-
Notifications
You must be signed in to change notification settings - Fork 10.5k
SIL: simplify deleting instructions while iterating over instructions. #62480
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
Conversation
@swift-ci test |
@swift-ci clean smoke test |
a4b331d
to
89c1ba2
Compare
@swift-ci test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a killer shortcut. I didn't know it was possible to do this and still reuse the ilist infrastructure.
Some changes in behavior though...
-
Dereferencing the iterator now gives you the deleted instruction instance. I guess that was intentional. I don't see any problem with it.
-
It's now possible to create an instruction iterator from a deleted instruction! I think that's dangerous. Can do something to prevent it?
-
The behavior of the new iterator is now incorrect when instructions are created. For example, a forward iterator at deleterInst will now skip over the newInst.
deletedInst
newInst
nextInst
I thought it was an important feature of the updating iterator to support instruction creation and deletion in the same code.
- I think this breaks badly when an instruction moves to a different block and a deleted iterator happens to still refer to it.
I don't like registering iterators. But I'm not sure it's a good tradeoff if we create new classes of bugs instead.
@@ -3885,8 +3885,7 @@ static void deleteRewrittenInstructions(AddressLoweringState &pass) { | |||
} | |||
// willDeleteInstruction was already called for open_existential_value to | |||
// update the registered type. Carry out the remaining deletion steps. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Carry out the remaining deletion steps.
This comment doesn't seem to make sense any more. Maybe "now fully erase the instruction, which will harmlessly call willDeleteInstruction again"
(I'm not sure that's actually harmless but you're claiming it is)
IIUC, the existing
But we never use the updating iterator this way. We always use it in iterator loops, like
(which is now
In such a for-loop it's never the case that an iterator of a deleted instruction is exposed. Unless you do something stupid like
If you e.g. delete the current instruction and replace it with 2 new instructions:
then with the old scheme, the next iteration after the current (deleted) instruction would see
True, but it also breaks badly with the old mechanism. Though, we could catch this case with the new scheme (check against the end-iterator of the current instruction's parent block), I don't think it's worth the effort. It's a known limitation that instructions must not be moved to other blocks while iterating. |
89c1ba2
to
9f55f9e
Compare
@swift-ci smoke test |
Instructions can only be moved and erased, but never _removed_ from a block.
… same instruction. This happens in AddressLowering.
…ist to a `std::vector`
Instead of setting the parent pointer to null, set the `lastInitializedBitfieldID` to -1. This allows to keep the parent block information, even when an instruction is removed from it's list.
Add `deletableInstructions()` and `reverseDeletableInstructions()` in SILBasicBlock. It allows deleting instructions while iterating over all instructions of the block. This is a replacement for `InstructionDeleter::updatingRange()`. It's a simpler implementation than the existing `UpdatingListIterator` and `UpdatingInstructionIteratorRegistry`, because it just needs to keep the prev/next pointers for "deleted" instructions instead of the iterator-registration machinery. It's also safer, because it doesn't require to delete instructions via a specific instance of an InstructionDeleter (which can be missed easily).
…g instructions during iteration Replace the generic `List` with the (non-generic) `InstructionList` and `BasicBlockList`. The `InstructionList` is now a bit different than the `BasicBlockList` because it supports that instructions are deleted while iterating over the list. Also add a test pass which tests instruction modification while iteration.
9f55f9e
to
d4d1620
Compare
@swift-ci smoke test |
My entire motivation for implementing the iterator registry was to handle inserting and moving instructions in all cases! You'll notice that the implementation was carefully designed to do that. Obviously, if we're only deleting nodes from a single list, then the usual mechanism of keeping the old back-pointers works. I badly wanted to avoid the registry, because it's not as efficient, but I realized it was impossible to handle such cases correctly without it. With the current registry, moving an instruction is always supposed to have the same iterator behavior as removing and reinserting the instruction. It's fine to move instructions into different blocks. Doing so should always update existing iterators so they continue referring to the same block. So, I'm not sure what you mean by "moving is broken". With the current registry, all new instructions inserted after the current position should be visited. So this should visit both inst1 and inst2:
The only way that you will skip a new instruction is if you advance the iterator before creating the instruction: This explicitly skips the new instruction as expected:
This does not skip the new instruction!
Again, I've fixed bugs in code that was doing this sort of thing. And that's the only reason that I thought the registry was needed. Regarding the problem of having two instances of the deleter, I think that's a separate issue. The deleter should be a singleton pattern based on the current pass. Other than the small amount of overhead, I don't see any problem with registering the iterators. The important thing is that you understand my original motivation. If you think keeping the old node pointers without updating them can't create subtle bugs then that's great. Or, if those problems can't occur in Swift sources (I don't understand how that works yet), then we could live dangerously in C++ in the meantime. |
For example, this crashes:
(after the move the loop condition is comparing against the wrong end-iterator).
In swift it's no problem because it's always comparing against the end iterator of the current instruction's block. This is e.g. happening in https://github.com/apple/swift/blob/main/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/ObjCBridgingOptimization.swift where
That's only the case if you call
It's definitely not a regression. And I think it's okay. We cannot catch all bad cases anyway and moving instructions to a different block is not a common thing to do. Anyway, thanks a lot for taking the time to review and for the discussion! |
Well, that's supposed to work just like erasing As I mentioned, I also designed the iterator registry to correctly handle moving within the block and inserting new instructions. |
Not using the new instruction callback is definitely a bug. The fact that it's easy to mess that up is a separate issue. Having an iterator that sometimes sees new instructions and other times mysteriously skips them does not make sense. |
I don't know how this could have ever worked. There is (and never was) a callback for moving instructions - only for deleting instructions. So the iterator registry cannot be informed of moving instructions. |
Ok, there are only two relevant questions then:
|
Exactly. It's https://github.com/apple/swift/blob/0d666e291183466f4abd4b83484aa1293eb59f84/lib/SILOptimizer/SILCombiner/SILCombiner.h#L73
Yeah, I think it's fine to not do any special handling for moving instructions. It's just important that it cannot result in a crash and the behavior is not undefined. |
On the C++ side we already supported this via the
InstructionDeleter
utility.On the Swift side it was not possible to delete instructions during iteration at all.
This change adds support for Swift SIL and also significantly simplifies the C++ implementation.
Instead of the iterator-registration machinery (
UpdatingListIterator
andUpdatingInstructionIteratorRegistry
) the new approach uses a simple trick to keep the iterators (C++ and Swift) in a consistent state: when "deleting" instructions, don't null the instruction's prev, next and parent pointers.This allows to "jump" back into the not-deleted instruction list from a deleted instruction.
Beside the simpler implementation, it's also safer, because it doesn't require to delete instructions via a specific instance of an
InstructionDeleter
(which can be missed easily).The new C++ APIs are
SILBasicBlock::deletableInstructions()
andSILBasicBlock::reverseDeletableInstructions()
.In Swift, no new APIs are needed. One can just use the existing
BasicBlock.instructions
while being able to delete instructions in the list during iteration.