Coverage Report

Created: 2026-10-24 07:23

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
341k
    fn merge_bundle_properties(&mut self, from: LiveBundleIndex, to: LiveBundleIndex) {
25
341k
        if self.bundles[from].cached_fixed() {
26
25.5k
            self.bundles[to].set_cached_fixed();
27
315k
        }
28
341k
        if self.bundles[from].cached_fixed_def() {
29
2.99k
            self.bundles[to].set_cached_fixed_def();
30
338k
        }
31
341k
        if self.bundles[from].cached_stack() {
32
0
            self.bundles[to].set_cached_stack();
33
341k
        }
34
341k
        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
341k
        }
40
341k
    }
41
42
915k
    pub fn merge_bundles(&mut self, from: LiveBundleIndex, to: LiveBundleIndex) -> bool {
43
915k
        if from == to {
44
            // Merge bundle into self -- trivial merge.
45
5.60k
            return true;
46
909k
        }
47
909k
        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
909k
        let from_rc = self.spillsets[self.bundles[from].spillset].class;
55
909k
        let to_rc = self.spillsets[self.bundles[to].spillset].class;
56
909k
        if from_rc != to_rc {
57
0
            trace!(" -> mismatching reg classes");
58
0
            return false;
59
909k
        }
60
61
        // If either bundle is already assigned (due to a pinned vreg), don't merge.
62
909k
        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
909k
        }
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
7.71M
        let adjust_range_start = |bundle_idx, range: CodeRange| {
88
7.71M
            if self.bundles[bundle_idx].cached_fixed_def() {
89
59.3k
                ProgPoint::before(range.from.inst())
90
            } else {
91
7.65M
                range.from
92
            }
93
7.71M
        };
94
95
        // Check for overlap in LiveRanges and for conflicting
96
        // requirements.
97
909k
        let ranges_from = &self.bundles[from].ranges[..];
98
909k
        let ranges_to = &self.bundles[to].ranges[..];
99
909k
        let mut idx_from = 0;
100
909k
        let mut idx_to = 0;
101
909k
        let mut range_count = 0;
102
4.36M
        while idx_from < ranges_from.len() && idx_to < ranges_to.len() {
103
4.00M
            range_count += 1;
104
4.00M
            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
4.00M
            }
112
113
4.00M
            if adjust_range_start(from, ranges_from[idx_from].range) >= ranges_to[idx_to].range.to {
114
300k
                idx_to += 1;
115
3.70M
            } else if adjust_range_start(to, ranges_to[idx_to].range)
116
3.70M
                >= ranges_from[idx_from].range.to
117
3.15M
            {
118
3.15M
                idx_from += 1;
119
3.15M
            } else {
120
                // Overlap -- cannot merge.
121
550k
                trace!(
122
                    " -> overlap between {:?} and {:?}, exiting",
123
0
                    ranges_from[idx_from].index,
124
0
                    ranges_to[idx_to].index
125
                );
126
550k
                return false;
127
            }
128
        }
129
130
        // Check for a requirements conflict.
131
359k
        if self.bundles[from].cached_stack()
132
359k
            || self.bundles[from].cached_fixed()
133
321k
            || self.bundles[from].limit.is_some()
134
321k
            || self.bundles[to].cached_stack()
135
321k
            || self.bundles[to].cached_fixed()
136
306k
            || self.bundles[to].limit.is_some()
137
        {
138
53.4k
            if self.merge_bundle_requirements(from, to).is_err() {
139
18.2k
                trace!(" -> conflicting requirements; aborting merge");
140
18.2k
                return false;
141
35.2k
            }
142
306k
        }
143
144
341k
        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
341k
        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
341k
        }
155
341k
        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
341k
        }
181
182
341k
        trace!(
183
            "merging: ranges_from = {:?} ranges_to = {:?}",
184
            ranges_from,
185
            ranges_to
186
        );
187
188
341k
        let empty_vec = LiveRangeList::new_in(self.ctx.bump());
189
341k
        let mut from_list = core::mem::replace(&mut self.bundles[from].ranges, empty_vec);
190
1.94M
        for entry in &from_list {
191
1.94M
            self.ranges[entry.index].bundle = to;
192
1.94M
        }
193
194
341k
        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
209k
            let single_entry = from_list.pop().unwrap();
202
209k
            let pos = self.bundles[to]
203
209k
                .ranges
204
209k
                .binary_search_by_key(&single_entry.range.from, |entry| entry.range.from)
