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 restrict: Restrict::NONE,
230 }
231 }
232
233 /// `ty rmw(ty *p, ty v) { return __atomic_fetch_<op>(p, v, 5); }` as the front end builds it,
234 /// which is one block with the read modify write in the middle of it.
235 fn built(op: RmwOp, ty: Type) -> (Interner, Func) {
236 let mut names = Interner::new();
237 let signature = Signature::new().with_params(&[Type::PTR, ty]).with_returns(&[ty]);
238 let mut func = Func::new(names.intern("rmw"), signature);
239 let entry = func.create_block();
240 let addr = func.append_param(entry, Type::PTR);
241 let operand = func.append_param(entry, ty);
242
243 let bytes = u64::from(ty.bits() / 8);
244 let mut build = Builder::new(&mut func, entry);
245 let old = build.atomic_rmw(op, addr, operand, info(bytes), Flags::NONE);
246 build.ret(&[old]);
247 (names, func)
248 }
249
250 fn printed(func: &Func, names: &mut Interner) -> String {
251 let module = Module::new(names.intern("rmw.c"), &target());
252 rucc_ir::print_func(&module, func, names)
253 }
254
255 fn verified(func: &Func, names: &mut Interner) {
256 let module = Module::new(names.intern("rmw.c"), &target());
257 rucc_ir::verify_func(&module, func, names).expect("the rewrite builds valid IR");
258 }
259
260 fn opcodes(func: &Func) -> Vec<Opcode> {
261 func.blocks().flat_map(|block| func.insts(block).map(|inst| func[inst].opcode)).collect()
262 }
263
264 fn only(func: &Func, opcode: Opcode) -> Inst {
265 let found: Vec<Inst> = func
266 .blocks()
267 .flat_map(|block| func.insts(block).collect::<Vec<_>>())
268 .filter(|&inst| func[inst].opcode == opcode)
269 .collect();
270 assert_eq!(found.len(), 1, "expected one {opcode:?}");
271 found[0]
272 }
273
274 /// The four the machine has no instruction for become three blocks and a compare and exchange.
275 ///
276 /// One block for what was in front of it, one for the loop and one for what came after, and the
277 /// read modify write itself is gone. The IR is checked rather than the shape being believed,
278 /// because a loop built by hand is exactly the thing that gets the block arguments wrong.
279 #[test]
280 fn an_operation_with_no_instruction_becomes_a_loop() {
281 for op in [RmwOp::And, RmwOp::Nand, RmwOp::Or, RmwOp::Xor] {
282 let (mut names, mut func) = built(op, Type::int(32));
283 loops(&mut func);
284 verified(&func, &mut names);
285
286 let text = printed(&func, &mut names);
287 assert_eq!(func.blocks().count(), 3, "{op:?}: {text}");
288 let kinds = opcodes(&func);
289 assert!(kinds.contains(&Opcode::Cmpxchg), "{op:?}: {text}");
290 assert!(kinds.contains(&Opcode::AtomicLoad), "{op:?}: {text}");
291 assert!(!kinds.contains(&Opcode::AtomicRmw), "{op:?}: {text}");
292 }
293 }
294
295 /// The value the loop answers is what was there before, which is what the instruction answered.
296 ///
297 /// It is the expected operand of the compare and exchange that goes to the block the loop leaves
298 /// to, and not what the exchange found or what was put there. Getting that wrong is the way this
299 /// shape is usually wrong, and it is invisible in the assembly until two threads run.
300 #[test]
301 fn the_loop_answers_the_value_that_was_there_before() {
302 let (mut names, mut func) = built(RmwOp::Or, Type::int(32));
303 loops(&mut func);
304
305 let exchange = only(&func, Opcode::Cmpxchg);
306 let expected = func[func[exchange].args][1];
307 let branch = only(&func, Opcode::BrIf);
308 let taken = func.successors(branch).next().expect("a branch has a first edge");
309 assert_eq!(func[taken.args], [expected], "{}", printed(&func, &mut names));
310
311 // And the edge back round carries what the exchange found, since a second read would be a
312 // second chance to be wrong.
313 let found = func[exchange].first_result.expect("the exchange answers what it found");
314 let again = func.successors(branch).nth(1).expect("a branch has a second edge");
315 assert_eq!(func[again.args], [found], "{}", printed(&func, &mut names));
316 }
317
318 /// A use of the value below the loop reads the parameter of the block the loop leaves to.
319 ///
320 /// The `return` was in the block the instruction was in, so it moved, and what it returns is no
321 /// longer a value anything defines. This is the substitution that makes the rewrite correct
322 /// rather than merely well shaped.
323 #[test]
324 fn a_use_below_the_loop_reads_the_block_parameter() {
325 let (mut names, mut func) = built(RmwOp::Xor, Type::int(32));
326 loops(&mut func);
327
328 let ret = only(&func, Opcode::Return);
329 let block = func.block_of(ret).expect("the return is in a block");
330 let returned: Vec<Value> = func[func[ret].args].to_vec();
331 assert_eq!(returned, func[block].params, "{}", printed(&func, &mut names));
332 }
333
334 /// The four operations that take a maximum or a minimum are a comparison and a select.
335 ///
336 /// No builtin in either family writes one of these yet, so the only way to reach them is to
337 /// build the instruction here. The pass covers them because the opcode does, and the two pairs
338 /// differ only in whether the comparison is signed.
339 #[test]
340 fn a_maximum_or_a_minimum_is_a_compare_and_a_select() {
341 for op in [RmwOp::SMax, RmwOp::SMin, RmwOp::UMax, RmwOp::UMin] {
342 let (mut names, mut func) = built(op, Type::int(64));
343 loops(&mut func);
344 verified(&func, &mut names);
345
346 let kinds = opcodes(&func);
347 assert!(kinds.contains(&Opcode::ICmp), "{op:?}");
348 assert!(kinds.contains(&Opcode::Select), "{op:?}");
349 assert!(kinds.contains(&Opcode::Cmpxchg), "{op:?}");
350 }
351 }
352
353 /// The three with an instruction are left exactly as they were, and so are the two on floats.
354 ///
355 /// A loop for an exchange or an add would be slower and larger for no reason, and a loop for a
356 /// float would need the value carried through an integer of the same width, which an eighty bit
357 /// float has none of. Both are left for `crate::lower` to answer, one by lowering it and one by
358 /// refusing it.
359 #[test]
360 fn what_has_an_instruction_and_what_has_no_width_are_left_alone() {
361 for op in [RmwOp::Xchg, RmwOp::Add, RmwOp::Sub] {
362 let (_, mut func) = built(op, Type::int(32));
363 loops(&mut func);
364 assert_eq!(func.blocks().count(), 1, "{op:?} has an instruction");
365 assert!(opcodes(&func).contains(&Opcode::AtomicRmw), "{op:?}");
366 }
367 for op in [RmwOp::FAdd, RmwOp::FSub] {
368 let (_, mut func) = built(op, Type::float(Float::F64));
369 loops(&mut func);
370 assert_eq!(func.blocks().count(), 1, "{op:?} has no width to carry it");
371 assert!(opcodes(&func).contains(&Opcode::AtomicRmw), "{op:?}");
372 }
373 }
374}