If I’m not mistaken, there is currently a fundamental issue with the ISel table patterns for two-address instructions (e.g. add
). I first ran into this problem when examining the problem below, which should return 69
, but returns 70
.
In this IR, with a
initialised to 34
, the value of %9
should be 69
(note that %9 = %6 + %8
, where
%6
is the initial value of a
(i.e. 34
), and %8
is %6 + 1
(i.e. 35
), so %9 = 34 + 35 + 69
).
defun main (%0, %0, %0) global leaf nomangle {
bb1:
%4 │ │ .ref b | @integer
%5 │ │ .ref a | @integer
%6 │ │ load %5 | integer
%7 │ │ imm 1 | <integer_literal>
%8 │ │ add %6, %7 | integer
│ │ store into %4, %8 | void
%9 │ │ add %6, %8 | integer
│ │ ret %9 | void
}
However, the result of the load
is clobbered by the first add
in the backend:
r1 | mov "a" integer, r1 8 DEF ; <-- This value, loaded into r1,
v8 | add 1, r1 8 ; <-- is clobbered by the add here,
r2 | mov r1 8, r2 8 DEF
v5 | mov r2 8, "b" integer
v9 | add r2 8, r1 8 ; <-- but intended to be used here.
r1 | ret
The reason for this seems to be the following rule in the ISel table:
match
MIR_ADD i1(Register lhs, Register rhs)
emit {
MX64_ADD(rhs, lhs)
MX64_MOV(lhs, i1)
}
The fundamental problem here is that the add
clobbers one of the operands before the result is moved
into a new register. Presumably, this rule assumes that neither operand will be used after this add
. This is often not true in optimised codegen. This ISel rule is correct, iff this add
instruction is the last use of both lhs
and rhs
.
A while ago, I mentioned this case and that, if the operand to an instruction in a pattern happens to be used somewhere outside the pattern, the pattern can generally not be applied. I don’t know if we’re checking for that at the moment, but we should.
The most general way of emitting a two-register add
correctly if the operands need to be preserved would be to move either operand into a new register and add the other operand into that new register. This means that, in this particular case, the pattern should work if we swap the order of the mov
and add
and add into i1
, but I haven’t thought about this too much.
Irrespective of that, there are cases (such as that of adding an immediate and a register) where the ‘ideal’ codegen (add the immediate directly into the register, clobbering it, which is 1 add
instruction) and the ‘operands-preserving’ codegen (move either operand into a new register and add the other one, preserving the register operand, which is 2 instructions, 1 mov
+ 1 add
) are not the same.
This means that, for those cases, we need both ‘ideal’ patterns that are applied iff the operands of all instructions in the pattern are not used outside the pattern (however, it’s ok if an operand of one instruction in the pattern is used by another instruction in the same pattern), as well as ‘fallback’ patterns that must not clobber any of their operands.
Side note: Yes, for add
in particular, we can also use lea
instead to get around this, but this problem persists in all the other two-address instructions that clobber one of their operands.