205
209k
                .unwrap_or_else(|pos| pos);
206
209k
            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
132k
            self.bundles[to].ranges.extend_from_slice(&from_list[..]);
211
132k
            self.bundles[to]
212
132k
                .ranges
213
132k
                .sort_unstable_by_key(|entry| entry.range.from);
214
        }
215
216
341k
        if self.annotations_enabled {
217
33.8k
            trace!("merging: merged = {:?}", self.bundles[to].ranges);
218
33.8k
            let mut last_range = None;
219
195k
            for i in 0..self.bundles[to].ranges.len() {
220
195k
                let entry = self.bundles[to].ranges[i];
221
195k
                if last_range.is_some() {
222
161k
                    debug_assert!(last_range.unwrap() < entry.range);
223
33.8k
                }
224
195k
                last_range = Some(entry.range);
225
226
195k
                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
195k
                }
238
239
195k
                trace!(
240
                    " -> merged result for bundle{}: range{}",
241
0
                    to.index(),
242
0
                    entry.index.index(),
243
                );
244
            }
245
307k
        }
246
247
341k
        if self.bundles[from].spillset != self.bundles[to].spillset {
248
341k
            // Widen the range for the target spillset to include the one being merged in.
249
341k
            let from_range = self.spillsets[self.bundles[from].spillset].range;
250
341k
            let to_range = &mut self.ctx.spillsets[self.ctx.bundles[to].spillset].range;
251
341k
            *to_range = to_range.join(from_range);
252
341k
        }
253
254
341k
        self.merge_bundle_properties(from, to);
255
256
341k
        true
257
915k
    }
258
259
12.1k
    pub fn merge_vreg_bundles(&mut self) {
260
        // Create a bundle for every vreg, initially.
261
12.1k
        trace!("merge_vreg_bundles: creating vreg bundles");
262
10.9M
        for vreg in 0..self.vregs.len() {
263
10.9M
            let vreg = VRegIndex::new(vreg);
264
10.9M
            if self.vregs[vreg].ranges.is_empty() {
265
0
                continue;
266
10.9M
            }
267
268
10.9M
            let bundle = self.ctx.bundles.add(self.ctx.scratch_bump.clone());
269
10.9M
            let mut range = self.vregs[vreg].ranges.first().unwrap().range;
270
271
10.9M
            self.bundles[bundle].ranges = self.vregs[vreg].ranges.clone();
272
10.9M
            trace!("vreg v{} gets bundle{}", vreg.index(), bundle.index());
273
15.6M
            for entry in &self.ctx.bundles[bundle].ranges {
274
15.6M
                trace!(
275
                    " -> with LR range{}: {:?}",
276
0
                    entry.index.index(),
277
                    entry.range
278
                );
279
15.6M
                range = range.join(entry.range);
280
15.6M
                self.ctx.ranges[entry.index].bundle = bundle;
281
            }
282
283
10.9M
            let mut fixed = false;
284
10.9M
            let mut fixed_def = false;
285
10.9M
            let mut stack = false;
286
10.9M
            let mut limit: Option<u8> = None;
287
15.6M
            for entry in &self.bundles[bundle].ranges {
288
21.2M
                for u in &self.ranges[entry.index].uses {
289
                    use OperandConstraint::*;
290
21.2M
                    match u.operand.constraint() {
291
                        FixedReg(_) => {
292
383k
                            fixed = true;
293
383k
                            if u.operand.kind() == OperandKind::Def {
294
66.0k
                                fixed_def = true;
295
317k
                            }
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
20.8M
                            continue;
308
                        }
309
                    }
310
383k
                    if fixed && stack && fixed_def {
311
0
                        break;
312
383k
                    }
313
                }
314
            }
315
10.9M
            if fixed {
316
297k
                self.bundles[bundle].set_cached_fixed();
317
10.6M
            }
318
10.9M
            if fixed_def {
319
66.0k
                self.bundles[bundle].set_cached_fixed_def();
320
10.9M
            }
321
10.9M
            if stack {
322
0
                self.bundles[bundle].set_cached_stack();
323
10.9M
            }
324
10.9M
            self.bundles[bundle].limit = limit;
325
326
            // Create a spillslot for this bundle.
327
10.9M
            let reg = self.vreg(vreg);
328
10.9M
            let ssidx = self.spillsets.push(SpillSet {
329
10.9M
                slot: SpillSlotIndex::invalid(),
330
10.9M
                required: false,
331
10.9M
                class: reg.class(),
332
10.9M
                hint: PReg::invalid(),
333
10.9M
                spill_bundle: LiveBundleIndex::invalid(),
334
10.9M
                splits: 0,
335
10.9M
                range,
336
10.9M
            });
337
10.9M
            self.bundles[bundle].spillset = ssidx;
338
        }
339
340
6.05M
        for inst in 0..self.func.num_insts() {
341
6.05M
            let inst = Inst::new(inst);
342
343
            // Attempt to merge Reuse-constraint operand outputs with the
344
            // corresponding inputs.
345
21.3M
            for op in self.func.inst_operands(inst) {
346
21.3M
                if let OperandConstraint::Reuse(reuse_idx) = op.constraint() {
347
709k
                    let src_vreg = op.vreg();
348
709k
                    let dst_vreg = self.func.inst_operands(inst)[reuse_idx].vreg();
349
350
709k
                    trace!(
351
                        "trying to merge reused-input def: src {} to dst {}",
352
                        src_vreg,
353
                        dst_vreg
354
                    );
355
709k
                    let src_bundle = self.ranges[self.vregs[src_vreg].ranges[0].index].bundle;
356
709k
                    debug_assert!(src_bundle.is_valid());
357
709k
                    let dest_bundle = self.ranges[self.vregs[dst_vreg].ranges[0].index].bundle;
358
709k
                    debug_assert!(dest_bundle.is_valid());
359
709k
                    self.merge_bundles(/* from */ dest_bundle, /* to */ src_bundle);
360
20.6M
                }
361
            }
362
        }
363
364
        // Attempt to merge blockparams with their inputs.
365
206k
        for i in 0..self.blockparam_outs.len() {
366
            let BlockparamOut {
367
206k
                from_vreg, to_vreg, ..
368
206k
            } = self.blockparam_outs[i];
369
206k
            trace!(
370
                "trying to merge blockparam v{} with input v{}",
371
0
                to_vreg.index(),
372
0
                from_vreg.index()
373
            );
374
206k
            let to_bundle = self.ranges[self.vregs[to_vreg].ranges[0].index].bundle;
375
206k
            debug_assert!(to_bundle.is_valid());
376
206k
            let from_bundle = self.ranges[self.vregs[from_vreg].ranges[0].index].bundle;
377
206k
            debug_assert!(from_bundle.is_valid());
378
206k
            trace!(
379
                " -> from bundle{} to bundle{}",
380
0
                from_bundle.index(),
381
0
                to_bundle.index()
382
            );
383
206k
            self.merge_bundles(from_bundle, to_bundle);
384
        }
385
386
12.1k
        trace!("done merging bundles");
387
12.1k
    }
