Expand description
Taking out a conversion whose bits nothing reads.
Design: spec/optimizer/37-machine-level-optimization.md section 37.4.
A register is one register at every width, and what says how much of it is in play is the
instruction naming it. movzbl %sil, %edi writes thirty two bits of rdi and reads eight of
rsi, and if the only thing that ever reads rdi is a movb, then the twenty four bits the
widening worked out are bits nobody ever looks at. What is left of the widening once those bits
are taken away is a copy of eight bits into a register, which is what the instruction after it
was going to read anyway, so the widening goes and its readers read its source instead.
That is the bit group liveness of gcc/ext-dce.cc at the width this compiler needs it at.
Liveness answers whether a register is read at all, this answers how much of it is read, and
the second question is the first one asked per group of bits rather than per register. Section
37.4 says to build this one of the two passes it offers, because it is more general than
compare elimination and because the analysis is the liveness the allocator already computes
with a number on it.
§Where the conversions come from
Not from code anybody wrote. C promotes nearly every operand of nearly every expression to
int before doing anything with it, so a program that adds two chars widens both of them,
adds at thirty two bits and stores eight, and the front end writes every one of those
conversions out because each of them is in the language’s own description of what the program
means. crate::widths and tier four of the rewrite rules take the ones that are two
instructions next to each other in the same block. What is left for this pass is the ones that
are not: a conversion in one block whose readers are in another, and a conversion the selector
itself wrote because the machine instruction it picked wanted its operand at a width the value
did not arrive at.
§What it finds, measured
Both directions of conversion are in scope and only one of them turns up, which was not what
was expected and is worth writing down rather than rounding off. Over the 1916 programs of
tamnd/rucc-corpus at -O2 this takes out 970 instructions and puts back 45, and not one of
the 7040 widenings in that assembly is among them: the count of movz and movs is the same
before and after. What goes is 469 movl, 259 movw and 242 movb between registers, which
are the narrowings, and the 45 that come back are movq, which is the allocator wanting a
plain copy where a narrowing had been doing that job as well as its own.
That is tier four of the rewrite rules having already been through the corpus. A widening the rules could not reach is one whose upper bits some reader really does read, and there is nothing here for a bit counter to find in it. A narrowing is the other way round: the machine writes one where a value is put in a register at a width, and whether the bits above it matter is a question about every reader of the result rather than about the pair, which is the question only this pass asks.
2091 bytes of .text over the corpus, 76 programs smaller and two larger by a byte each, and
2048 bytes off SQLite’s amalgamation at -O2. The two that grow are an eight bit division,
where every narrowing that goes was also the move that got the answer out of the register the
division fixes, so the allocator writes a full width copy of the same pair in its place. Nine
of the ten are the same length either way and the tenth is movb %dl, %bl becoming
movq %rdx, %rbx, which is the one byte: the byte names of those two registers need no prefix
and the sixty four bit move needs the one that says so.
§The analysis
One number per register, which is how many of its low bits anything reads. It starts at none and grows, so a register nothing has been seen to read yet is one whose answer is still being worked out rather than one nothing reads.
Three things raise it. An instruction reading a register raises it to the width that
instruction names the operand at, which is rucc_target::BitInsts::width and is the target’s
answer rather than this pass’s. An edge carrying a register into a block raises it to whatever
the parameter it arrives as needs, which is what carries the answer across a block boundary and
is the whole reason this finds anything the rules do not. And an instruction that copies the
low bits of its source raises its source only as far as its own result is read, since the bits
of the source above that are bits it puts nowhere anything reads.
The last of those is what makes the answer a fixpoint rather than a walk: a chain of conversions passes the number back along itself, and how far it passes depends on a number the same pass is still working out. It only ever grows and it is bounded by the widest operand on the machine, so it settles.
§What it will not do
An operand the target’s description does not name at a width. An address register, an operand of an opcode written as no instruction at all, and an opcode from somewhere other than this target all answer that they read everything, which is section 37.7’s warning honoured by construction: a store reads every bit of the value it stores because the description says the operand is as wide as the store is, and anything the description is silent about is treated as reading the lot rather than as reading nothing.
A physical register on either side. Machine IR is in SSA form until the allocator has run, so a virtual register is written once and the register a reader would be sent to instead still holds what it held. A physical one is not: the frame pointer and the stack pointer are already physical here and a call writes every register it is allowed to, so sending a reader to one of those would be sending it to whatever happened to be there.
A conversion whose result is read as wide as it is written. That is a widening whose upper bits somebody does read, which is the whole instruction doing its job.
A conversion whose result nothing reads at all. That is an instruction that computes something nobody wants, which is dead code rather than dead bits, and taking it out here would be this pass answering a question it was not asked and reporting a number that says it found widenings it had not. What this is about is a register something reads less of than was put in it.
§Where it runs
After selection and before allocation, which is the window where the machine instructions exist
and the registers are still virtual. Section 37.6 puts it third in the group that runs there,
after combining and if-conversion and before compare elimination and addressing-mode folding,
and that is where crate::pipeline calls it.
Functions§
- dead
- Takes out every conversion whose result nothing reads above the width of its source, and gives back how many.