Replies: 1 comment
-
This is done. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
From @tangent-vector :
It seems like the result of us spinning around on this several times was that there are really two different cases of instructions where we do hoisting/deduplication today:
True constants, where the IR instruction represents a logical value.
Instructions that are always safe/beneficial to deduplicate, so that it is worth folding that behavior into the IRBuilder
The current code tries to handle both (1) and (2) with the same deduplication cache, and that is at least part of what causes challenges.
Note: an example of an instruction in case (2) is lookup_interface_method or specialze: the same operation applied to the same operands will always produce the same result and the operation is such that we can safely do global value numbering and "hoist" it to the earliest point possibe.
Case (2) can still be folded into the IR builder, or we can just define a distinct pass to handle it.
(One random aside is that once IRBuilders send all instruction creating through a single bottleneck, it becomes possible to allow some kind of policy to intercept that point, so that maybe different IR builders have different sets of things they automatically optimize)
Anyway...
Case (1) is the one we actually care about for deduplication, because things that are a logically-constant values are the things that need to be used as keys when looking up and re-using specializations in various cases.
We've seemingly agreed that while there are non-zero cases where the compiler might currently try to replaceUses a constant (like a type) those are probably all things we can handle better with a more special-case approach.
We are thus close to a solution of:
All literals are constants
There is a list of opcodes such that if that opcode is applied to a type and operands that are all constants, the resulting instruction is a constant
The IRModule should maintain a cache of those instructions
All constants should be inserted at the global scope of their module
If for some reason we want to remove a constant instruction from the module, we need logic to unlink it from use/def and parenting information while leaving its operands filled in. If it is later looked up in the cache again, we can "revitalize" that instruction and re-add it to the module.
Constant instructions need some kind of flag/marker bit so that they can be identified without recursion
Attempts to set the type or an operand of a constant instruction to a new value need to be an error
Attempts to invoke replaceUsesWith() on a constant instruction need to be an error
We can have a more expansive version of setOperand() / replaceUsesWith() that handles constant instructions, at the cost of potentially doing more complicated surgery on the IR (e.g., setting the operand of a constant X to some value actually involves constructing a new instruction Y with the desired operands, then identifying all the instructions that use X and either replacing that use directly (if non-constant) or creating a new copy of that instruction...)
The IR deserialization logic needs to be able to construct instructions in an uninitialized state and then initialize them. We need to allow the deserialization to do its thing, and then have a post-process pass that identifies constants in whatever got deserialized (adding them to the cache, and dealing with any conflicts if/when they arise)
I think that is an implementable plan at this point
One interesting thing I realized when I was thinking about this while driving somewhere:
We had a concern when we talked about this that we use various caches with IRInstKeys during our passes, and a big concenr with those caches is that we cannot allow the keys that they use to change their hash, or to be invalidated (e.g., if the representative IRInst* for some value changes to a different instruction with a different pointer).
First, there's the non-trivial thing we could in principle compute the hash of a constant IRInst based on its value rather than on its pointer, but that's maybe not worth getting deep into.
The more interesting thing is that if we are doing this deduplication thing, then every IRModule will already have a built-in deduplication cache and, furthermore, that deduplication cache is such that we can actually re-use it across passes, or even across serialization boundaries.
We also have the property that the way a "constant" is defined above intentionally does not take into account decorations or children (although it will take into account IRAttrs, which is 100% intentional in the design of IRAttrs).
What that means is that if we ever wanted to cache other things in our IR, we can actually do the following:
Define a unique constant-capable IROp opcode for our cache entries (e.g., cachedSpecialization)
Set the operands of that IROp to be the key(s) of each entry (e.g., cachedSpecialization(someGeneric, arg1, arg2, ...))
Now, any attempt to create an instruction with the given opcode and (constant) operands will either retrieve a matching pre-existing instruction, or create a fresh one
We can store the value for such an entry as something like a return instruction set as a child of the cachedSpecialization (or whatever inst). Because the value is referenced via a child and not an operand, it cannot affect the hash code of the instruction.
We can thus hijack the single deduplication cache to store things like specializations of generics, layouts for types, etc. Anything where we want to be able to look it up quickly based on a combination of IRInsts.
Beta Was this translation helpful? Give feedback.
All reactions