Coverage Report

Created: 2026-08-08 08:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/regalloc2/src/ion/merge.rs
Line
Count
Source
1
/*
2
 * This file was initially derived from the files
3
 * `js/src/jit/BacktrackingAllocator.h` and
4
 * `js/src/jit/BacktrackingAllocator.cpp` in Mozilla Firefox, and was
5
 * originally licensed under the Mozilla Public License 2.0. We
6
 * subsequently relicensed it to Apache-2.0 WITH LLVM-exception (see
7
 * https://github.com/bytecodealliance/regalloc2/issues/7).
8
 *
9
 * Since the initial port, the design has been substantially evolved
10
 * and optimized.
11
 */
12
13
//! Bundle merging.
14
15
use crate::ion::data_structures::{
16
    BlockparamOut, CodeRange, Env, LiveBundleIndex, LiveRangeList, SpillSet, SpillSlotIndex,
17
    VRegIndex,
18
};
19
use crate::{Function, Inst, OperandConstraint, OperandKind, PReg, ProgPoint};
20
use alloc::format;
21
use core::convert::TryFrom;
22
23
impl<'a, F: Function> Env<'a, F> {
24
0
    fn merge_bundle_properties(&mut self, from: LiveBundleIndex, to: LiveBundleIndex) {
25
0
        if self.bundles[from].cached_fixed() {
26
0
            self.bundles[to].set_cached_fixed();
27
0
        }
28
0
        if self.bundles[from].cached_fixed_def() {
29
0
            self.bundles[to].set_cached_fixed_def();
30
0
        }
31
0
        if self.bundles[from].cached_stack() {
32
0
            self.bundles[to].set_cached_stack();
33
0
        }
34
0
        if let Some(theirs) = self.bundles[from].limit {
35
0
            match self.bundles[to].limit {
36
0
                Some(ours) => self.bundles[to].limit = Some(ours.min(theirs)),
37
0
                None => self.bundles[to].limit = Some(theirs),
38
            }
39
0
        }
40
0
    }
41
42
0
    pub fn merge_bundles(&mut self, from: LiveBundleIndex, to: LiveBundleIndex) -> bool {
43
0
        if from == to {
44
            // Merge bundle into self -- trivial merge.
45
0
            return true;
46
0
        }
47
0
        trace!(
48
            "merging from bundle{} to bundle{}",
49
0
            from.index(),
50
0
            to.index()
51
        );
52
53
        // Both bundles must deal with the same RegClass.
54
0
        let from_rc = self.spillsets[self.bundles[from].spillset].class;
55
0
        let to_rc = self.spillsets[self.bundles[to].spillset].class;
56
0
        if from_rc != to_rc {
57
0
            trace!(" -> mismatching reg classes");
58
0
            return false;
59
0
        }
60
61
        // If either bundle is already assigned (due to a pinned vreg), don't merge.
62
0
        if self.bundles[from].allocation.is_some() || self.bundles[to].allocation.is_some() {
63
0
            trace!("one of the bundles is already assigned (pinned)");
64
0
            return false;
65
0
        }
66
67
        #[cfg(debug_assertions)]
68
        {
69
            // Sanity check: both bundles should contain only ranges with appropriate VReg classes.
70
            for entry in &self.bundles[from].ranges {
71
                let vreg = self.ranges[entry.index].vreg;
72
                debug_assert_eq!(from_rc, self.vreg(vreg).class());
73
            }
74
            for entry in &self.bundles[to].ranges {
75
                let vreg = self.ranges[entry.index].vreg;
76
                debug_assert_eq!(to_rc, self.vreg(vreg).class());
77
            }
78
        }
79
80
        // If a bundle has a fixed-reg def then we need to be careful to not
81
        // extend the bundle to include another use in the same instruction.
82
        // This could result in a minimal bundle that is impossible to split.
83
        //
84
        // This can only happen with an early use and a late def, so we round
85
        // the start of each range containing a fixed def up to the start of
86
        // its instruction to detect overlaps.
87
0
        let adjust_range_start = |bundle_idx, range: CodeRange| {
88
0
            if self.bundles[bundle_idx].cached_fixed_def() {
89
0
                ProgPoint::before(range.from.inst())
90
            } else {
91
0
                range.from
92
            }
93
0
        };
94
95
        // Check for overlap in LiveRanges and for conflicting
96
        // requirements.
97
0
        let ranges_from = &self.bundles[from].ranges[..];
98
0
        let ranges_to = &self.bundles[to].ranges[..];
99
0
        let mut idx_from = 0;
100
0
        let mut idx_to = 0;
101
0
        let mut range_count = 0;
102
0
        while idx_from < ranges_from.len() && idx_to < ranges_to.len() {
103
0
            range_count += 1;
104
0
            if range_count > 200 {
105
0
                trace!(
106
                    "reached merge complexity (range_count = {}); exiting",
107
                    range_count
108
                );
109
                // Limit merge complexity.
110
0
                return false;
111
0
            }
112
113
0
            if adjust_range_start(from, ranges_from[idx_from].range) >= ranges_to[idx_to].range.to {
114
0
                idx_to += 1;
115
0
            } else if adjust_range_start(to, ranges_to[idx_to].range)
116
0
                >= ranges_from[idx_from].range.to
117
0
            {
118
0
                idx_from += 1;
119
0
            } else {
120
                // Overlap -- cannot merge.
121
0
                trace!(
122
                    " -> overlap between {:?} and {:?}, exiting",
123
0
                    ranges_from[idx_from].index,
124
0
                    ranges_to[idx_to].index
125
                );
126
0
                return false;
127
            }
128
        }
129
130
        // Check for a requirements conflict.
131
0
        if self.bundles[from].cached_stack()
132
0
            || self.bundles[from].cached_fixed()
133
0
            || self.bundles[from].limit.is_some()
134
0
            || self.bundles[to].cached_stack()
135
0
            || self.bundles[to].cached_fixed()
136
0
            || self.bundles[to].limit.is_some()
137
        {
138
0
            if self.merge_bundle_requirements(from, to).is_err() {
139
0
                trace!(" -> conflicting requirements; aborting merge");
140
0
                return false;
141
0
            }
142
0
        }
143
144
0
        trace!(" -> committing to merge");
145
146
        // If we reach here, then the bundles do not overlap -- merge
147
        // them!  We do this with a merge-sort-like scan over both
148
        // lists, building a new range list and replacing the list on
149
        // `to` when we're done.
150
0
        if ranges_from.is_empty() {
151
            // `from` bundle is empty -- trivial merge.
152
0
            trace!(" -> from bundle{} is empty; trivial merge", from.index());
153
0
            return true;
154
0
        }
155
0
        if ranges_to.is_empty() {
156
            // `to` bundle is empty -- just move the list over from
157
            // `from` and set `bundle` up-link on all ranges.
158
0
            trace!(" -> to bundle{} is empty; trivial merge", to.index());
159
0
            let empty_vec = LiveRangeList::new_in(self.ctx.bump());
160
0
            let list = core::mem::replace(&mut self.bundles[from].ranges, empty_vec);
161
0
            for entry in &list {
162
0
                self.ranges[entry.index].bundle = to;
163
164
0
                if self.annotations_enabled {
165
0
                    self.annotate(
166
0
                        entry.range.from,
167
0
                        format!(
168
0
                            " MERGE range{} v{} from bundle{} to bundle{}",
169
0
                            entry.index.index(),
170
0
                            self.ranges[entry.index].vreg.index(),
171
0
                            from.index(),
172
0
                            to.index(),
173
0
                        ),
174
0
                    );
175
0
                }
176
            }
177
0
            self.bundles[to].ranges = list;
178
0
            self.merge_bundle_properties(from, to);
179
0
            return true;
180
0
        }
181
182
0
        trace!(
183
            "merging: ranges_from = {:?} ranges_to = {:?}",
184
            ranges_from,
185
            ranges_to
186
        );
187
188
0
        let empty_vec = LiveRangeList::new_in(self.ctx.bump());
189
0
        let mut from_list = core::mem::replace(&mut self.bundles[from].ranges, empty_vec);
190
0
        for entry in &from_list {
191
0
            self.ranges[entry.index].bundle = to;
192
0
        }
193
194
0
        if from_list.len() == 1 {
195
            // Optimize for the common case where `from_list` contains a single
196
            // item. Using a binary search to find the insertion point and then
197
            // calling `insert` is more efficient than re-sorting the entire
198
            // list, specially after the changes in sorting algorithms introduced
199
            // in rustc 1.81.
200
            // See: https://github.com/bytecodealliance/regalloc2/issues/203
201
0
            let single_entry = from_list.pop().unwrap();
202
0
            let pos = self.bundles[to]
203
0
                .ranges
204
0
                .binary_search_by_key(&single_entry.range.from, |entry| entry.range.from)
205
0
                .unwrap_or_else(|pos| pos);
206
0
            self.bundles[to].ranges.insert(pos, single_entry);
207
        } else {
208
            // Two non-empty lists of LiveRanges: concatenate and sort. This is
209
            // faster than a mergesort-like merge into a new list, empirically.
210
0
            self.bundles[to].ranges.extend_from_slice(&from_list[..]);
211
0
            self.bundles[to]
212
0
                .ranges
213
0
                .sort_unstable_by_key(|entry| entry.range.from);
214
        }
215
216
0
        if self.annotations_enabled {
217
0
            trace!("merging: merged = {:?}", self.bundles[to].ranges);
218
0
            let mut last_range = None;
219
0
            for i in 0..self.bundles[to].ranges.len() {
220
0
                let entry = self.bundles[to].ranges[i];
221
0
                if last_range.is_some() {
222
0
                    debug_assert!(last_range.unwrap() < entry.range);
223
0
                }
224
0
                last_range = Some(entry.range);
225
226
0
                if self.ranges[entry.index].bundle == from {
227
0
                    self.annotate(
228
0
                        entry.range.from,
229
0
                        format!(
230
0
                            " MERGE range{} v{} from bundle{} to bundle{}",
231
0
                            entry.index.index(),
232
0
                            self.ranges[entry.index].vreg.index(),
233
0
                            from.index(),
234
0
                            to.index(),
235
0
                        ),
236
0
                    );
237
0
                }
238
239
0
                trace!(
240
                    " -> merged result for bundle{}: range{}",
241
0
                    to.index(),
242
0
                    entry.index.index(),
243
                );
244
            }
245
0
        }
246
247
0
        if self.bundles[from].spillset != self.bundles[to].spillset {
248
0
            // Widen the range for the target spillset to include the one being merged in.
249
0
            let from_range = self.spillsets[self.bundles[from].spillset].range;
250
0
            let to_range = &mut self.ctx.spillsets[self.ctx.bundles[to].spillset].range;
251
0
            *to_range = to_range.join(from_range);
252
0
        }
253
254
0
        self.merge_bundle_properties(from, to);
255
256
0
        true
257
0
    }
258
259
0
    pub fn merge_vreg_bundles(&mut self) {
260
        // Create a bundle for every vreg, initially.
261
0
        trace!("merge_vreg_bundles: creating vreg bundles");
262
0
        for vreg in 0..self.vregs.len() {
263
0
            let vreg = VRegIndex::new(vreg);
264
0
            if self.vregs[vreg].ranges.is_empty() {
265
0
                continue;
266
0
            }
267
268
0
            let bundle = self.ctx.bundles.add(self.ctx.scratch_bump.clone());
269
0
            let mut range = self.vregs[vreg].ranges.first().unwrap().range;
270
271
0
            self.bundles[bundle].ranges = self.vregs[vreg].ranges.clone();
272
0
            trace!("vreg v{} gets bundle{}", vreg.index(), bundle.index());
273
0
            for entry in &self.ctx.bundles[bundle].ranges {
274
0
                trace!(
275
                    " -> with LR range{}: {:?}",
276
0
                    entry.index.index(),
277
                    entry.range
278
                );
279
0
                range = range.join(entry.range);
280
0
                self.ctx.ranges[entry.index].bundle = bundle;
281
            }
282
283
0
            let mut fixed = false;
284
0
            let mut fixed_def = false;
285
0
            let mut stack = false;
286
0
            let mut limit: Option<u8> = None;
287
0
            for entry in &self.bundles[bundle].ranges {
288
0
                for u in &self.ranges[entry.index].uses {
289
                    use OperandConstraint::*;
290
0
                    match u.operand.constraint() {
291
                        FixedReg(_) => {
292
0
                            fixed = true;
293
0
                            if u.operand.kind() == OperandKind::Def {
294
0
                                fixed_def = true;
295
0
                            }
296
                        }
297
0
                        Stack => stack = true,
298
0
                        Limit(current) => {
299
0
                            let current = u8::try_from(current)
300
0
                                .expect("the current limit is too large to fit in a u8");
301
0
                            match limit {
302
0
                                Some(prev) => limit = Some(prev.min(current)),
303
0
                                None => limit = Some(current),
304
                            }
305
                        }
306
                        Any | Reg | Reuse(_) => {
307
0
                            continue;
308
                        }
309
                    }
310
0
                    if fixed && stack && fixed_def {
311
0
                        break;
312
0
                    }
313
                }
314
            }
315
0
            if fixed {
316
0
                self.bundles[bundle].set_cached_fixed();
317
0
            }
318
0
            if fixed_def {
319
0
                self.bundles[bundle].set_cached_fixed_def();
320
0
            }
321
0
            if stack {
322
0
                self.bundles[bundle].set_cached_stack();
323
0
            }
324
0
            self.bundles[bundle].limit = limit;
325
326
            // Create a spillslot for this bundle.
327
0
            let reg = self.vreg(vreg);
328
0
            let ssidx = self.spillsets.push(SpillSet {
329
0
                slot: SpillSlotIndex::invalid(),
330
0
                required: false,
331
0
                class: reg.class(),
332
0
                hint: PReg::invalid(),
333
0
                spill_bundle: LiveBundleIndex::invalid(),
334
0
                splits: 0,
335
0
                range,
336
0
            });
337
0
            self.bundles[bundle].spillset = ssidx;
338
        }
339
340
0
        for inst in 0..self.func.num_insts() {
341
0
            let inst = Inst::new(inst);
342
343
            // Attempt to merge Reuse-constraint operand outputs with the
344
            // corresponding inputs.
345
0
            for op in self.func.inst_operands(inst) {
346
0
                if let OperandConstraint::Reuse(reuse_idx) = op.constraint() {
347
0
                    let src_vreg = op.vreg();
348
0
                    let dst_vreg = self.func.inst_operands(inst)[reuse_idx].vreg();
349
350
0
                    trace!(
351
                        "trying to merge reused-input def: src {} to dst {}",
352
                        src_vreg,
353
                        dst_vreg
354
                    );
355
0
                    let src_bundle = self.ranges[self.vregs[src_vreg].ranges[0].index].bundle;
356
0
                    debug_assert!(src_bundle.is_valid());
357
0
                    let dest_bundle = self.ranges[self.vregs[dst_vreg].ranges[0].index].bundle;
358
0
                    debug_assert!(dest_bundle.is_valid());
359
0
                    self.merge_bundles(/* from */ dest_bundle, /* to */ src_bundle);
360
0
                }
361
            }
362
        }
363
364
        // Attempt to merge blockparams with their inputs.
365
0
        for i in 0..self.blockparam_outs.len() {
366
            let BlockparamOut {
367
0
                from_vreg, to_vreg, ..
368
0
            } = self.blockparam_outs[i];
369
0
            trace!(
370
                "trying to merge blockparam v{} with input v{}",
371
0
                to_vreg.index(),
372
0
                from_vreg.index()
373
            );
374
0
            let to_bundle = self.ranges[self.vregs[to_vreg].ranges[0].index].bundle;
375
0
            debug_assert!(to_bundle.is_valid());
376
0
            let from_bundle = self.ranges[self.vregs[from_vreg].ranges[0].index].bundle;
377
0
            debug_assert!(from_bundle.is_valid());
378
0
            trace!(
379
                " -> from bundle{} to bundle{}",
380
0
                from_bundle.index(),
381
0
                to_bundle.index()
382
            );
383
0
            self.merge_bundles(from_bundle, to_bundle);
384
        }
385
386
0
        trace!("done merging bundles");
387
0
    }
388
389
0
    pub fn compute_bundle_prio(&self, bundle: LiveBundleIndex) -> u32 {
390
        // The priority is simply the total "length" -- the number of
391
        // instructions covered by all LiveRanges.
392
0
        let mut total = 0;
393
0
        for entry in &self.bundles[bundle].ranges {
394
0
            total += entry.range.len() as u32;
395
0
        }
396
0
        trace!(" -> prio {total}");
397
0
        total
398
0
    }
399
400
0
    pub fn compute_bundle_limit(&self, bundle: LiveBundleIndex) -> Option<u8> {
401
0
        let mut limit: Option<u8> = None;
402
0
        for entry in &self.bundles[bundle].ranges {
403
0
            for u in &self.ranges[entry.index].uses {
404
                use OperandConstraint::*;
405
0
                match u.operand.constraint() {
406
0
                    Limit(current) => {
407
0
                        let current = u8::try_from(current)
408
0
                            .expect("the current limit is too large to fit in a u8");
409
0
                        match limit {
410
0
                            Some(prev) => limit = Some(prev.min(current)),
411
0
                            None => limit = Some(current),
412
                        }
413
                    }
414
                    FixedReg(_) | Stack => {
415
0
                        break;
416
                    }
417
                    Any | Reg | Reuse(_) => {
418
0
                        continue;
419
                    }
420
                }
421
            }
422
        }
423
0
        limit
424
0
    }
425
426
0
    pub fn queue_bundles(&mut self) {
427
0
        for bundle in 0..self.bundles.len() {
428
0
            trace!("enqueueing bundle{}", bundle);
429
0
            let bundle = LiveBundleIndex::new(bundle);
430
0
            if self.bundles[bundle].ranges.is_empty() {
431
0
                trace!(" -> no ranges; skipping");
432
0
                continue;
433
0
            }
434
0
            self.recompute_bundle_properties(bundle);
435
0
            let prio = self.bundles[bundle].prio as usize;
436
0
            self.allocation_queue.insert(bundle, prio, PReg::invalid());
437
        }
438
0
        self.output.stats.merged_bundle_count = self.allocation_queue.heap.len();
439
0
    }
440
}