Skip to main content

Module retry

Module retry 

Source
Expand description

The read modify writes the machine has no single instruction for, as a loop around the compare and exchange.

Design: spec/10-backend.md section 10.5, which names this as the third exemption from the rule that every lowering is a rule in the table.

§What is here and why it is not a rule

Opcode::AtomicRmw carries thirteen operations. x86-64 has an instruction for three of them: xchg puts a value there, lock xadd adds one, and a subtraction is the same instruction over the negated operand. The other ten have no instruction at any width, and what stands in for one is the loop every architecture manual writes out by hand: read what is there, work out what should be there instead, put it back if nothing else got in first, and go round again when something did.

That loop is blocks, and blocks are why this is a pass rather than a rule. A rule rewrites one instruction into instructions; it has no way to say that control leaves a block here and arrives somewhere else. So the shape is built in the IR, before anything below has been told what the blocks are, and by the time the selector sees it there is nothing left but a compare and exchange it already has a rule for.

§Where it runs

Straight after switch::switches and before expand::orderings.

After the switches because both of these create blocks and expand may not: every pass in expand.rs rewrites an instruction in the place it stands, and the two passes that change the shape of the control flow are kept together at the front where the function is still the one the optimizer handed over.

Before the orderings because the head of the loop reads the address with an atomic_load, and it is expand::orderings that turns that into the plain load this machine does anyway. Running the other way round would leave an ordered access nothing below understands.

§What the loop is

For old = atomic_rmw op, addr, operand in a block, with tail for whatever followed it:

head:                              ; what was in front of the instruction
  first = atomic_load ty, addr     ; relaxed, because the compare and exchange carries the order
  jump spin(first)
spin(seen):
  want = <op> seen, operand
  got, ok = cmpxchg addr, seen, want
  br_if ok, done(seen), spin(got)
done(before):                      ; `tail`, with every use of `old` reading `before`

The value the loop answers is the one that was there before, which is what AtomicRmw answers, so the value handed to done is seen and not want. A name that asked for the value afterwards got the arithmetic that works one out from the other back in rucc-lower, over the result of the instruction this pass is rewriting, and that arithmetic is in tail and needs nothing from here.

The failing edge carries got rather than going back to the load. That is the whole reason the compare and exchange answers what it found: a second read would be a second chance to be wrong, and the value the exchange saw is the freshest one there is.

The edge from spin to itself is critical, since spin has two ways out and is arrived at two ways. Nothing here splits it, because split::critical runs below and splitting it twice is worse than splitting it once.

§What it leaves alone

An exchange, an addition and a subtraction, because those three have instructions and a loop would be slower and larger for no reason. Anything whose value is not an integer the machine compares and exchanges at, which is the two floating operations: a compare and exchange of a float wants the value carried through an integer of the same width, an eighty bit float has no such width, and until that is worked out the refusal in lower.rs is the honest answer.

Functions§

loops
Rewrites every read modify write this machine has no instruction for into a loop around the compare and exchange, and leaves the rest of them alone.