rucc_codegen/retry.rs
1//! The read modify writes the machine has no single instruction for, as a loop around the compare
2//! and exchange.
3//!
4//! Design: `spec/10-backend.md` section 10.5, which names this as the third exemption from the rule
5//! that every lowering is a rule in the table.
6//!
7//! # What is here and why it is not a rule
8//!
9//! `Opcode::AtomicRmw` carries thirteen operations. x86-64 has an instruction for three of them:
10//! `xchg` puts a value there, `lock xadd` adds one, and a subtraction is the same instruction over
11//! the negated operand. The other ten have no instruction at any width, and what stands in for one
12//! is the loop every architecture manual writes out by hand: read what is there, work out what
13//! should be there instead, put it back if nothing else got in first, and go round again when
14//! something did.
15//!
16//! That loop is blocks, and blocks are why this is a pass rather than a rule. A rule rewrites one
17//! instruction into instructions; it has no way to say that control leaves a block here and arrives
18//! somewhere else. So the shape is built in the IR, before anything below has been told what the
19//! blocks are, and by the time the selector sees it there is nothing left but a compare and exchange
20//! it already has a rule for.
21//!
22//! # Where it runs
23//!
24//! Straight after `switch::switches` and before `expand::orderings`.
25//!
26//! After the switches because both of these create blocks and `expand` may not: every pass in
27//! `expand.rs` rewrites an instruction in the place it stands, and the two passes that change the
28//! shape of the control flow are kept together at the front where the function is still the one the
29//! optimizer handed over.
30//!
31//! Before the orderings because the head of the loop reads the address with an `atomic_load`, and it
32//! is `expand::orderings` that turns that into the plain load this machine does anyway. Running the
33//! other way round would leave an ordered access nothing below understands.
34//!
35//! # What the loop is
36//!
37//! For `old = atomic_rmw op, addr, operand` in a block, with `tail` for whatever followed it:
38//!
39//! ```text
40//! head: ; what was in front of the instruction
41//! first = atomic_load ty, addr ; relaxed, because the compare and exchange carries the order
42//! jump spin(first)
43//! spin(seen):
44//! want = <op> seen, operand
45//! got, ok = cmpxchg addr, seen, want
46//! br_if ok, done(seen), spin(got)
47//! done(before): ; `tail`, with every use of `old` reading `before`
48//! ```
49//!
50//! The value the loop answers is the one that was there before, which is what `AtomicRmw` answers,
51//! so the value handed to `done` is `seen` and not `want`. A name that asked for the value afterwards
52//! got the arithmetic that works one out from the other back in `rucc-lower`, over the result of the
53//! instruction this pass is rewriting, and that arithmetic is in `tail` and needs nothing from here.
54//!
55//! The failing edge carries `got` rather than going back to the load. That is the whole reason the
56//! compare and exchange answers what it found: a second read would be a second chance to be wrong,
57//! and the value the exchange saw is the freshest one there is.
58//!
59//! The edge from `spin` to itself is critical, since `spin` has two ways out and is arrived at two
60//! ways. Nothing here splits it, because `split::critical` runs below and splitting it twice is
61//! worse than splitting it once.
62//!
63//! # What it leaves alone
64//!
65//! An exchange, an addition and a subtraction, because those three have instructions and a loop
66//! would be slower and larger for no reason. Anything whose value is not an integer the machine
67//! compares and exchanges at, which is the two floating operations: a compare and exchange of a
68//! float wants the value carried through an integer of the same width, an eighty bit float has no
69//! such width, and until that is worked out the refusal in `lower.rs` is the honest answer.
70
71use rucc_ir::{
72 Block, Builder, Extra, Flags, Func, Inst, IntPred, MemInfo, MemOrder, Opcode, RmwOp, Type,
73 Value,
74};
75
76/// Rewrites every read modify write this machine has no instruction for into a loop around the
77/// compare and exchange, and leaves the rest of them alone.
78///
79/// The function is changed in place. It gains two blocks and loses one instruction for each one
80/// rewritten, and every function without one of these is untouched.
81pub fn loops(func: &mut Func) {
82 let found: Vec<Inst> = func
83 .blocks()
84 .flat_map(|block| func.insts(block).collect::<Vec<_>>())
85 .filter(|&inst| wanted(func, inst))
86 .collect();
87 for inst in found {
88 rewrite(func, inst);
89 }
90}
91
92/// Whether this instruction is one of the ones with no instruction behind it.
93///
94/// The three the machine has are left as they are, and so is anything whose value is not an integer
95/// at a width the machine compares and exchanges at, which is what the two floating operations are.
96fn wanted(func: &Func, inst: Inst) -> bool {
97 let Extra::Rmw(op, _) = func[inst].extra else { return false };
98 if matches!(op, RmwOp::Xchg | RmwOp::Add | RmwOp::Sub) {
99 return false;
100 }
101 let Some(old) = func[inst].first_result else { return false };
102 let ty = func[old].ty;
103 ty.is_int() && matches!(ty.bits(), 8 | 16 | 32 | 64)
104}
105
106/// One read modify write, as the three blocks the loop is.
107fn rewrite(func: &mut Func, inst: Inst) {
108 let head = func.block_of(inst).expect("the instruction is in a block");
109 let span = func.span(inst);
110 let Extra::Rmw(op, mem) = func[inst].extra else { return };
111 let info = func[mem];
112 let flags = func[inst].flags;
113 let [addr, operand] = func[func[inst].args] else { return };
114 let Some(old) = func[inst].first_result else { return };
115 let ty = func[old].ty;
116
117 // Everything after the instruction, which is what moves into the block the loop leaves to.
118 // Collected before anything is taken out of the block, because taking one out is what the list
119 // is walked in order to do.
120 let tail: Vec<Inst> = func.insts(head).skip_while(|&at| at != inst).skip(1).collect();
121
122 let spin = func.create_block();
123 let done = func.create_block();
124 let seen = func.append_param(spin, ty);
125 let before = func.append_param(done, ty);
126
127 func.remove_inst(inst);
128 for at in tail {
129 func.remove_inst(at);
130 func.append_inst(done, at);
131 }
132
133 // Relaxed, because what makes the whole of this indivisible is the compare and exchange and a
134 // stronger load in front of it would be a barrier bought twice. The read is not the moment the
135 // operation happens; the exchange that agrees with it is.
136 let mut build = Builder::new(func, head).at(span);
137 let first = build.atomic_load(ty, addr, MemInfo { order: MemOrder::Relaxed, ..info }, flags);
138 build.jump(spin, &[first]);
139
140 let mut build = Builder::new(func, spin).at(span);
141 let want = compute(&mut build, op, seen, operand, ty);
142 let (got, ok) = build.cmpxchg(addr, seen, want, info, flags);
143 build.br_if(ok, done, &[seen], spin, &[got]);
144
145 // Last, so that nothing written above is rewritten by it. None of it reads `old` anyway, but the
146 // arguments the branch above carries are values this walk looks at, and a substitution that is
147 // right only because of what happens not to be in a list is one waiting to be wrong.
148 replace(func, old, before);
149}
150
151/// What the loop puts back, which is the operation over what it read and the operand.
152///
153/// Six shapes for eight operations. Four are one instruction. A nand is the and and then every bit
154/// of the answer flipped, which is an exclusive or against every bit set because the IR has no not
155/// and that is what one is. The four that take a maximum or a minimum are a comparison and a select,
156/// and which comparison is the whole of the difference between the signed pair and the unsigned one.
157fn compute(build: &mut Builder<'_>, op: RmwOp, seen: Value, operand: Value, ty: Type) -> Value {
158 let opcode = match op {
159 RmwOp::And | RmwOp::Nand => Opcode::And,
160 RmwOp::Or => Opcode::Or,
161 RmwOp::Xor => Opcode::Xor,
162 RmwOp::SMax => return pick(build, IntPred::Sgt, seen, operand),
163 RmwOp::SMin => return pick(build, IntPred::Slt, seen, operand),
164 RmwOp::UMax => return pick(build, IntPred::Ugt, seen, operand),
165 RmwOp::UMin => return pick(build, IntPred::Ult, seen, operand),
166 _ => unreachable!("the operations with an instruction never reach this pass"),
167 };
168 let answer = build.binary(opcode, seen, operand, Flags::NONE);
169 if op != RmwOp::Nand {
170 return answer;
171 }
172 let ones = build.iconst(ty, -1);
173 build.binary(Opcode::Xor, answer, ones, Flags::NONE)
174}
175
176/// Whichever of the two the comparison prefers, as the comparison and a select over it.
177///
178/// The value read comes first in the comparison and is what the select takes when it holds, so a
179/// maximum of two equal values answers the one that was there. That is not observable here, since
180/// the two are equal, and it is the way round every other compiler writes it.
181fn pick(build: &mut Builder<'_>, pred: IntPred, seen: Value, operand: Value) -> Value {
182 let wins = build.icmp(pred, seen, operand);
183 build.select(wins, seen, operand)
184}
185
186/// Every use of one value made a use of another, across the whole function.
187///
188/// Two places hold a use: the operand list of an instruction, and the argument list of a branch's
189/// edge. Both are runs of values in the same pool, so both are the same rewrite, and a walk that
190/// covers the two of them covers every use there is.
191///
192/// The whole function rather than the part below the loop, because a use above it cannot exist. The
193/// value being replaced is the result of an instruction that stood where the loop now stands, and
194/// nothing that runs before an instruction reads what it produced.
195fn replace(func: &mut Func, from: Value, to: Value) {
196 let blocks: Vec<Block> = func.blocks().collect();
197 for block in blocks {
198 let insts: Vec<Inst> = func.insts(block).collect();
199 for inst in insts {
200 let mut lists = vec![func[inst].args];
201 lists.extend(func.successors(inst).map(|call| call.args));
202 for list in lists {
203 func.rewrite(list, |value| if value == from { to } else { value });
204 }
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use rucc_base::Interner;
212 use rucc_ir::{
213 Builder, Float, Func, MemInfo, MemOrder, Module, Opcode, Restrict, Signature, Type,
214 };
215 use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
216
217 use super::{Flags, Inst, RmwOp, Value, loops};
218
219 fn target() -> TargetInfo {
220 TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
221 }
222
223 fn info(bytes: u64) -> MemInfo {
224 MemInfo {
225 size: bytes,
226 align: u32::try_from(bytes).expect("a small width"),
227 order: MemOrder::SeqCst,
228 tbaa: None,
229 owns: 0,
230 restrict: Restrict::NONE,
231 }
232 }
233
234 /// `ty rmw(ty *p, ty v) { return __atomic_fetch_<op>(p, v, 5); }` as the front end builds it,
235 /// which is one block with the read modify write in the middle of it.
236 fn built(op: RmwOp, ty: Type) -> (Interner, Func) {
237 let mut names = Interner::new();
238 let signature = Signature::new().with_params(&[Type::PTR, ty]).with_returns(&[ty]);
239 let mut func = Func::new(names.intern("rmw"), signature);
240 let entry = func.create_block();
241 let addr = func.append_param(entry, Type::PTR);
242 let operand = func.append_param(entry, ty);
243
244 let bytes = u64::from(ty.bits() / 8);
245 let mut build = Builder::new(&mut func, entry);
246 let old = build.atomic_rmw(op, addr, operand, info(bytes), Flags::NONE);
247 build.ret(&[old]);
248 (names, func)
249 }
250
251 fn printed(func: &Func, names: &mut Interner) -> String {
252 let module = Module::new(names.intern("rmw.c"), &target());
253 rucc_ir::print_func(&module, func, names)
254 }
255
256 fn verified(func: &Func, names: &mut Interner) {
257 let module = Module::new(names.intern("rmw.c"), &target());
258 rucc_ir::verify_func(&module, func, names).expect("the rewrite builds valid IR");
259 }
260
261 fn opcodes(func: &Func) -> Vec<Opcode> {
262 func.blocks().flat_map(|block| func.insts(block).map(|inst| func[inst].opcode)).collect()
263 }
264
265 fn only(func: &Func, opcode: Opcode) -> Inst {
266 let found: Vec<Inst> = func
267 .blocks()
268 .flat_map(|block| func.insts(block).collect::<Vec<_>>())
269 .filter(|&inst| func[inst].opcode == opcode)
270 .collect();
271 assert_eq!(found.len(), 1, "expected one {opcode:?}");
272 found[0]
273 }
274
275 /// The four the machine has no instruction for become three blocks and a compare and exchange.
276 ///
277 /// One block for what was in front of it, one for the loop and one for what came after, and the
278 /// read modify write itself is gone. The IR is checked rather than the shape being believed,
279 /// because a loop built by hand is exactly the thing that gets the block arguments wrong.
280 #[test]
281 fn an_operation_with_no_instruction_becomes_a_loop() {
282 for op in [RmwOp::And, RmwOp::Nand, RmwOp::Or, RmwOp::Xor] {
283 let (mut names, mut func) = built(op, Type::int(32));
284 loops(&mut func);
285 verified(&func, &mut names);
286
287 let text = printed(&func, &mut names);
288 assert_eq!(func.blocks().count(), 3, "{op:?}: {text}");
289 let kinds = opcodes(&func);
290 assert!(kinds.contains(&Opcode::Cmpxchg), "{op:?}: {text}");
291 assert!(kinds.contains(&Opcode::AtomicLoad), "{op:?}: {text}");
292 assert!(!kinds.contains(&Opcode::AtomicRmw), "{op:?}: {text}");
293 }
294 }
295
296 /// The value the loop answers is what was there before, which is what the instruction answered.
297 ///
298 /// It is the expected operand of the compare and exchange that goes to the block the loop leaves
299 /// to, and not what the exchange found or what was put there. Getting that wrong is the way this
300 /// shape is usually wrong, and it is invisible in the assembly until two threads run.
301 #[test]
302 fn the_loop_answers_the_value_that_was_there_before() {
303 let (mut names, mut func) = built(RmwOp::Or, Type::int(32));
304 loops(&mut func);
305
306 let exchange = only(&func, Opcode::Cmpxchg);
307 let expected = func[func[exchange].args][1];
308 let branch = only(&func, Opcode::BrIf);
309 let taken = func.successors(branch).next().expect("a branch has a first edge");
310 assert_eq!(func[taken.args], [expected], "{}", printed(&func, &mut names));
311
312 // And the edge back round carries what the exchange found, since a second read would be a
313 // second chance to be wrong.
314 let found = func[exchange].first_result.expect("the exchange answers what it found");
315 let again = func.successors(branch).nth(1).expect("a branch has a second edge");
316 assert_eq!(func[again.args], [found], "{}", printed(&func, &mut names));
317 }
318
319 /// A use of the value below the loop reads the parameter of the block the loop leaves to.
320 ///
321 /// The `return` was in the block the instruction was in, so it moved, and what it returns is no
322 /// longer a value anything defines. This is the substitution that makes the rewrite correct
323 /// rather than merely well shaped.
324 #[test]
325 fn a_use_below_the_loop_reads_the_block_parameter() {
326 let (mut names, mut func) = built(RmwOp::Xor, Type::int(32));
327 loops(&mut func);
328
329 let ret = only(&func, Opcode::Return);
330 let block = func.block_of(ret).expect("the return is in a block");
331 let returned: Vec<Value> = func[func[ret].args].to_vec();
332 assert_eq!(returned, func[block].params, "{}", printed(&func, &mut names));
333 }
334
335 /// The four operations that take a maximum or a minimum are a comparison and a select.
336 ///
337 /// No builtin in either family writes one of these yet, so the only way to reach them is to
338 /// build the instruction here. The pass covers them because the opcode does, and the two pairs
339 /// differ only in whether the comparison is signed.
340 #[test]
341 fn a_maximum_or_a_minimum_is_a_compare_and_a_select() {
342 for op in [RmwOp::SMax, RmwOp::SMin, RmwOp::UMax, RmwOp::UMin] {
343 let (mut names, mut func) = built(op, Type::int(64));
344 loops(&mut func);
345 verified(&func, &mut names);
346
347 let kinds = opcodes(&func);
348 assert!(kinds.contains(&Opcode::ICmp), "{op:?}");
349 assert!(kinds.contains(&Opcode::Select), "{op:?}");
350 assert!(kinds.contains(&Opcode::Cmpxchg), "{op:?}");
351 }
352 }
353
354 /// The three with an instruction are left exactly as they were, and so are the two on floats.
355 ///
356 /// A loop for an exchange or an add would be slower and larger for no reason, and a loop for a
357 /// float would need the value carried through an integer of the same width, which an eighty bit
358 /// float has none of. Both are left for `crate::lower` to answer, one by lowering it and one by
359 /// refusing it.
360 #[test]
361 fn what_has_an_instruction_and_what_has_no_width_are_left_alone() {
362 for op in [RmwOp::Xchg, RmwOp::Add, RmwOp::Sub] {
363 let (_, mut func) = built(op, Type::int(32));
364 loops(&mut func);
365 assert_eq!(func.blocks().count(), 1, "{op:?} has an instruction");
366 assert!(opcodes(&func).contains(&Opcode::AtomicRmw), "{op:?}");
367 }
368 for op in [RmwOp::FAdd, RmwOp::FSub] {
369 let (_, mut func) = built(op, Type::float(Float::F64));
370 loops(&mut func);
371 assert_eq!(func.blocks().count(), 1, "{op:?} has no width to carry it");
372 assert!(opcodes(&func).contains(&Opcode::AtomicRmw), "{op:?}");
373 }
374 }
375}