388
389
12.3M
    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
12.3M
        let mut total = 0;
393
20.2M
        for entry in &self.bundles[bundle].ranges {
394
20.2M
            total += entry.range.len() as u32;
395
20.2M
        }
396
12.3M
        trace!(" -> prio {total}");
397
12.3M
        total
398
12.3M
    }
399
400
12.3M
    pub fn compute_bundle_limit(&self, bundle: LiveBundleIndex) -> Option<u8> {
401
12.3M
        let mut limit: Option<u8> = None;
402
20.2M
        for entry in &self.bundles[bundle].ranges {
403
25.1M
            for u in &self.ranges[entry.index].uses {
404
                use OperandConstraint::*;
405
25.1M
                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
800k
                        break;
416
                    }
417
                    Any | Reg | Reuse(_) => {
418
24.3M
                        continue;
419
                    }
420
                }
421
            }
422
        }
423
12.3M
        limit
424
12.3M
    }
425
426
12.1k
    pub fn queue_bundles(&mut self) {
427
10.9M
        for bundle in 0..self.bundles.len() {
428
10.9M
            trace!("enqueueing bundle{}", bundle);
429
10.9M
            let bundle = LiveBundleIndex::new(bundle);
430
10.9M
            if self.bundles[bundle].ranges.is_empty() {
431
341k
                trace!(" -> no ranges; skipping");
432
341k
                continue;
433
10.6M
            }
434
10.6M
            self.recompute_bundle_properties(bundle);
435
10.6M
            let prio = self.bundles[bundle].prio as usize;
436
10.6M
            self.allocation_queue.insert(bundle, prio, PReg::invalid());
437
        }
438
12.1k
        self.output.stats.merged_bundle_count = self.allocation_queue.heap.len();
439
12.1k
    }
440
}