Expand description
A load of something the block has already read or written is that value, not a second read.
Design: spec/optimizer/16-gvn-and-pre.md section 16.2, which calls redundant load elimination
the real prize of that document. Value numbering over arithmetic is worth less than people
expect on C, because the front end does not generate the same expression twice and the
programmer does not write it twice. Loads are different. p->x three times in a function is
three reads of memory, and if nothing wrote through an aliasing pointer in between, two of them
are work the program does not have to do.
§The restricted version, and why this is it
Section 16.2 asks for two of these. The one at -O2 walks memory SSA back from each load to
its clobbering definition, translates an address backwards through a block’s parameters, and
sees through a memcpy. The one here is the other: same block only, no phi translation, no
memcpy, and no memory SSA at all. That is the version the section says should be at -O1,
and it says why: it catches the repeated p->x in one basic block, which is the majority of the
opportunities, for a fraction of the machinery.
The two are not alternatives and this is not a stand-in for the other one. What it is is the part that can be written without an alias oracle, and the part whose cost is one walk over each block.
§What it knows
One table per block, from an address to the value that address holds, thrown away at the end of the block because what reaches a block from its predecessors is the question this version does not ask. A store writes what it stored. A load that had to happen writes what it read. Either way the next load of that address is that value.
An address is one SSA value, compared by identity. Two pointers that are the same address by
arithmetic and not by name are two addresses here, which costs opportunities and no
correctness: an address computed twice out of the same parts is tracked twice and neither copy
is forwarded to the other. What would give the two one name is value numbering over the
arithmetic, which is the other half of document 16 and is not built. It costs more than it
sounds like it should. a[i] = v; total += a[i]; written in C is a store and a load whose
addresses are two separate runs of the same multiply and add, because the front end emits the
subscript twice, so the shape this pass is most obviously for is one it cannot see until that
lands.
§What throws the table away
Anything that could write anywhere. There is no alias analysis in this pass, so a store to one
address is treated as a possible write to every address, and the table is emptied before the
store records what it just wrote. A call, an atomic, a fence and a memcpy empty it and record
nothing. That is Opcode::touches_memory, which is the conservative predicate, so an opcode
added to the IR later throws the table away rather than being quietly assumed harmless.
This costs less than it sounds like. A store immediately followed by a read of what was stored
still works, because the store empties the table and then puts back the one entry the load is
about to ask for. What the barrier really costs is the second address: *p = v; total += *q;
with two locals is refused even where the two cannot be the same object, and telling them apart
is the alias oracle’s answer rather than this pass’s.
A volatile access empties the table and records nothing either way. Whether a volatile store
could be forwarded from is an argument about what volatile promises, and this pass does not
need to have it.
§The width, which is where the miscompilation would be
Section 16.6 names it as the single most likely wrong answer in that document: a load forwarded from a store of a different size. Section 09.5 has the three-way distinction, which is that a store covering the load exactly is the value, one covering it partially needs an extract, and one not covering it at all means the walk should continue.
This pass only ever takes the first of the three. The address has to be the same SSA value and the type has to be equal, which is the same width and the same reading of the bits, and anything else is left alone and counted. Two-way is what somebody writes first and it is right most of the time, which is what makes it worth being explicit that this is not that.
§What it leaves behind
The load goes, rather than staying and having its result forwarded. crate::dce would not
remove it: has_effects is true of every load, because a pass that removed one would need to
know the address is dereferenced anyway, and this is the pass that knows it. The load being
removed is safe for a reason nothing else in the pipeline has: something already read or wrote
that exact address in this block, so the address is one the program dereferences whatever
happens next.
The store stays. Removing a store that a later store covers is dead store elimination, which is document 17 and a different pass.
Structs§
- Load
Forward - The pass.