Merge pull request #1 from cfallin/initial-regalloc

Initial implementation of regalloc2.
This commit is contained in:
Chris Fallin
2021-08-31 18:19:59 -07:00
committed by GitHub
43 changed files with 12133 additions and 0 deletions

0
.empty
View File

64
.github/workflows/rust.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
# Derived from regalloc.rs' GitHub CI config file.
name: Rust
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
# Lint code with rustfmt, report an error if it needs to be run.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install rustfmt
run: rustup component add rustfmt
- name: Run rustfmt and check there's no difference
run: cargo fmt --all -- --check
# Make sure the code compiles and that all the tests pass.
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build
- name: Run tests
run: cargo test --all --verbose
# Lint dependency graph for security advisories, duplicate versions, and
# incompatible licences.
cargo_deny:
name: Cargo deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- run: |
set -e
curl -L https://github.com/EmbarkStudios/cargo-deny/releases/download/0.8.5/cargo-deny-0.8.5-x86_64-unknown-linux-musl.tar.gz | tar xzf -
mv cargo-deny-*-x86_64-unknown-linux-musl/cargo-deny cargo-deny
echo `pwd` >> $GITHUB_PATH
- run: cargo deny check
# Builds the fuzz targets.
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install nightly
run: rustup toolchain install nightly
- name: Install cargo-fuzz
run: cargo +nightly install cargo-fuzz
- name: Build ssagen fuzzing target
run: cargo +nightly fuzz build ssagen
- name: Build moves fuzzing target
run: cargo +nightly fuzz build moves
- name: Build ion fuzzing target
run: cargo +nightly fuzz build ion
- name: Build and smoke-test ion_checker fuzzing target
run: cargo +nightly fuzz run ion_checker ./fuzz/smoketest/ion_checker.bin

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
Cargo.lock
target/
.*.swp
*~

24
Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "regalloc2"
version = "0.0.1"
authors = ["Chris Fallin <chris@cfallin.org>", "Mozilla SpiderMonkey Developers"]
edition = "2018"
license = "Apache-2.0 WITH LLVM-exception AND MPL-2.0"
description = "Backtracking register allocator inspired from IonMonkey"
repository = "https://github.com/bytecodealliance/regalloc2"
[dependencies]
log = { version = "0.4.8", default-features = false }
smallvec = "1.6.1"
fxhash = "0.2.1"
# The below are only needed for fuzzing.
# Keep this in sync with libfuzzer_sys's crate version:
arbitrary = { version = "^0.4.6", optional = true }
[profile.release]
debug = true
[features]
default = []
fuzzing = ["arbitrary"]

220
LICENSE Normal file
View File

@@ -0,0 +1,220 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
## regalloc2: another register allocator
This is a register allocator that started life as, and is about 50%
still, a port of IonMonkey's backtracking register allocator to
Rust. In many regards, it has been generalized, optimized, and
improved since the initial port, and now supports both SSA and non-SSA
use-cases.
In addition, it contains substantial amounts of testing infrastructure
(fuzzing harnesses and checkers) that does not exist in the original
IonMonkey allocator.
See the [design overview](doc/DESIGN.md) for (much!) more detail on
how the allocator works.
## License
Unless otherwise specified, code in this crate is licensed under the Apache 2.0
License with LLVM Exception. This license text can be found in the file
`LICENSE`.
Files in the `src/ion/` directory are directly ported from original C++ code in
IonMonkey, a part of the Firefox codebase. Parts of `src/lib.rs` are also
definitions that are directly translated from this original code. As a result,
these files are derivative works and are covered by the Mozilla Public License
(MPL) 2.0, as described in license headers in those files. Please see the
notices in relevant files for links to the original IonMonkey source files from
which they have been translated/derived. The MPL text can be found in
`src/ion/LICENSE`.
Parts of the code are derived from regalloc.rs: in particular,
`src/checker.rs` and `src/domtree.rs`. This crate has the same license
as regalloc.rs, so the license on these files does not differ.

1650
doc/DESIGN.md Normal file

File diff suppressed because it is too large Load Diff

34
doc/TODO Normal file
View File

@@ -0,0 +1,34 @@
# Features
- Large-input support (> 1M vregs, > 1M blocks)
- Two operand impls: u64-based and u32-based. Always accept
u64-based `Operand` publicly (do not expose this in interface).
- Trait to generalize over them and support both internally
(parameterize the whole allocator impl)
- On data-structure init, choose one or the other based on max vreg
index
- Update halfmove keys: u128 rather than u64
- Support allocation of register pairs (or overlapping registers generally)
- Rematerialization
- Stack-location constraints that place operands in user-defined stack
locations (distinct from SpillSlots) (e.g., stack args)
# Performance
- Investigate better register hinting
- Investigate more principled cost functions and split locations,
especially around loop nests
- Investigate ways to improve bundle-merging; e.g., merge moves before
other types of connections
- Add limited inter-block redundant-move elimination: propagate across
splits but not joins.
- Optimize allocations (some reports of 5-7% of time spent in allocator)
# Cleanup
- Remove support for non-SSA code once no longer necessary

3
fuzz/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
target
corpus
artifacts

51
fuzz/Cargo.toml Normal file
View File

@@ -0,0 +1,51 @@
[package]
name = "regalloc2-fuzz"
version = "0.0.0"
authors = ["Chris Fallin <chris@cfallin.org>"]
license = "MPL-2.0 AND Apache-2.0 WITH LLVM-exception"
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
regalloc2 = { path = "../", features = ["fuzzing"] }
libfuzzer-sys = "0.3"
arbitrary = { version = "^0.4.6", features = ["derive"] }
log = { version = "0.4.8", default-features = false }
env_logger = "0.8.3"
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "domtree"
path = "fuzz_targets/domtree.rs"
test = false
doc = false
[[bin]]
name = "ssagen"
path = "fuzz_targets/ssagen.rs"
test = false
doc = false
[[bin]]
name = "ion"
path = "fuzz_targets/ion.rs"
test = false
doc = false
[[bin]]
name = "moves"
path = "fuzz_targets/moves.rs"
test = false
doc = false
[[bin]]
name = "ion_checker"
path = "fuzz_targets/ion_checker.rs"
test = false
doc = false

View File

@@ -0,0 +1,133 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
#![no_main]
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use std::collections::HashSet;
use regalloc2::{
fuzzing::{domtree, postorder},
Block,
};
#[derive(Clone, Debug)]
struct CFG {
num_blocks: usize,
preds: Vec<Vec<Block>>,
succs: Vec<Vec<Block>>,
}
impl Arbitrary for CFG {
fn arbitrary(u: &mut Unstructured) -> Result<CFG> {
let num_blocks = u.int_in_range(1..=1000)?;
let mut succs = vec![];
for _ in 0..num_blocks {
let mut block_succs = vec![];
for _ in 0..u.int_in_range(0..=5)? {
block_succs.push(Block::new(u.int_in_range(0..=(num_blocks - 1))?));
}
succs.push(block_succs);
}
let mut preds = vec![];
for _ in 0..num_blocks {
preds.push(vec![]);
}
for from in 0..num_blocks {
for succ in &succs[from] {
preds[succ.index()].push(Block::new(from));
}
}
Ok(CFG {
num_blocks,
preds,
succs,
})
}
}
#[derive(Clone, Debug)]
struct Path {
blocks: Vec<Block>,
}
impl Path {
fn choose_from_cfg(cfg: &CFG, u: &mut Unstructured) -> Result<Path> {
let succs = u.int_in_range(0..=(2 * cfg.num_blocks))?;
let mut block = Block::new(0);
let mut blocks = vec![];
blocks.push(block);
for _ in 0..succs {
if cfg.succs[block.index()].is_empty() {
break;
}
block = *u.choose(&cfg.succs[block.index()])?;
blocks.push(block);
}
Ok(Path { blocks })
}
}
fn check_idom_violations(idom: &[Block], path: &Path) {
// "a dom b" means that any path from the entry block through the CFG that
// contains a and b will contain a before b.
//
// To test this, for any given block b_i, we have the set S of b_0 .. b_{i-1},
// and we walk up the domtree from b_i to get all blocks that dominate b_i;
// each such block must appear in S. (Otherwise, we have a counterexample
// for which dominance says it should appear in the path prefix, but it does
// not.)
let mut visited = HashSet::new();
visited.insert(Block::new(0));
for block in &path.blocks {
let mut parent = idom[block.index()];
let mut domset = HashSet::new();
domset.insert(*block);
while parent.is_valid() {
assert!(visited.contains(&parent));
domset.insert(parent);
let next = idom[parent.index()];
parent = next;
}
// Check that `dominates()` returns true for every block in domset,
// and false for every other block.
for domblock in 0..idom.len() {
let domblock = Block::new(domblock);
assert_eq!(
domset.contains(&domblock),
domtree::dominates(idom, domblock, *block)
);
}
visited.insert(*block);
}
}
#[derive(Clone, Debug)]
struct TestCase {
cfg: CFG,
path: Path,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<TestCase> {
let cfg = CFG::arbitrary(u)?;
let path = Path::choose_from_cfg(&cfg, u)?;
Ok(TestCase { cfg, path })
}
}
fuzz_target!(|testcase: TestCase| {
let postord = postorder::calculate(testcase.cfg.num_blocks, Block::new(0), |block| {
&testcase.cfg.succs[block.index()]
});
let idom = domtree::calculate(
testcase.cfg.num_blocks,
|block| &testcase.cfg.preds[block.index()],
&postord[..],
Block::new(0),
);
check_idom_violations(&idom[..], &testcase.path);
});

16
fuzz/fuzz_targets/ion.rs Normal file
View File

@@ -0,0 +1,16 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
#![no_main]
use libfuzzer_sys::fuzz_target;
use regalloc2::fuzzing::func::Func;
fuzz_target!(|func: Func| {
let _ = env_logger::try_init();
log::trace!("func:\n{:?}", func);
let env = regalloc2::fuzzing::func::machine_env();
let _out = regalloc2::fuzzing::ion::run(&func, &env, false).expect("regalloc did not succeed");
});

View File

@@ -0,0 +1,48 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
#![no_main]
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use regalloc2::fuzzing::checker::Checker;
use regalloc2::fuzzing::func::{Func, Options};
#[derive(Clone, Debug)]
struct TestCase {
func: Func,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<TestCase> {
Ok(TestCase {
func: Func::arbitrary_with_options(
u,
&Options {
reused_inputs: true,
fixed_regs: true,
clobbers: true,
control_flow: true,
reducible: false,
block_params: true,
always_local_uses: false,
reftypes: true,
},
)?,
})
}
}
fuzz_target!(|testcase: TestCase| {
let func = testcase.func;
let _ = env_logger::try_init();
log::trace!("func:\n{:?}", func);
let env = regalloc2::fuzzing::func::machine_env();
let out = regalloc2::fuzzing::ion::run(&func, &env, true).expect("regalloc did not succeed");
let mut checker = Checker::new(&func);
checker.prepare(&out);
checker.run().expect("checker failed");
});

View File

@@ -0,0 +1,81 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
#![no_main]
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use regalloc2::fuzzing::moves::ParallelMoves;
use regalloc2::{Allocation, PReg, RegClass};
use std::collections::HashSet;
#[derive(Clone, Debug)]
struct TestCase {
moves: Vec<(Allocation, Allocation)>,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<Self> {
let mut ret = TestCase { moves: vec![] };
let mut written = HashSet::new();
while bool::arbitrary(u)? {
let reg1 = u.int_in_range(0..=30)?;
let reg2 = u.int_in_range(0..=30)?;
if written.contains(&reg2) {
break;
}
written.insert(reg2);
ret.moves.push((
Allocation::reg(PReg::new(reg1, RegClass::Int)),
Allocation::reg(PReg::new(reg2, RegClass::Int)),
));
}
Ok(ret)
}
}
fuzz_target!(|testcase: TestCase| {
let _ = env_logger::try_init();
let scratch = Allocation::reg(PReg::new(31, RegClass::Int));
let mut par = ParallelMoves::new(scratch);
for &(src, dst) in &testcase.moves {
par.add(src, dst, ());
}
let moves = par.resolve();
// Compute the final source reg for each dest reg in the original
// parallel-move set.
let mut final_src_per_dest: Vec<Option<usize>> = vec![None; 32];
for &(src, dst) in &testcase.moves {
if let (Some(preg_src), Some(preg_dst)) = (src.as_reg(), dst.as_reg()) {
final_src_per_dest[preg_dst.hw_enc()] = Some(preg_src.hw_enc());
}
}
// Simulate the sequence of moves.
let mut regfile: Vec<Option<usize>> = vec![None; 32];
for i in 0..32 {
regfile[i] = Some(i);
}
for (src, dst, _) in moves {
if let (Some(preg_src), Some(preg_dst)) = (src.as_reg(), dst.as_reg()) {
let data = regfile[preg_src.hw_enc()];
regfile[preg_dst.hw_enc()] = data;
} else {
panic!("Bad allocation in move list");
}
}
// Assert that the expected register-moves occurred.
// N.B.: range up to 31 (not 32) to skip scratch register.
for i in 0..31 {
if let Some(orig_src) = final_src_per_dest[i] {
assert_eq!(regfile[i], Some(orig_src));
} else {
// Should be untouched.
assert_eq!(regfile[i], Some(i));
}
}
});

View File

@@ -0,0 +1,42 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
#![no_main]
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use regalloc2::fuzzing::cfg::CFGInfo;
use regalloc2::fuzzing::func::{Func, Options};
use regalloc2::fuzzing::ssa::validate_ssa;
#[derive(Debug)]
struct TestCase {
f: Func,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<Self> {
Ok(TestCase {
f: Func::arbitrary_with_options(
u,
&Options {
reused_inputs: true,
fixed_regs: true,
clobbers: true,
control_flow: true,
reducible: false,
always_local_uses: false,
block_params: true,
reftypes: true,
},
)?,
})
}
}
fuzz_target!(|t: TestCase| {
let cfginfo = CFGInfo::new(&t.f).expect("could not create CFG info");
validate_ssa(&t.f, &cfginfo).expect("invalid SSA");
});

Binary file not shown.

153
src/cfg.rs Normal file
View File

@@ -0,0 +1,153 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
//! Lightweight CFG analyses.
use crate::{domtree, postorder, Block, Function, Inst, OperandKind, ProgPoint, RegAllocError};
use smallvec::{smallvec, SmallVec};
#[derive(Clone, Debug)]
pub struct CFGInfo {
/// Postorder traversal of blocks.
pub postorder: Vec<Block>,
/// Domtree parents, indexed by block.
pub domtree: Vec<Block>,
/// For each instruction, the block it belongs to.
pub insn_block: Vec<Block>,
/// For each vreg, the instruction that defines it, if any.
pub vreg_def_inst: Vec<Inst>,
/// For each vreg, the block that defines it as a blockparam, if
/// any. (Every vreg must have a valid entry in either
/// `vreg_def_inst` or `vreg_def_blockparam`.)
pub vreg_def_blockparam: Vec<(Block, u32)>,
/// For each block, the first instruction.
pub block_entry: Vec<ProgPoint>,
/// For each block, the last instruction.
pub block_exit: Vec<ProgPoint>,
/// For each block, what is the approximate loop depth?
///
/// This measure is fully precise iff the input CFG is reducible
/// and blocks are in RPO, so that loop backedges are precisely
/// those whose block target indices are less than their source
/// indices. Otherwise, it will be approximate, but should still
/// be usable for heuristic purposes.
pub approx_loop_depth: Vec<u32>,
}
impl CFGInfo {
pub fn new<F: Function>(f: &F) -> Result<CFGInfo, RegAllocError> {
let postorder = postorder::calculate(f.num_blocks(), f.entry_block(), |block| {
f.block_succs(block)
});
let domtree = domtree::calculate(
f.num_blocks(),
|block| f.block_preds(block),
&postorder[..],
f.entry_block(),
);
let mut insn_block = vec![Block::invalid(); f.num_insts()];
let mut vreg_def_inst = vec![Inst::invalid(); f.num_vregs()];
let mut vreg_def_blockparam = vec![(Block::invalid(), 0); f.num_vregs()];
let mut block_entry = vec![ProgPoint::before(Inst::invalid()); f.num_blocks()];
let mut block_exit = vec![ProgPoint::before(Inst::invalid()); f.num_blocks()];
let mut backedge_in = vec![0; f.num_blocks()];
let mut backedge_out = vec![0; f.num_blocks()];
for block in 0..f.num_blocks() {
let block = Block::new(block);
for (i, param) in f.block_params(block).iter().enumerate() {
vreg_def_blockparam[param.vreg()] = (block, i as u32);
}
for inst in f.block_insns(block).iter() {
insn_block[inst.index()] = block;
for operand in f.inst_operands(inst) {
match operand.kind() {
OperandKind::Def => {
vreg_def_inst[operand.vreg().vreg()] = inst;
}
_ => {}
}
}
}
block_entry[block.index()] = ProgPoint::before(f.block_insns(block).first());
block_exit[block.index()] = ProgPoint::after(f.block_insns(block).last());
// Check critical edge condition: if there is more than
// one predecessor, each must have only one successor
// (this block).
let preds = f.block_preds(block).len() + if block == f.entry_block() { 1 } else { 0 };
if preds > 1 {
for &pred in f.block_preds(block) {
let succs = f.block_succs(pred).len();
if succs > 1 {
return Err(RegAllocError::CritEdge(pred, block));
}
}
}
// Check branch-arg condition: if any successors have more
// than one predecessor (given above, there will only be
// one such successor), then the last instruction of this
// block (the branch) cannot have any args other than the
// blockparams.
let mut require_no_branch_args = false;
for &succ in f.block_succs(block) {
let preds = f.block_preds(succ).len() + if succ == f.entry_block() { 1 } else { 0 };
if preds > 1 {
require_no_branch_args = true;
}
}
if require_no_branch_args {
let last = f.block_insns(block).last();
if f.branch_blockparam_arg_offset(block, last) > 0 {
return Err(RegAllocError::DisallowedBranchArg(last));
}
}
for &succ in f.block_succs(block) {
if succ.index() <= block.index() {
backedge_in[succ.index()] += 1;
backedge_out[block.index()] += 1;
}
}
}
let mut approx_loop_depth = vec![];
let mut backedge_stack: SmallVec<[usize; 4]> = smallvec![];
let mut cur_depth = 0;
for block in 0..f.num_blocks() {
if backedge_in[block] > 0 {
cur_depth += 1;
backedge_stack.push(backedge_in[block]);
}
approx_loop_depth.push(cur_depth);
while backedge_stack.len() > 0 && backedge_out[block] > 0 {
backedge_out[block] -= 1;
*backedge_stack.last_mut().unwrap() -= 1;
if *backedge_stack.last().unwrap() == 0 {
cur_depth -= 1;
backedge_stack.pop();
}
}
}
Ok(CFGInfo {
postorder,
domtree,
insn_block,
vreg_def_inst,
vreg_def_blockparam,
block_entry,
block_exit,
approx_loop_depth,
})
}
pub fn dominates(&self, a: Block, b: Block) -> bool {
domtree::dominates(&self.domtree[..], a, b)
}
}

745
src/checker.rs Normal file
View File

@@ -0,0 +1,745 @@
/*
* The following code is derived from `lib/src/checker.rs` in the
* regalloc.rs project
* (https://github.com/bytecodealliance/regalloc.rs). regalloc.rs is
* also licensed under Apache-2.0 with the LLVM exception, as the rest
* of regalloc2's non-Ion-derived code is.
*/
//! Checker: verifies that spills/reloads/moves retain equivalent
//! dataflow to original, VReg-based code.
//!
//! The basic idea is that we track symbolic values as they flow
//! through spills and reloads. The symbolic values represent
//! particular virtual registers in the original function body
//! presented to the register allocator. Any instruction in the
//! original function body (i.e., not added by the allocator)
//! conceptually generates a symbolic value "Vn" when storing to (or
//! modifying) a virtual register.
//!
//! These symbolic values are precise but partial: in other words, if
//! a physical register is described as containing a virtual register
//! at a program point, it must actually contain the value of this
//! register (modulo any analysis bugs); but it may resolve to
//! `Conflicts` even in cases where one *could* statically prove that
//! it contains a certain register, because the analysis is not
//! perfectly path-sensitive or value-sensitive. However, all
//! assignments *produced by our register allocator* should be
//! analyzed fully precisely.
//!
//! Operand constraints (fixed register, register, any) are also checked
//! at each operand.
//!
//! The dataflow analysis state at each program point is:
//!
//! - map of: Allocation -> lattice value (top > Vn symbols (unordered) > bottom)
//!
//! And the transfer functions for instructions are (where `A` is the
//! above map from allocated physical registers to symbolic values):
//!
//! - `Edit::Move` inserted by RA: [ alloc_d := alloc_s ]
//!
//! A[alloc_d] := A[alloc_s]
//!
//! - phi-node [ V_i := phi block_j:V_j, block_k:V_k, ... ]
//! with allocations [ A_i := phi block_j:A_j, block_k:A_k, ... ]
//! (N.B.: phi-nodes are not semantically present in the final
//! machine code, but we include their allocations so that this
//! checker can work)
//!
//! A[A_i] := meet(A[A_j], A[A_k], ...)
//!
//! - statement in pre-regalloc function [ V_i := op V_j, V_k, ... ]
//! with allocated form [ A_i := op A_j, A_k, ... ]
//!
//! A[A_i] := `V_i`
//!
//! In other words, a statement, even after allocation, generates
//! a symbol that corresponds to its original virtual-register
//! def.
//!
//! (N.B.: moves in pre-regalloc function fall into this last case
//! -- they are "just another operation" and generate a new
//! symbol)
//!
//! At control-flow join points, the symbols meet using a very simple
//! lattice meet-function: two different symbols in the same
//! allocation meet to "conflicted"; otherwise, the symbol meets with
//! itself to produce itself (reflexivity).
//!
//! To check correctness, we first find the dataflow fixpoint with the
//! above lattice and transfer/meet functions. Then, at each op, we
//! examine the dataflow solution at the preceding program point, and
//! check that the allocation for each op arg (input/use) contains the
//! symbol corresponding to the original virtual register specified
//! for this arg.
#![allow(dead_code)]
use crate::{
Allocation, AllocationKind, Block, Edit, Function, Inst, InstPosition, Operand,
OperandConstraint, OperandKind, OperandPos, Output, PReg, ProgPoint, SpillSlot, VReg,
};
use std::collections::{HashMap, HashSet, VecDeque};
use std::default::Default;
use std::hash::Hash;
use std::result::Result;
/// A set of errors detected by the regalloc checker.
#[derive(Clone, Debug)]
pub struct CheckerErrors {
errors: Vec<CheckerError>,
}
/// A single error detected by the regalloc checker.
#[derive(Clone, Debug)]
pub enum CheckerError {
MissingAllocation {
inst: Inst,
op: Operand,
},
UnknownValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
},
ConflictedValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
},
IncorrectValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
actual: VReg,
},
ConstraintViolated {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotReg {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotFixedReg {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotReuse {
inst: Inst,
op: Operand,
alloc: Allocation,
expected_alloc: Allocation,
},
AllocationIsNotStack {
inst: Inst,
op: Operand,
alloc: Allocation,
},
ConflictedValueInStackmap {
inst: Inst,
slot: SpillSlot,
},
NonRefValueInStackmap {
inst: Inst,
slot: SpillSlot,
vreg: VReg,
},
}
/// Abstract state for an allocation.
///
/// Forms a lattice with \top (`Unknown`), \bot (`Conflicted`), and a
/// number of mutually unordered value-points in between, one per real
/// or virtual register. Any two different registers meet to \bot.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CheckerValue {
/// "top" value: this storage slot has no known value.
Unknown,
/// "bottom" value: this storage slot has a conflicted value.
Conflicted,
/// Reg: this storage slot has a value that originated as a def
/// into the given virtual register.
///
/// The boolean flag indicates whether the value is
/// reference-typed.
Reg(VReg, bool),
}
impl Default for CheckerValue {
fn default() -> CheckerValue {
CheckerValue::Unknown
}
}
impl CheckerValue {
/// Meet function of the abstract-interpretation value lattice.
fn meet(&self, other: &CheckerValue) -> CheckerValue {
match (self, other) {
(&CheckerValue::Unknown, _) => *other,
(_, &CheckerValue::Unknown) => *self,
(&CheckerValue::Conflicted, _) => *self,
(_, &CheckerValue::Conflicted) => *other,
(&CheckerValue::Reg(r1, ref1), &CheckerValue::Reg(r2, ref2))
if r1 == r2 && ref1 == ref2 =>
{
CheckerValue::Reg(r1, ref1)
}
_ => {
log::trace!("{:?} and {:?} meet to Conflicted", self, other);
CheckerValue::Conflicted
}
}
}
}
/// State that steps through program points as we scan over the instruction stream.
#[derive(Clone, Debug, PartialEq, Eq)]
struct CheckerState {
allocations: HashMap<Allocation, CheckerValue>,
}
impl Default for CheckerState {
fn default() -> CheckerState {
CheckerState {
allocations: HashMap::new(),
}
}
}
impl std::fmt::Display for CheckerValue {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
CheckerValue::Unknown => write!(f, "?"),
CheckerValue::Conflicted => write!(f, "!"),
CheckerValue::Reg(r, false) => write!(f, "{}", r),
CheckerValue::Reg(r, true) => write!(f, "{}/ref", r),
}
}
}
fn merge_map<K: Copy + Clone + PartialEq + Eq + Hash>(
into: &mut HashMap<K, CheckerValue>,
from: &HashMap<K, CheckerValue>,
) {
for (k, v) in from {
let into_v = into.entry(*k).or_insert(Default::default());
let merged = into_v.meet(v);
*into_v = merged;
}
}
impl CheckerState {
/// Create a new checker state.
fn new() -> CheckerState {
Default::default()
}
/// Merge this checker state with another at a CFG join-point.
fn meet_with(&mut self, other: &CheckerState) {
merge_map(&mut self.allocations, &other.allocations);
}
fn check_val(
&self,
inst: Inst,
op: Operand,
alloc: Allocation,
val: CheckerValue,
allocs: &[Allocation],
) -> Result<(), CheckerError> {
if alloc == Allocation::none() {
return Err(CheckerError::MissingAllocation { inst, op });
}
match val {
CheckerValue::Unknown => {
return Err(CheckerError::UnknownValueInAllocation { inst, op, alloc });
}
CheckerValue::Conflicted => {
return Err(CheckerError::ConflictedValueInAllocation { inst, op, alloc });
}
CheckerValue::Reg(r, _) if r != op.vreg() => {
return Err(CheckerError::IncorrectValueInAllocation {
inst,
op,
alloc,
actual: r,
});
}
_ => {}
}
self.check_constraint(inst, op, alloc, allocs)?;
Ok(())
}
/// Check an instruction against this state. This must be called
/// twice: once with `InstPosition::Before`, and once with
/// `InstPosition::After` (after updating state with defs).
fn check(&self, pos: InstPosition, checkinst: &CheckerInst) -> Result<(), CheckerError> {
match checkinst {
&CheckerInst::Op {
inst,
ref operands,
ref allocs,
..
} => {
// Skip Use-checks at the After point if there are any
// reused inputs: the Def which reuses the input
// happens early.
let has_reused_input = operands
.iter()
.any(|op| matches!(op.constraint(), OperandConstraint::Reuse(_)));
if has_reused_input && pos == InstPosition::After {
return Ok(());
}
// For each operand, check (i) that the allocation
// contains the expected vreg, and (ii) that it meets
// the requirements of the OperandConstraint.
for (op, alloc) in operands.iter().zip(allocs.iter()) {
let is_here = match (op.pos(), pos) {
(OperandPos::Early, InstPosition::Before) => true,
(OperandPos::Late, InstPosition::After) => true,
_ => false,
};
if !is_here {
continue;
}
if op.kind() == OperandKind::Def {
continue;
}
let val = self
.allocations
.get(alloc)
.cloned()
.unwrap_or(Default::default());
log::trace!(
"checker: checkinst {:?}: op {:?}, alloc {:?}, checker value {:?}",
checkinst,
op,
alloc,
val
);
self.check_val(inst, *op, *alloc, val, allocs)?;
}
}
&CheckerInst::Safepoint { inst, ref slots } => {
for &slot in slots {
let alloc = Allocation::stack(slot);
let val = self
.allocations
.get(&alloc)
.cloned()
.unwrap_or(Default::default());
log::trace!(
"checker: checkinst {:?}: safepoint slot {}, checker value {:?}",
checkinst,
slot,
val
);
match val {
CheckerValue::Unknown => {}
CheckerValue::Conflicted => {
return Err(CheckerError::ConflictedValueInStackmap { inst, slot });
}
CheckerValue::Reg(vreg, false) => {
return Err(CheckerError::NonRefValueInStackmap { inst, slot, vreg });
}
CheckerValue::Reg(_, true) => {}
}
}
}
_ => {}
}
Ok(())
}
/// Update according to instruction.
fn update<'a, F: Function>(&mut self, checkinst: &CheckerInst, checker: &Checker<'a, F>) {
match checkinst {
&CheckerInst::Move { into, from } => {
let val = self
.allocations
.get(&from)
.cloned()
.unwrap_or(Default::default());
log::trace!(
"checker: checkinst {:?} updating: move {:?} -> {:?} val {:?}",
checkinst,
from,
into,
val
);
self.allocations.insert(into, val);
}
&CheckerInst::Op {
ref operands,
ref allocs,
ref clobbers,
..
} => {
for (op, alloc) in operands.iter().zip(allocs.iter()) {
if op.kind() != OperandKind::Def {
continue;
}
let reftyped = checker.reftyped_vregs.contains(&op.vreg());
self.allocations
.insert(*alloc, CheckerValue::Reg(op.vreg(), reftyped));
}
for clobber in clobbers {
self.allocations.remove(&Allocation::reg(*clobber));
}
}
&CheckerInst::DefAlloc { alloc, vreg } => {
let reftyped = checker.reftyped_vregs.contains(&vreg);
self.allocations
.insert(alloc, CheckerValue::Reg(vreg, reftyped));
}
&CheckerInst::Safepoint { ref slots, .. } => {
for (alloc, value) in &mut self.allocations {
if let CheckerValue::Reg(_, true) = *value {
if alloc.is_reg() {
*value = CheckerValue::Conflicted;
} else if alloc.is_stack() && !slots.contains(&alloc.as_stack().unwrap()) {
*value = CheckerValue::Conflicted;
}
}
}
}
}
}
fn check_constraint(
&self,
inst: Inst,
op: Operand,
alloc: Allocation,
allocs: &[Allocation],
) -> Result<(), CheckerError> {
match op.constraint() {
OperandConstraint::Any => {}
OperandConstraint::Reg => {
if alloc.kind() != AllocationKind::Reg {
return Err(CheckerError::AllocationIsNotReg { inst, op, alloc });
}
}
OperandConstraint::Stack => {
if alloc.kind() != AllocationKind::Stack {
return Err(CheckerError::AllocationIsNotStack { inst, op, alloc });
}
}
OperandConstraint::FixedReg(preg) => {
if alloc != Allocation::reg(preg) {
return Err(CheckerError::AllocationIsNotFixedReg { inst, op, alloc });
}
}
OperandConstraint::Reuse(idx) => {
if alloc.kind() != AllocationKind::Reg {
return Err(CheckerError::AllocationIsNotReg { inst, op, alloc });
}
if alloc != allocs[idx] {
return Err(CheckerError::AllocationIsNotReuse {
inst,
op,
alloc,
expected_alloc: allocs[idx],
});
}
}
}
Ok(())
}
}
/// An instruction representation in the checker's BB summary.
#[derive(Clone, Debug)]
pub(crate) enum CheckerInst {
/// A move between allocations (these could be registers or
/// spillslots).
Move { into: Allocation, from: Allocation },
/// A regular instruction with fixed use and def slots. Contains
/// both the original operands (as given to the regalloc) and the
/// allocation results.
Op {
inst: Inst,
operands: Vec<Operand>,
allocs: Vec<Allocation>,
clobbers: Vec<PReg>,
},
/// Define an allocation's contents. Like BlockParams but for one
/// allocation. Used sometimes when moves are elided but ownership
/// of a value is logically transferred to a new vreg.
DefAlloc { alloc: Allocation, vreg: VReg },
/// A safepoint, with the given SpillSlots specified as containing
/// reftyped values. All other reftyped values become invalid.
Safepoint { inst: Inst, slots: Vec<SpillSlot> },
}
#[derive(Debug)]
pub struct Checker<'a, F: Function> {
f: &'a F,
bb_in: HashMap<Block, CheckerState>,
bb_insts: HashMap<Block, Vec<CheckerInst>>,
reftyped_vregs: HashSet<VReg>,
}
impl<'a, F: Function> Checker<'a, F> {
/// Create a new checker for the given function, initializing CFG
/// info immediately. The client should call the `add_*()`
/// methods to add abstract instructions to each BB before
/// invoking `run()` to check for errors.
pub fn new(f: &'a F) -> Checker<'a, F> {
let mut bb_in = HashMap::new();
let mut bb_insts = HashMap::new();
let mut reftyped_vregs = HashSet::new();
for block in 0..f.num_blocks() {
let block = Block::new(block);
bb_in.insert(block, Default::default());
bb_insts.insert(block, vec![]);
}
for &vreg in f.reftype_vregs() {
reftyped_vregs.insert(vreg);
}
Checker {
f,
bb_in,
bb_insts,
reftyped_vregs,
}
}
/// Build the list of checker instructions based on the given func
/// and allocation results.
pub fn prepare(&mut self, out: &Output) {
log::trace!("checker: out = {:?}", out);
// Preprocess safepoint stack-maps into per-inst vecs.
let mut safepoint_slots: HashMap<Inst, Vec<SpillSlot>> = HashMap::new();
for &(progpoint, slot) in &out.safepoint_slots {
safepoint_slots
.entry(progpoint.inst())
.or_insert_with(|| vec![])
.push(slot);
}
// For each original instruction, create an `Op`.
let mut last_inst = None;
let mut insert_idx = 0;
for block in 0..self.f.num_blocks() {
let block = Block::new(block);
for inst in self.f.block_insns(block).iter() {
assert!(last_inst.is_none() || inst > last_inst.unwrap());
last_inst = Some(inst);
// Any inserted edits before instruction.
self.handle_edits(block, out, &mut insert_idx, ProgPoint::before(inst));
// If this is a safepoint, then check the spillslots at this point.
if self.f.requires_refs_on_stack(inst) {
let slots = safepoint_slots.remove(&inst).unwrap_or_else(|| vec![]);
let checkinst = CheckerInst::Safepoint { inst, slots };
self.bb_insts.get_mut(&block).unwrap().push(checkinst);
}
// Skip if this is a branch: the blockparams do not
// exist in post-regalloc code, and the edge-moves
// have to be inserted before the branch rather than
// after.
if !self.f.is_branch(inst) {
// Instruction itself.
let operands: Vec<_> = self.f.inst_operands(inst).iter().cloned().collect();
let allocs: Vec<_> = out.inst_allocs(inst).iter().cloned().collect();
let clobbers: Vec<_> = self.f.inst_clobbers(inst).iter().cloned().collect();
let checkinst = CheckerInst::Op {
inst,
operands,
allocs,
clobbers,
};
log::trace!("checker: adding inst {:?}", checkinst);
self.bb_insts.get_mut(&block).unwrap().push(checkinst);
}
// Any inserted edits after instruction.
self.handle_edits(block, out, &mut insert_idx, ProgPoint::after(inst));
}
}
}
fn handle_edits(&mut self, block: Block, out: &Output, idx: &mut usize, pos: ProgPoint) {
while *idx < out.edits.len() && out.edits[*idx].0 <= pos {
let &(edit_pos, ref edit) = &out.edits[*idx];
*idx += 1;
if edit_pos < pos {
continue;
}
log::trace!("checker: adding edit {:?} at pos {:?}", edit, pos);
match edit {
&Edit::Move { from, to, to_vreg } => {
self.bb_insts
.get_mut(&block)
.unwrap()
.push(CheckerInst::Move { into: to, from });
if let Some(vreg) = to_vreg {
self.bb_insts
.get_mut(&block)
.unwrap()
.push(CheckerInst::DefAlloc { alloc: to, vreg });
}
}
&Edit::DefAlloc { alloc, vreg } => {
self.bb_insts
.get_mut(&block)
.unwrap()
.push(CheckerInst::DefAlloc { alloc, vreg });
}
}
}
}
/// Perform the dataflow analysis to compute checker state at each BB entry.
fn analyze(&mut self) {
let mut queue = VecDeque::new();
let mut queue_set = HashSet::new();
for block in 0..self.f.num_blocks() {
let block = Block::new(block);
queue.push_back(block);
queue_set.insert(block);
}
while !queue.is_empty() {
let block = queue.pop_front().unwrap();
queue_set.remove(&block);
let mut state = self.bb_in.get(&block).cloned().unwrap();
log::trace!("analyze: block {} has state {:?}", block.index(), state);
for inst in self.bb_insts.get(&block).unwrap() {
state.update(inst, self);
log::trace!("analyze: inst {:?} -> state {:?}", inst, state);
}
for &succ in self.f.block_succs(block) {
let cur_succ_in = self.bb_in.get(&succ).unwrap();
let mut new_state = state.clone();
new_state.meet_with(cur_succ_in);
let changed = &new_state != cur_succ_in;
if changed {
log::trace!(
"analyze: block {} state changed from {:?} to {:?}; pushing onto queue",
succ.index(),
cur_succ_in,
new_state
);
self.bb_in.insert(succ, new_state);
if !queue_set.contains(&succ) {
queue.push_back(succ);
queue_set.insert(succ);
}
}
}
}
}
/// Using BB-start state computed by `analyze()`, step the checker state
/// through each BB and check each instruction's register allocations
/// for errors.
fn find_errors(&self) -> Result<(), CheckerErrors> {
let mut errors = vec![];
for (block, input) in &self.bb_in {
let mut state = input.clone();
for inst in self.bb_insts.get(block).unwrap() {
if let Err(e) = state.check(InstPosition::Before, inst) {
log::trace!("Checker error: {:?}", e);
errors.push(e);
}
state.update(inst, self);
if let Err(e) = state.check(InstPosition::After, inst) {
log::trace!("Checker error: {:?}", e);
errors.push(e);
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(CheckerErrors { errors })
}
}
/// Find any errors, returning `Err(CheckerErrors)` with all errors found
/// or `Ok(())` otherwise.
pub fn run(mut self) -> Result<(), CheckerErrors> {
self.analyze();
let result = self.find_errors();
log::trace!("=== CHECKER RESULT ===");
fn print_state(state: &CheckerState) {
let mut s = vec![];
for (alloc, state) in &state.allocations {
s.push(format!("{} := {}", alloc, state));
}
log::trace!(" {{ {} }}", s.join(", "))
}
for vreg in self.f.reftype_vregs() {
log::trace!(" REF: {}", vreg);
}
for bb in 0..self.f.num_blocks() {
let bb = Block::new(bb);
log::trace!("block{}:", bb.index());
let insts = self.bb_insts.get(&bb).unwrap();
let mut state = self.bb_in.get(&bb).unwrap().clone();
print_state(&state);
for inst in insts {
match inst {
&CheckerInst::Op {
inst,
ref operands,
ref allocs,
ref clobbers,
} => {
log::trace!(
" inst{}: {:?} ({:?}) clobbers:{:?}",
inst.index(),
operands,
allocs,
clobbers
);
}
&CheckerInst::Move { from, into } => {
log::trace!(" {} -> {}", from, into);
}
&CheckerInst::DefAlloc { alloc, vreg } => {
log::trace!(" defalloc: {}:{}", vreg, alloc);
}
&CheckerInst::Safepoint { ref slots, .. } => {
let mut slotargs = vec![];
for &slot in slots {
slotargs.push(format!("{}", slot));
}
log::trace!(" safepoint: {}", slotargs.join(", "));
}
}
state.update(inst, &self);
print_state(&state);
}
}
result
}
}

118
src/domtree.rs Normal file
View File

@@ -0,0 +1,118 @@
/*
* Derives from the dominator tree implementation in regalloc.rs, which is
* licensed under the Apache Public License 2.0 with LLVM Exception. See:
* https://github.com/bytecodealliance/regalloc.rs
*/
// This is an implementation of the algorithm described in
//
// A Simple, Fast Dominance Algorithm
// Keith D. Cooper, Timothy J. Harvey, and Ken Kennedy
// Department of Computer Science, Rice University, Houston, Texas, USA
// TR-06-33870
// https://www.cs.rice.edu/~keith/EMBED/dom.pdf
use crate::Block;
// Helper
fn merge_sets(
idom: &[Block], // map from Block to Block
block_to_rpo: &[Option<u32>],
mut node1: Block,
mut node2: Block,
) -> Block {
while node1 != node2 {
if node1.is_invalid() || node2.is_invalid() {
return Block::invalid();
}
let rpo1 = block_to_rpo[node1.index()].unwrap();
let rpo2 = block_to_rpo[node2.index()].unwrap();
if rpo1 > rpo2 {
node1 = idom[node1.index()];
} else if rpo2 > rpo1 {
node2 = idom[node2.index()];
}
}
assert!(node1 == node2);
node1
}
pub fn calculate<'a, PredFn: Fn(Block) -> &'a [Block]>(
num_blocks: usize,
preds: PredFn,
post_ord: &[Block],
start: Block,
) -> Vec<Block> {
// We have post_ord, which is the postorder sequence.
// Compute maps from RPO to block number and vice-versa.
let mut block_to_rpo = vec![None; num_blocks];
block_to_rpo.resize(num_blocks, None);
for (i, rpo_block) in post_ord.iter().rev().enumerate() {
block_to_rpo[rpo_block.index()] = Some(i as u32);
}
let mut idom = vec![Block::invalid(); num_blocks];
// The start node must have itself as a parent.
idom[start.index()] = start;
let mut changed = true;
while changed {
changed = false;
// Consider blocks in reverse postorder. Skip any that are unreachable.
for &node in post_ord.iter().rev() {
let rponum = block_to_rpo[node.index()].unwrap();
let mut parent = Block::invalid();
for &pred in preds(node).iter() {
let pred_rpo = match block_to_rpo[pred.index()] {
Some(r) => r,
None => {
// Skip unreachable preds.
continue;
}
};
if pred_rpo < rponum {
parent = pred;
break;
}
}
if parent.is_valid() {
for &pred in preds(node).iter() {
if pred == parent {
continue;
}
if idom[pred.index()].is_invalid() {
continue;
}
parent = merge_sets(&idom, &block_to_rpo[..], parent, pred);
}
}
if parent.is_valid() && parent != idom[node.index()] {
idom[node.index()] = parent;
changed = true;
}
}
}
// Now set the start node's dominator-tree parent to "invalid";
// this allows the loop in `dominates` to terminate.
idom[start.index()] = Block::invalid();
idom
}
pub fn dominates(idom: &[Block], a: Block, mut b: Block) -> bool {
loop {
if a == b {
return true;
}
if b.is_invalid() {
return false;
}
b = idom[b.index()];
}
}

603
src/fuzzing/func.rs Normal file
View File

@@ -0,0 +1,603 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
use crate::{
domtree, postorder, Allocation, Block, Function, Inst, InstRange, MachineEnv, Operand,
OperandConstraint, OperandKind, OperandPos, PReg, RegClass, VReg,
};
use arbitrary::Result as ArbitraryResult;
use arbitrary::{Arbitrary, Unstructured};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InstOpcode {
Phi,
Op,
Call,
Ret,
Branch,
}
#[derive(Clone, Debug)]
pub struct InstData {
op: InstOpcode,
operands: Vec<Operand>,
clobbers: Vec<PReg>,
is_safepoint: bool,
}
impl InstData {
pub fn op(def: usize, uses: &[usize]) -> InstData {
let mut operands = vec![Operand::reg_def(VReg::new(def, RegClass::Int))];
for &u in uses {
operands.push(Operand::reg_use(VReg::new(u, RegClass::Int)));
}
InstData {
op: InstOpcode::Op,
operands,
clobbers: vec![],
is_safepoint: false,
}
}
pub fn branch(uses: &[usize]) -> InstData {
let mut operands = vec![];
for &u in uses {
operands.push(Operand::reg_use(VReg::new(u, RegClass::Int)));
}
InstData {
op: InstOpcode::Branch,
operands,
clobbers: vec![],
is_safepoint: false,
}
}
pub fn ret() -> InstData {
InstData {
op: InstOpcode::Ret,
operands: vec![],
clobbers: vec![],
is_safepoint: false,
}
}
}
#[derive(Clone)]
pub struct Func {
insts: Vec<InstData>,
blocks: Vec<InstRange>,
block_preds: Vec<Vec<Block>>,
block_succs: Vec<Vec<Block>>,
block_params: Vec<Vec<VReg>>,
num_vregs: usize,
reftype_vregs: Vec<VReg>,
}
impl Function for Func {
fn num_insts(&self) -> usize {
self.insts.len()
}
fn num_blocks(&self) -> usize {
self.blocks.len()
}
fn entry_block(&self) -> Block {
assert!(self.blocks.len() > 0);
Block::new(0)
}
fn block_insns(&self, block: Block) -> InstRange {
self.blocks[block.index()]
}
fn block_succs(&self, block: Block) -> &[Block] {
&self.block_succs[block.index()][..]
}
fn block_preds(&self, block: Block) -> &[Block] {
&self.block_preds[block.index()][..]
}
fn block_params(&self, block: Block) -> &[VReg] {
&self.block_params[block.index()][..]
}
fn is_call(&self, insn: Inst) -> bool {
self.insts[insn.index()].op == InstOpcode::Call
}
fn is_ret(&self, insn: Inst) -> bool {
self.insts[insn.index()].op == InstOpcode::Ret
}
fn is_branch(&self, insn: Inst) -> bool {
self.insts[insn.index()].op == InstOpcode::Branch
}
fn branch_blockparam_arg_offset(&self, _: Block, _: Inst) -> usize {
// Branch blockparam args always start at zero for this
// Function implementation.
0
}
fn requires_refs_on_stack(&self, insn: Inst) -> bool {
self.insts[insn.index()].is_safepoint
}
fn reftype_vregs(&self) -> &[VReg] {
&self.reftype_vregs[..]
}
fn is_move(&self, _: Inst) -> Option<(Operand, Operand)> {
None
}
fn inst_operands(&self, insn: Inst) -> &[Operand] {
&self.insts[insn.index()].operands[..]
}
fn inst_clobbers(&self, insn: Inst) -> &[PReg] {
&self.insts[insn.index()].clobbers[..]
}
fn num_vregs(&self) -> usize {
self.num_vregs
}
fn spillslot_size(&self, regclass: RegClass) -> usize {
match regclass {
RegClass::Int => 1,
RegClass::Float => 2,
}
}
}
struct FuncBuilder {
postorder: Vec<Block>,
idom: Vec<Block>,
f: Func,
insts_per_block: Vec<Vec<InstData>>,
}
impl FuncBuilder {
fn new() -> Self {
FuncBuilder {
postorder: vec![],
idom: vec![],
f: Func {
block_preds: vec![],
block_succs: vec![],
block_params: vec![],
insts: vec![],
blocks: vec![],
num_vregs: 0,
reftype_vregs: vec![],
},
insts_per_block: vec![],
}
}
pub fn add_block(&mut self) -> Block {
let b = Block::new(self.f.blocks.len());
self.f
.blocks
.push(InstRange::forward(Inst::new(0), Inst::new(0)));
self.f.block_preds.push(vec![]);
self.f.block_succs.push(vec![]);
self.f.block_params.push(vec![]);
self.insts_per_block.push(vec![]);
b
}
pub fn add_inst(&mut self, block: Block, data: InstData) {
self.insts_per_block[block.index()].push(data);
}
pub fn add_edge(&mut self, from: Block, to: Block) {
self.f.block_succs[from.index()].push(to);
self.f.block_preds[to.index()].push(from);
}
pub fn set_block_params(&mut self, block: Block, params: &[VReg]) {
self.f.block_params[block.index()] = params.iter().cloned().collect();
}
fn compute_doms(&mut self) {
self.postorder = postorder::calculate(self.f.blocks.len(), Block::new(0), |block| {
&self.f.block_succs[block.index()][..]
});
self.idom = domtree::calculate(
self.f.blocks.len(),
|block| &self.f.block_preds[block.index()][..],
&self.postorder[..],
Block::new(0),
);
}
fn finalize(mut self) -> Func {
for (blocknum, blockrange) in self.f.blocks.iter_mut().enumerate() {
let begin_inst = self.f.insts.len();
for inst in &self.insts_per_block[blocknum] {
self.f.insts.push(inst.clone());
}
let end_inst = self.f.insts.len();
*blockrange = InstRange::forward(Inst::new(begin_inst), Inst::new(end_inst));
}
self.f
}
}
impl Arbitrary for OperandConstraint {
fn arbitrary(u: &mut Unstructured) -> ArbitraryResult<Self> {
Ok(*u.choose(&[OperandConstraint::Any, OperandConstraint::Reg])?)
}
}
fn choose_dominating_block(
idom: &[Block],
mut block: Block,
allow_self: bool,
u: &mut Unstructured,
) -> ArbitraryResult<Block> {
assert!(block.is_valid());
let orig_block = block;
loop {
if (allow_self || block != orig_block) && bool::arbitrary(u)? {
break;
}
if idom[block.index()].is_invalid() {
break;
}
block = idom[block.index()];
}
let block = if block != orig_block || allow_self {
block
} else {
Block::invalid()
};
Ok(block)
}
#[derive(Clone, Copy, Debug)]
pub struct Options {
pub reused_inputs: bool,
pub fixed_regs: bool,
pub clobbers: bool,
pub control_flow: bool,
pub reducible: bool,
pub block_params: bool,
pub always_local_uses: bool,
pub reftypes: bool,
}
impl std::default::Default for Options {
fn default() -> Self {
Options {
reused_inputs: false,
fixed_regs: false,
clobbers: false,
control_flow: true,
reducible: false,
block_params: true,
always_local_uses: false,
reftypes: false,
}
}
}
impl Arbitrary for Func {
fn arbitrary(u: &mut Unstructured) -> ArbitraryResult<Func> {
Func::arbitrary_with_options(u, &Options::default())
}
}
impl Func {
pub fn arbitrary_with_options(u: &mut Unstructured, opts: &Options) -> ArbitraryResult<Func> {
// General strategy:
// 1. Create an arbitrary CFG.
// 2. Create a list of vregs to define in each block.
// 3. Define some of those vregs in each block as blockparams.f.
// 4. Populate blocks with ops that define the rest of the vregs.
// - For each use, choose an available vreg: either one
// already defined (via blockparam or inst) in this block,
// or one defined in a dominating block.
let mut builder = FuncBuilder::new();
for _ in 0..u.int_in_range(1..=100)? {
builder.add_block();
}
let num_blocks = builder.f.blocks.len();
// Generate a CFG. Create a "spine" of either single blocks,
// with links to the next; or fork patterns, with the left
// fork linking to the next and the right fork in `out_blocks`
// to be connected below. This creates an arbitrary CFG with
// split critical edges, which is a property that we require
// for the regalloc.
let mut from = 0;
let mut out_blocks = vec![];
let mut in_blocks = vec![];
// For reducibility, if selected: enforce strict nesting of backedges
let mut max_backedge_src = 0;
let mut min_backedge_dest = num_blocks;
while from < num_blocks {
in_blocks.push(from);
if num_blocks > 3 && from < num_blocks - 3 && bool::arbitrary(u)? && opts.control_flow {
// To avoid critical edges, we use from+1 as an edge
// block, and advance `from` an extra block; `from+2`
// will be the next normal iteration.
builder.add_edge(Block::new(from), Block::new(from + 1));
builder.add_edge(Block::new(from), Block::new(from + 2));
builder.add_edge(Block::new(from + 2), Block::new(from + 3));
out_blocks.push(from + 1);
from += 2;
} else if from < num_blocks - 1 {
builder.add_edge(Block::new(from), Block::new(from + 1));
}
from += 1;
}
for pred in out_blocks {
let mut succ = *u.choose(&in_blocks[..])?;
if opts.reducible && (pred >= succ) {
if pred < max_backedge_src || succ > min_backedge_dest {
// If the chosen edge would result in an
// irreducible CFG, just make this a diamond
// instead.
succ = pred + 2;
} else {
max_backedge_src = pred;
min_backedge_dest = succ;
}
}
builder.add_edge(Block::new(pred), Block::new(succ));
}
builder.compute_doms();
for block in 0..num_blocks {
builder.f.block_preds[block].clear();
}
for block in 0..num_blocks {
for &succ in &builder.f.block_succs[block] {
builder.f.block_preds[succ.index()].push(Block::new(block));
}
}
builder.compute_doms();
let mut vregs_by_block = vec![];
let mut vregs_by_block_to_be_defined = vec![];
let mut block_params = vec![vec![]; num_blocks];
for block in 0..num_blocks {
let mut vregs = vec![];
for _ in 0..u.int_in_range(5..=15)? {
let vreg = VReg::new(builder.f.num_vregs, RegClass::Int);
builder.f.num_vregs += 1;
vregs.push(vreg);
if opts.reftypes && bool::arbitrary(u)? {
builder.f.reftype_vregs.push(vreg);
}
}
vregs_by_block.push(vregs.clone());
vregs_by_block_to_be_defined.push(vec![]);
let mut max_block_params = u.int_in_range(0..=std::cmp::min(3, vregs.len() / 3))?;
for &vreg in &vregs {
if block > 0 && opts.block_params && bool::arbitrary(u)? && max_block_params > 0 {
block_params[block].push(vreg);
max_block_params -= 1;
} else {
vregs_by_block_to_be_defined.last_mut().unwrap().push(vreg);
}
}
vregs_by_block_to_be_defined.last_mut().unwrap().reverse();
builder.set_block_params(Block::new(block), &block_params[block][..]);
}
for block in 0..num_blocks {
let mut avail = block_params[block].clone();
let mut remaining_nonlocal_uses = u.int_in_range(0..=3)?;
while let Some(vreg) = vregs_by_block_to_be_defined[block].pop() {
let def_constraint = OperandConstraint::arbitrary(u)?;
let def_pos = if bool::arbitrary(u)? {
OperandPos::Early
} else {
OperandPos::Late
};
let mut operands = vec![Operand::new(
vreg,
def_constraint,
OperandKind::Def,
def_pos,
)];
let mut allocations = vec![Allocation::none()];
for _ in 0..u.int_in_range(0..=3)? {
let vreg = if avail.len() > 0
&& (opts.always_local_uses
|| remaining_nonlocal_uses == 0
|| bool::arbitrary(u)?)
{
*u.choose(&avail[..])?
} else if !opts.always_local_uses {
let def_block = choose_dominating_block(
&builder.idom[..],
Block::new(block),
/* allow_self = */ false,
u,
)?;
if !def_block.is_valid() {
// No vregs already defined, and no pred blocks that dominate us
// (perhaps we are the entry block): just stop generating inputs.
break;
}
remaining_nonlocal_uses -= 1;
*u.choose(&vregs_by_block[def_block.index()])?
} else {
break;
};
let use_constraint = OperandConstraint::arbitrary(u)?;
operands.push(Operand::new(
vreg,
use_constraint,
OperandKind::Use,
OperandPos::Early,
));
allocations.push(Allocation::none());
}
let mut clobbers: Vec<PReg> = vec![];
if operands.len() > 1 && opts.reused_inputs && bool::arbitrary(u)? {
// Make the def a reused input.
let op = operands[0];
assert_eq!(op.kind(), OperandKind::Def);
let reused = u.int_in_range(1..=(operands.len() - 1))?;
operands[0] = Operand::new(
op.vreg(),
OperandConstraint::Reuse(reused),
op.kind(),
OperandPos::Late,
);
// Make sure reused input is a Reg.
let op = operands[reused];
operands[reused] = Operand::new(
op.vreg(),
OperandConstraint::Reg,
op.kind(),
OperandPos::Early,
);
} else if opts.fixed_regs && bool::arbitrary(u)? {
let mut fixed = vec![];
for _ in 0..u.int_in_range(0..=operands.len() - 1)? {
// Pick an operand and make it a fixed reg.
let fixed_reg = PReg::new(u.int_in_range(0..=30)?, RegClass::Int);
if fixed.contains(&fixed_reg) {
break;
}
fixed.push(fixed_reg);
let i = u.int_in_range(0..=(operands.len() - 1))?;
let op = operands[i];
operands[i] = Operand::new(
op.vreg(),
OperandConstraint::FixedReg(fixed_reg),
op.kind(),
op.pos(),
);
}
} else if opts.clobbers && bool::arbitrary(u)? {
for _ in 0..u.int_in_range(0..=5)? {
let reg = u.int_in_range(0..=30)?;
if clobbers.iter().any(|r| r.hw_enc() == reg) {
break;
}
clobbers.push(PReg::new(reg, RegClass::Int));
}
}
let is_safepoint = opts.reftypes
&& operands
.iter()
.all(|op| !builder.f.reftype_vregs.contains(&op.vreg()))
&& bool::arbitrary(u)?;
let op = *u.choose(&[InstOpcode::Op, InstOpcode::Call])?;
builder.add_inst(
Block::new(block),
InstData {
op,
operands,
clobbers,
is_safepoint,
},
);
avail.push(vreg);
}
// Define the branch with blockparam args that must end
// the block.
if builder.f.block_succs[block].len() > 0 {
let mut args = vec![];
for &succ in &builder.f.block_succs[block] {
for _ in 0..builder.f.block_params[succ.index()].len() {
let dom_block = choose_dominating_block(
&builder.idom[..],
Block::new(block),
false,
u,
)?;
let vreg = if dom_block.is_valid() && bool::arbitrary(u)? {
u.choose(&vregs_by_block[dom_block.index()][..])?
} else {
u.choose(&avail[..])?
};
args.push(vreg.vreg());
}
}
builder.add_inst(Block::new(block), InstData::branch(&args[..]));
} else {
builder.add_inst(Block::new(block), InstData::ret());
}
}
Ok(builder.finalize())
}
}
impl std::fmt::Debug for Func {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{{\n")?;
for vreg in self.reftype_vregs() {
write!(f, " REF: {}\n", vreg)?;
}
for (i, blockrange) in self.blocks.iter().enumerate() {
let succs = self.block_succs[i]
.iter()
.map(|b| b.index())
.collect::<Vec<_>>();
let preds = self.block_preds[i]
.iter()
.map(|b| b.index())
.collect::<Vec<_>>();
let params = self.block_params[i]
.iter()
.map(|v| format!("v{}", v.vreg()))
.collect::<Vec<_>>()
.join(", ");
write!(
f,
" block{}({}): # succs:{:?} preds:{:?}\n",
i, params, succs, preds
)?;
for inst in blockrange.iter() {
if self.requires_refs_on_stack(inst) {
write!(f, " -- SAFEPOINT --\n")?;
}
write!(
f,
" inst{}: {:?} ops:{:?} clobber:{:?}\n",
inst.index(),
self.insts[inst.index()].op,
self.insts[inst.index()].operands,
self.insts[inst.index()].clobbers
)?;
}
}
write!(f, "}}\n")?;
Ok(())
}
}
pub fn machine_env() -> MachineEnv {
// Reg 31 is the scratch reg.
let regs: Vec<PReg> = (0..31).map(|i| PReg::new(i, RegClass::Int)).collect();
let preferred_regs_by_class: [Vec<PReg>; 2] = [regs.iter().cloned().take(24).collect(), vec![]];
let non_preferred_regs_by_class: [Vec<PReg>; 2] =
[regs.iter().cloned().skip(24).collect(), vec![]];
let scratch_by_class: [PReg; 2] = [PReg::new(31, RegClass::Int), PReg::new(0, RegClass::Float)];
MachineEnv {
regs,
preferred_regs_by_class,
non_preferred_regs_by_class,
scratch_by_class,
}
}

32
src/fuzzing/mod.rs Normal file
View File

@@ -0,0 +1,32 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
//! Utilities for fuzzing.
pub mod func;
// Re-exports for fuzz targets.
pub mod domtree {
pub use crate::domtree::*;
}
pub mod postorder {
pub use crate::postorder::*;
}
pub mod moves {
pub use crate::moves::*;
}
pub mod cfg {
pub use crate::cfg::*;
}
pub mod ssa {
pub use crate::ssa::*;
}
pub mod ion {
pub use crate::ion::*;
}
pub mod checker {
pub use crate::checker::*;
}

181
src/index.rs Normal file
View File

@@ -0,0 +1,181 @@
#[macro_export]
macro_rules! define_index {
($ix:ident) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $ix(pub u32);
impl $ix {
#[inline(always)]
pub fn new(i: usize) -> Self {
Self(i as u32)
}
#[inline(always)]
pub fn index(self) -> usize {
assert!(self.is_valid());
self.0 as usize
}
#[inline(always)]
pub fn invalid() -> Self {
Self(u32::MAX)
}
#[inline(always)]
pub fn is_invalid(self) -> bool {
self == Self::invalid()
}
#[inline(always)]
pub fn is_valid(self) -> bool {
self != Self::invalid()
}
#[inline(always)]
pub fn next(self) -> $ix {
assert!(self.is_valid());
Self(self.0 + 1)
}
#[inline(always)]
pub fn prev(self) -> $ix {
assert!(self.is_valid());
Self(self.0 - 1)
}
#[inline(always)]
pub fn raw_u32(self) -> u32 {
self.0
}
}
impl crate::index::ContainerIndex for $ix {}
};
}
pub trait ContainerIndex: Clone + Copy + std::fmt::Debug + PartialEq + Eq {}
pub trait ContainerComparator {
type Ix: ContainerIndex;
fn compare(&self, a: Self::Ix, b: Self::Ix) -> std::cmp::Ordering;
}
define_index!(Inst);
define_index!(Block);
#[derive(Clone, Copy, Debug)]
pub struct InstRange(Inst, Inst, bool);
impl InstRange {
#[inline(always)]
pub fn forward(from: Inst, to: Inst) -> Self {
assert!(from.index() <= to.index());
InstRange(from, to, true)
}
#[inline(always)]
pub fn backward(from: Inst, to: Inst) -> Self {
assert!(from.index() >= to.index());
InstRange(to, from, false)
}
#[inline(always)]
pub fn first(self) -> Inst {
assert!(self.len() > 0);
if self.is_forward() {
self.0
} else {
self.1.prev()
}
}
#[inline(always)]
pub fn last(self) -> Inst {
assert!(self.len() > 0);
if self.is_forward() {
self.1.prev()
} else {
self.0
}
}
#[inline(always)]
pub fn rest(self) -> InstRange {
assert!(self.len() > 0);
if self.is_forward() {
InstRange::forward(self.0.next(), self.1)
} else {
InstRange::backward(self.1.prev(), self.0)
}
}
#[inline(always)]
pub fn len(self) -> usize {
self.1.index() - self.0.index()
}
#[inline(always)]
pub fn is_forward(self) -> bool {
self.2
}
#[inline(always)]
pub fn rev(self) -> Self {
Self(self.0, self.1, !self.2)
}
#[inline(always)]
pub fn iter(self) -> InstRangeIter {
InstRangeIter(self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct InstRangeIter(InstRange);
impl Iterator for InstRangeIter {
type Item = Inst;
#[inline(always)]
fn next(&mut self) -> Option<Inst> {
if self.0.len() == 0 {
None
} else {
let ret = self.0.first();
self.0 = self.0.rest();
Some(ret)
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_inst_range() {
let range = InstRange::forward(Inst::new(0), Inst::new(0));
assert_eq!(range.len(), 0);
let range = InstRange::forward(Inst::new(0), Inst::new(5));
assert_eq!(range.first().index(), 0);
assert_eq!(range.last().index(), 4);
assert_eq!(range.len(), 5);
assert_eq!(
range.iter().collect::<Vec<_>>(),
vec![
Inst::new(0),
Inst::new(1),
Inst::new(2),
Inst::new(3),
Inst::new(4)
]
);
let range = range.rev();
assert_eq!(range.first().index(), 4);
assert_eq!(range.last().index(), 0);
assert_eq!(range.len(), 5);
assert_eq!(
range.iter().collect::<Vec<_>>(),
vec![
Inst::new(4),
Inst::new(3),
Inst::new(2),
Inst::new(1),
Inst::new(0)
]
);
}
}

367
src/indexset.rs Normal file
View File

@@ -0,0 +1,367 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
//! Index sets: sets of integers that represent indices into a space.
use fxhash::FxHashMap;
use std::cell::Cell;
const SMALL_ELEMS: usize = 12;
/// A hybrid large/small-mode sparse mapping from integer indices to
/// elements.
///
/// The trailing `(u32, u64)` elements in each variant is a one-item
/// cache to allow fast access when streaming through.
#[derive(Clone, Debug)]
enum AdaptiveMap {
Small {
len: u32,
keys: [u32; SMALL_ELEMS],
values: [u64; SMALL_ELEMS],
},
Large(FxHashMap<u32, u64>),
}
const INVALID: u32 = 0xffff_ffff;
impl AdaptiveMap {
fn new() -> Self {
Self::Small {
len: 0,
keys: [INVALID; SMALL_ELEMS],
values: [0; SMALL_ELEMS],
}
}
/// Expand into `Large` mode if we are at capacity and have no
/// zero-value pairs that can be trimmed.
#[inline(never)]
fn expand(&mut self) {
match self {
&mut Self::Small {
ref mut len,
ref mut keys,
ref mut values,
} => {
// Note: we *may* remain as `Small` if there are any
// zero elements. Try removing them first, before we
// commit to a memory allocation.
if values.iter().any(|v| *v == 0) {
let mut out = 0;
for i in 0..(*len as usize) {
if values[i] == 0 {
continue;
}
if out < i {
keys[out] = keys[i];
values[out] = values[i];
}
out += 1;
}
*len = out as u32;
} else {
let mut map = FxHashMap::default();
for i in 0..(*len as usize) {
map.insert(keys[i], values[i]);
}
*self = Self::Large(map);
}
}
_ => {}
}
}
#[inline(always)]
fn get_or_insert<'a>(&'a mut self, key: u32) -> &'a mut u64 {
// Check whether the key is present and we are in small mode;
// if no to both, we need to expand first.
let (needs_expand, small_mode_idx) = match self {
&mut Self::Small { len, ref keys, .. } => {
// Perform this scan but do not return right away;
// doing so runs into overlapping-borrow issues
// because the current non-lexical lifetimes
// implementation is not able to see that the `self`
// mutable borrow on return is only on the
// early-return path.
let small_mode_idx = keys.iter().take(len as usize).position(|k| *k == key);
let needs_expand = small_mode_idx.is_none() && len == SMALL_ELEMS as u32;
(needs_expand, small_mode_idx)
}
_ => (false, None),
};
if needs_expand {
assert!(small_mode_idx.is_none());
self.expand();
}
match self {
&mut Self::Small {
ref mut len,
ref mut keys,
ref mut values,
} => {
// If we found the key already while checking whether
// we need to expand above, use that index to return
// early.
if let Some(i) = small_mode_idx {
return &mut values[i];
}
// Otherwise, the key must not be present; add a new
// entry.
assert!(*len < SMALL_ELEMS as u32);
let idx = *len;
*len += 1;
keys[idx as usize] = key;
values[idx as usize] = 0;
&mut values[idx as usize]
}
&mut Self::Large(ref mut map) => map.entry(key).or_insert(0),
}
}
#[inline(always)]
fn get_mut(&mut self, key: u32) -> Option<&mut u64> {
match self {
&mut Self::Small {
len,
ref keys,
ref mut values,
} => {
for i in 0..len {
if keys[i as usize] == key {
return Some(&mut values[i as usize]);
}
}
None
}
&mut Self::Large(ref mut map) => map.get_mut(&key),
}
}
#[inline(always)]
fn get(&self, key: u32) -> Option<u64> {
match self {
&Self::Small {
len,
ref keys,
ref values,
} => {
for i in 0..len {
if keys[i as usize] == key {
let value = values[i as usize];
return Some(value);
}
}
None
}
&Self::Large(ref map) => {
let value = map.get(&key).cloned();
value
}
}
}
fn iter<'a>(&'a self) -> AdaptiveMapIter<'a> {
match self {
&Self::Small {
len,
ref keys,
ref values,
} => AdaptiveMapIter::Small(&keys[0..len as usize], &values[0..len as usize]),
&Self::Large(ref map) => AdaptiveMapIter::Large(map.iter()),
}
}
}
enum AdaptiveMapIter<'a> {
Small(&'a [u32], &'a [u64]),
Large(std::collections::hash_map::Iter<'a, u32, u64>),
}
impl<'a> std::iter::Iterator for AdaptiveMapIter<'a> {
type Item = (u32, u64);
fn next(&mut self) -> Option<Self::Item> {
match self {
&mut Self::Small(ref mut keys, ref mut values) => {
if keys.is_empty() {
None
} else {
let (k, v) = ((*keys)[0], (*values)[0]);
*keys = &(*keys)[1..];
*values = &(*values)[1..];
Some((k, v))
}
}
&mut Self::Large(ref mut it) => it.next().map(|(&k, &v)| (k, v)),
}
}
}
/// A conceptually infinite-length set of indices that allows union
/// and efficient iteration over elements.
#[derive(Clone)]
pub struct IndexSet {
elems: AdaptiveMap,
cache: Cell<(u32, u64)>,
}
const BITS_PER_WORD: usize = 64;
impl IndexSet {
pub fn new() -> Self {
Self {
elems: AdaptiveMap::new(),
cache: Cell::new((INVALID, 0)),
}
}
#[inline(always)]
fn elem(&mut self, bit_index: usize) -> &mut u64 {
let word_index = (bit_index / BITS_PER_WORD) as u32;
if self.cache.get().0 == word_index {
self.cache.set((INVALID, 0));
}
self.elems.get_or_insert(word_index)
}
#[inline(always)]
fn maybe_elem_mut(&mut self, bit_index: usize) -> Option<&mut u64> {
let word_index = (bit_index / BITS_PER_WORD) as u32;
if self.cache.get().0 == word_index {
self.cache.set((INVALID, 0));
}
self.elems.get_mut(word_index)
}
#[inline(always)]
fn maybe_elem(&self, bit_index: usize) -> Option<u64> {
let word_index = (bit_index / BITS_PER_WORD) as u32;
if self.cache.get().0 == word_index {
Some(self.cache.get().1)
} else {
self.elems.get(word_index)
}
}
#[inline(always)]
pub fn set(&mut self, idx: usize, val: bool) {
let bit = idx % BITS_PER_WORD;
if val {
*self.elem(idx) |= 1 << bit;
} else if let Some(word) = self.maybe_elem_mut(idx) {
*word &= !(1 << bit);
}
}
pub fn assign(&mut self, other: &Self) {
self.elems = other.elems.clone();
self.cache = other.cache.clone();
}
#[inline(always)]
pub fn get(&self, idx: usize) -> bool {
let bit = idx % BITS_PER_WORD;
if let Some(word) = self.maybe_elem(idx) {
(word & (1 << bit)) != 0
} else {
false
}
}
pub fn union_with(&mut self, other: &Self) -> bool {
let mut changed = 0;
for (word_idx, bits) in other.elems.iter() {
if bits == 0 {
continue;
}
let word_idx = word_idx as usize;
let self_word = self.elem(word_idx * BITS_PER_WORD);
changed |= bits & !*self_word;
*self_word |= bits;
}
changed != 0
}
pub fn iter<'a>(&'a self) -> impl Iterator<Item = usize> + 'a {
self.elems.iter().flat_map(|(word_idx, bits)| {
let word_idx = word_idx as usize;
set_bits(bits).map(move |i| BITS_PER_WORD * word_idx + i)
})
}
/// Is the adaptive data structure in "small" mode? This is meant
/// for testing assertions only.
pub(crate) fn is_small(&self) -> bool {
match &self.elems {
&AdaptiveMap::Small { .. } => true,
_ => false,
}
}
}
fn set_bits(bits: u64) -> impl Iterator<Item = usize> {
let iter = SetBitsIter(bits);
iter
}
pub struct SetBitsIter(u64);
impl Iterator for SetBitsIter {
type Item = usize;
fn next(&mut self) -> Option<usize> {
// Build an `Option<NonZeroU64>` so that on the nonzero path,
// the compiler can optimize the trailing-zeroes operator
// using that knowledge.
std::num::NonZeroU64::new(self.0).map(|nz| {
let bitidx = nz.trailing_zeros();
self.0 &= self.0 - 1; // clear highest set bit
bitidx as usize
})
}
}
impl std::fmt::Debug for IndexSet {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let vals = self.iter().collect::<Vec<_>>();
write!(f, "{:?}", vals)
}
}
#[cfg(test)]
mod test {
use super::IndexSet;
#[test]
fn test_set_bits_iter() {
let mut vec = IndexSet::new();
let mut sum = 0;
for i in 0..1024 {
if i % 17 == 0 {
vec.set(i, true);
sum += i;
}
}
let mut checksum = 0;
for bit in vec.iter() {
assert!(bit % 17 == 0);
checksum += bit;
}
assert_eq!(sum, checksum);
}
#[test]
fn test_expand_remove_zero_elems() {
let mut vec = IndexSet::new();
// Set 12 different words (this is the max small-mode size).
for i in 0..12 {
vec.set(64 * i, true);
}
// Now clear a bit, and set a bit in a different word. We
// should still be in small mode.
vec.set(64 * 5, false);
vec.set(64 * 100, true);
assert!(vec.is_small());
}
}

373
src/ion/LICENSE Normal file
View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

535
src/ion/data_structures.rs Normal file
View File

@@ -0,0 +1,535 @@
/*
* The following license applies to this file, which was initially
* derived from the files `js/src/jit/BacktrackingAllocator.h` and
* `js/src/jit/BacktrackingAllocator.cpp` in Mozilla Firefox:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Since the initial port, the design has been substantially evolved
* and optimized.
*/
//! Data structures for backtracking allocator.
use super::liveranges::SpillWeight;
use crate::cfg::CFGInfo;
use crate::index::ContainerComparator;
use crate::indexset::IndexSet;
use crate::{
define_index, Allocation, Block, Edit, Function, Inst, MachineEnv, Operand, PReg, ProgPoint,
RegClass, SpillSlot, VReg,
};
use smallvec::SmallVec;
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Debug;
/// A range from `from` (inclusive) to `to` (exclusive).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CodeRange {
pub from: ProgPoint,
pub to: ProgPoint,
}
impl CodeRange {
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.from == self.to
}
#[inline(always)]
pub fn contains(&self, other: &Self) -> bool {
other.from >= self.from && other.to <= self.to
}
#[inline(always)]
pub fn contains_point(&self, other: ProgPoint) -> bool {
other >= self.from && other < self.to
}
#[inline(always)]
pub fn overlaps(&self, other: &Self) -> bool {
other.to > self.from && other.from < self.to
}
#[inline(always)]
pub fn len(&self) -> usize {
self.to.inst().index() - self.from.inst().index()
}
}
impl std::cmp::PartialOrd for CodeRange {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for CodeRange {
#[inline(always)]
fn cmp(&self, other: &Self) -> Ordering {
if self.to <= other.from {
Ordering::Less
} else if self.from >= other.to {
Ordering::Greater
} else {
Ordering::Equal
}
}
}
define_index!(LiveBundleIndex);
define_index!(LiveRangeIndex);
define_index!(SpillSetIndex);
define_index!(UseIndex);
define_index!(VRegIndex);
define_index!(PRegIndex);
define_index!(SpillSlotIndex);
/// Used to carry small sets of bundles, e.g. for conflict sets.
pub type LiveBundleVec = SmallVec<[LiveBundleIndex; 4]>;
#[derive(Clone, Copy, Debug)]
pub struct LiveRangeListEntry {
pub range: CodeRange,
pub index: LiveRangeIndex,
}
pub type LiveRangeList = SmallVec<[LiveRangeListEntry; 4]>;
pub type UseList = SmallVec<[Use; 2]>;
#[derive(Clone, Debug)]
pub struct LiveRange {
pub range: CodeRange,
pub vreg: VRegIndex,
pub bundle: LiveBundleIndex,
pub uses_spill_weight_and_flags: u32,
pub uses: UseList,
pub merged_into: LiveRangeIndex,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum LiveRangeFlag {
StartsAtDef = 1,
}
impl LiveRange {
#[inline(always)]
pub fn set_flag(&mut self, flag: LiveRangeFlag) {
self.uses_spill_weight_and_flags |= (flag as u32) << 29;
}
#[inline(always)]
pub fn clear_flag(&mut self, flag: LiveRangeFlag) {
self.uses_spill_weight_and_flags &= !((flag as u32) << 29);
}
#[inline(always)]
pub fn assign_flag(&mut self, flag: LiveRangeFlag, val: bool) {
let bit = if val { (flag as u32) << 29 } else { 0 };
self.uses_spill_weight_and_flags &= 0xe000_0000;
self.uses_spill_weight_and_flags |= bit;
}
#[inline(always)]
pub fn has_flag(&self, flag: LiveRangeFlag) -> bool {
self.uses_spill_weight_and_flags & ((flag as u32) << 29) != 0
}
#[inline(always)]
pub fn flag_word(&self) -> u32 {
self.uses_spill_weight_and_flags & 0xe000_0000
}
#[inline(always)]
pub fn merge_flags(&mut self, flag_word: u32) {
self.uses_spill_weight_and_flags |= flag_word;
}
#[inline(always)]
pub fn uses_spill_weight(&self) -> SpillWeight {
let bits = (self.uses_spill_weight_and_flags & 0x1fff_ffff) << 2;
SpillWeight::from_f32(f32::from_bits(bits))
}
#[inline(always)]
pub fn set_uses_spill_weight(&mut self, weight: SpillWeight) {
let weight_bits = (weight.to_f32().to_bits() >> 2) & 0x1fff_ffff;
self.uses_spill_weight_and_flags =
(self.uses_spill_weight_and_flags & 0xe000_0000) | weight_bits;
}
}
#[derive(Clone, Copy, Debug)]
pub struct Use {
pub operand: Operand,
pub pos: ProgPoint,
pub slot: u8,
pub weight: u16,
}
impl Use {
#[inline(always)]
pub fn new(operand: Operand, pos: ProgPoint, slot: u8) -> Self {
Self {
operand,
pos,
slot,
// Weight is updated on insertion into LR.
weight: 0,
}
}
}
pub const SLOT_NONE: u8 = u8::MAX;
#[derive(Clone, Debug)]
pub struct LiveBundle {
pub ranges: LiveRangeList,
pub spillset: SpillSetIndex,
pub allocation: Allocation,
pub prio: u32, // recomputed after every bulk update
pub spill_weight_and_props: u32,
}
impl LiveBundle {
#[inline(always)]
pub fn set_cached_spill_weight_and_props(
&mut self,
spill_weight: u32,
minimal: bool,
fixed: bool,
stack: bool,
) {
debug_assert!(spill_weight < ((1 << 29) - 1));
self.spill_weight_and_props = spill_weight
| (if minimal { 1 << 31 } else { 0 })
| (if fixed { 1 << 30 } else { 0 })
| (if stack { 1 << 29 } else { 0 });
}
#[inline(always)]
pub fn cached_minimal(&self) -> bool {
self.spill_weight_and_props & (1 << 31) != 0
}
#[inline(always)]
pub fn cached_fixed(&self) -> bool {
self.spill_weight_and_props & (1 << 30) != 0
}
#[inline(always)]
pub fn cached_stack(&self) -> bool {
self.spill_weight_and_props & (1 << 29) != 0
}
#[inline(always)]
pub fn set_cached_fixed(&mut self) {
self.spill_weight_and_props |= 1 << 30;
}
#[inline(always)]
pub fn set_cached_stack(&mut self) {
self.spill_weight_and_props |= 1 << 29;
}
#[inline(always)]
pub fn cached_spill_weight(&self) -> u32 {
self.spill_weight_and_props & ((1 << 29) - 1)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BundleProperties {
pub minimal: bool,
pub fixed: bool,
}
#[derive(Clone, Debug)]
pub struct SpillSet {
pub vregs: SmallVec<[VRegIndex; 2]>,
pub slot: SpillSlotIndex,
pub reg_hint: PReg,
pub class: RegClass,
pub spill_bundle: LiveBundleIndex,
pub required: bool,
pub size: u8,
}
#[derive(Clone, Debug)]
pub struct VRegData {
pub ranges: LiveRangeList,
pub blockparam: Block,
pub is_ref: bool,
pub is_pinned: bool,
}
#[derive(Clone, Debug)]
pub struct PRegData {
pub reg: PReg,
pub allocations: LiveRangeSet,
}
#[derive(Clone, Debug)]
pub struct Env<'a, F: Function> {
pub func: &'a F,
pub env: &'a MachineEnv,
pub cfginfo: CFGInfo,
pub liveins: Vec<IndexSet>,
pub liveouts: Vec<IndexSet>,
/// Blockparam outputs: from-vreg, (end of) from-block, (start of)
/// to-block, to-vreg. The field order is significant: these are sorted so
/// that a scan over vregs, then blocks in each range, can scan in
/// order through this (sorted) list and add allocs to the
/// half-move list.
pub blockparam_outs: Vec<(VRegIndex, Block, Block, VRegIndex)>,
/// Blockparam inputs: to-vreg, (start of) to-block, (end of)
/// from-block. As above for `blockparam_outs`, field order is
/// significant.
pub blockparam_ins: Vec<(VRegIndex, Block, Block)>,
/// Blockparam allocs: block, idx, vreg, alloc. Info to describe
/// blockparam locations at block entry, for metadata purposes
/// (e.g. for the checker).
pub blockparam_allocs: Vec<(Block, u32, VRegIndex, Allocation)>,
pub ranges: Vec<LiveRange>,
pub bundles: Vec<LiveBundle>,
pub spillsets: Vec<SpillSet>,
pub vregs: Vec<VRegData>,
pub vreg_regs: Vec<VReg>,
pub pregs: Vec<PRegData>,
pub allocation_queue: PrioQueue,
pub clobbers: Vec<Inst>, // Sorted list of insts with clobbers.
pub safepoints: Vec<Inst>, // Sorted list of safepoint insts.
pub safepoints_per_vreg: HashMap<usize, HashSet<Inst>>,
pub spilled_bundles: Vec<LiveBundleIndex>,
pub spillslots: Vec<SpillSlotData>,
pub slots_by_size: Vec<SpillSlotList>,
pub extra_spillslot: Vec<Option<Allocation>>,
// Program moves: these are moves in the provided program that we
// handle with our internal machinery, in order to avoid the
// overhead of ordinary operand processing. We expect the client
// to not generate any code for instructions that return
// `Some(..)` for `.is_move()`, and instead use the edits that we
// provide to implement those moves (or some simplified version of
// them) post-regalloc.
//
// (from-vreg, inst, from-alloc), sorted by (from-vreg, inst)
pub prog_move_srcs: Vec<((VRegIndex, Inst), Allocation)>,
// (to-vreg, inst, to-alloc), sorted by (to-vreg, inst)
pub prog_move_dsts: Vec<((VRegIndex, Inst), Allocation)>,
// (from-vreg, to-vreg) for bundle-merging.
pub prog_move_merges: Vec<(LiveRangeIndex, LiveRangeIndex)>,
// When multiple fixed-register constraints are present on a
// single VReg at a single program point (this can happen for,
// e.g., call args that use the same value multiple times), we
// remove all but one of the fixed-register constraints, make a
// note here, and add a clobber with that PReg instread to keep
// the register available. When we produce the final edit-list, we
// will insert a copy from wherever the VReg's primary allocation
// was to the approprate PReg.
//
// (progpoint, copy-from-preg, copy-to-preg, to-slot)
pub multi_fixed_reg_fixups: Vec<(ProgPoint, PRegIndex, PRegIndex, usize)>,
pub inserted_moves: Vec<InsertedMove>,
// Output:
pub edits: Vec<(u32, InsertMovePrio, Edit)>,
pub allocs: Vec<Allocation>,
pub inst_alloc_offsets: Vec<u32>,
pub num_spillslots: u32,
pub safepoint_slots: Vec<(ProgPoint, SpillSlot)>,
pub allocated_bundle_count: usize,
pub stats: Stats,
// For debug output only: a list of textual annotations at every
// ProgPoint to insert into the final allocated program listing.
pub debug_annotations: std::collections::HashMap<ProgPoint, Vec<String>>,
pub annotations_enabled: bool,
}
#[derive(Clone, Debug)]
pub struct SpillSlotData {
pub ranges: LiveRangeSet,
pub class: RegClass,
pub alloc: Allocation,
pub next_spillslot: SpillSlotIndex,
}
#[derive(Clone, Debug)]
pub struct SpillSlotList {
pub first_spillslot: SpillSlotIndex,
pub last_spillslot: SpillSlotIndex,
}
#[derive(Clone, Debug)]
pub struct PrioQueue {
pub heap: std::collections::BinaryHeap<PrioQueueEntry>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct PrioQueueEntry {
pub prio: u32,
pub bundle: LiveBundleIndex,
pub reg_hint: PReg,
}
#[derive(Clone, Debug)]
pub struct LiveRangeSet {
pub btree: BTreeMap<LiveRangeKey, LiveRangeIndex>,
}
#[derive(Clone, Copy, Debug)]
pub struct LiveRangeKey {
pub from: u32,
pub to: u32,
}
impl LiveRangeKey {
#[inline(always)]
pub fn from_range(range: &CodeRange) -> Self {
Self {
from: range.from.to_index(),
to: range.to.to_index(),
}
}
#[inline(always)]
pub fn to_range(&self) -> CodeRange {
CodeRange {
from: ProgPoint::from_index(self.from),
to: ProgPoint::from_index(self.to),
}
}
}
impl std::cmp::PartialEq for LiveRangeKey {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
self.to > other.from && self.from < other.to
}
}
impl std::cmp::Eq for LiveRangeKey {}
impl std::cmp::PartialOrd for LiveRangeKey {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for LiveRangeKey {
#[inline(always)]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self.to <= other.from {
std::cmp::Ordering::Less
} else if self.from >= other.to {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}
}
pub struct PrioQueueComparator<'a> {
pub prios: &'a [usize],
}
impl<'a> ContainerComparator for PrioQueueComparator<'a> {
type Ix = LiveBundleIndex;
fn compare(&self, a: Self::Ix, b: Self::Ix) -> std::cmp::Ordering {
self.prios[a.index()].cmp(&self.prios[b.index()])
}
}
impl PrioQueue {
pub fn new() -> Self {
PrioQueue {
heap: std::collections::BinaryHeap::new(),
}
}
#[inline(always)]
pub fn insert(&mut self, bundle: LiveBundleIndex, prio: usize, reg_hint: PReg) {
self.heap.push(PrioQueueEntry {
prio: prio as u32,
bundle,
reg_hint,
});
}
#[inline(always)]
pub fn is_empty(self) -> bool {
self.heap.is_empty()
}
#[inline(always)]
pub fn pop(&mut self) -> Option<(LiveBundleIndex, PReg)> {
self.heap.pop().map(|entry| (entry.bundle, entry.reg_hint))
}
}
impl LiveRangeSet {
pub(crate) fn new() -> Self {
Self {
btree: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct InsertedMove {
pub pos: ProgPoint,
pub prio: InsertMovePrio,
pub from_alloc: Allocation,
pub to_alloc: Allocation,
pub to_vreg: Option<VReg>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum InsertMovePrio {
InEdgeMoves,
BlockParam,
Regular,
PostRegular,
MultiFixedReg,
ReusedInput,
OutEdgeMoves,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Stats {
pub livein_blocks: usize,
pub livein_iterations: usize,
pub initial_liverange_count: usize,
pub merged_bundle_count: usize,
pub prog_moves: usize,
pub prog_moves_dead_src: usize,
pub prog_move_merge_attempt: usize,
pub prog_move_merge_success: usize,
pub process_bundle_count: usize,
pub process_bundle_reg_probes_fixed: usize,
pub process_bundle_reg_success_fixed: usize,
pub process_bundle_bounding_range_probe_start_any: usize,
pub process_bundle_bounding_range_probes_any: usize,
pub process_bundle_bounding_range_success_any: usize,
pub process_bundle_reg_probe_start_any: usize,
pub process_bundle_reg_probes_any: usize,
pub process_bundle_reg_success_any: usize,
pub evict_bundle_event: usize,
pub evict_bundle_count: usize,
pub splits: usize,
pub splits_clobbers: usize,
pub splits_hot: usize,
pub splits_conflicts: usize,
pub splits_defs: usize,
pub splits_all: usize,
pub final_liverange_count: usize,
pub final_bundle_count: usize,
pub spill_bundle_count: usize,
pub spill_bundle_reg_probes: usize,
pub spill_bundle_reg_success: usize,
pub blockparam_ins_count: usize,
pub blockparam_outs_count: usize,
pub blockparam_allocs_count: usize,
pub halfmoves_count: usize,
pub edits_count: usize,
}

141
src/ion/dump.rs Normal file
View File

@@ -0,0 +1,141 @@
//! Debugging output.
use super::Env;
use crate::{Block, Function, ProgPoint};
impl<'a, F: Function> Env<'a, F> {
pub fn dump_state(&self) {
log::trace!("Bundles:");
for (i, b) in self.bundles.iter().enumerate() {
log::trace!(
"bundle{}: spillset={:?} alloc={:?}",
i,
b.spillset,
b.allocation
);
for entry in &b.ranges {
log::trace!(
" * range {:?} -- {:?}: range{}",
entry.range.from,
entry.range.to,
entry.index.index()
);
}
}
log::trace!("VRegs:");
for (i, v) in self.vregs.iter().enumerate() {
log::trace!("vreg{}:", i);
for entry in &v.ranges {
log::trace!(
" * range {:?} -- {:?}: range{}",
entry.range.from,
entry.range.to,
entry.index.index()
);
}
}
log::trace!("Ranges:");
for (i, r) in self.ranges.iter().enumerate() {
log::trace!(
"range{}: range={:?} vreg={:?} bundle={:?} weight={:?}",
i,
r.range,
r.vreg,
r.bundle,
r.uses_spill_weight(),
);
for u in &r.uses {
log::trace!(" * use at {:?} (slot {}): {:?}", u.pos, u.slot, u.operand);
}
}
}
pub fn annotate(&mut self, progpoint: ProgPoint, s: String) {
if self.annotations_enabled {
self.debug_annotations
.entry(progpoint)
.or_insert_with(|| vec![])
.push(s);
}
}
pub fn dump_results(&self) {
log::info!("=== REGALLOC RESULTS ===");
for block in 0..self.func.num_blocks() {
let block = Block::new(block);
log::info!(
"block{}: [succs {:?} preds {:?}]",
block.index(),
self.func
.block_succs(block)
.iter()
.map(|b| b.index())
.collect::<Vec<_>>(),
self.func
.block_preds(block)
.iter()
.map(|b| b.index())
.collect::<Vec<_>>()
);
for inst in self.func.block_insns(block).iter() {
for annotation in self
.debug_annotations
.get(&ProgPoint::before(inst))
.map(|v| &v[..])
.unwrap_or(&[])
{
log::info!(" inst{}-pre: {}", inst.index(), annotation);
}
let ops = self
.func
.inst_operands(inst)
.iter()
.map(|op| format!("{}", op))
.collect::<Vec<_>>();
let clobbers = self
.func
.inst_clobbers(inst)
.iter()
.map(|preg| format!("{}", preg))
.collect::<Vec<_>>();
let allocs = (0..ops.len())
.map(|i| format!("{}", self.get_alloc(inst, i)))
.collect::<Vec<_>>();
let opname = if self.func.is_branch(inst) {
"br"
} else if self.func.is_call(inst) {
"call"
} else if self.func.is_ret(inst) {
"ret"
} else {
"op"
};
let args = ops
.iter()
.zip(allocs.iter())
.map(|(op, alloc)| format!("{} [{}]", op, alloc))
.collect::<Vec<_>>();
let clobbers = if clobbers.is_empty() {
"".to_string()
} else {
format!(" [clobber: {}]", clobbers.join(", "))
};
log::info!(
" inst{}: {} {}{}",
inst.index(),
opname,
args.join(", "),
clobbers
);
for annotation in self
.debug_annotations
.get(&ProgPoint::after(inst))
.map(|v| &v[..])
.unwrap_or(&[])
{
log::info!(" inst{}-post: {}", inst.index(), annotation);
}
}
}
}
}

1249
src/ion/liveranges.rs Normal file

File diff suppressed because it is too large Load Diff

439
src/ion/merge.rs Normal file
View File

@@ -0,0 +1,439 @@
/*
* The following license applies to this file, which was initially
* derived from the files `js/src/jit/BacktrackingAllocator.h` and
* `js/src/jit/BacktrackingAllocator.cpp` in Mozilla Firefox:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Since the initial port, the design has been substantially evolved
* and optimized.
*/
//! Bundle merging.
use super::{
Env, LiveBundleIndex, LiveRangeIndex, LiveRangeKey, Requirement, SpillSet, SpillSetIndex,
SpillSlotIndex, VRegIndex,
};
use crate::{Function, Inst, OperandConstraint, PReg};
use smallvec::smallvec;
impl<'a, F: Function> Env<'a, F> {
pub fn merge_bundles(&mut self, from: LiveBundleIndex, to: LiveBundleIndex) -> bool {
if from == to {
// Merge bundle into self -- trivial merge.
return true;
}
log::trace!(
"merging from bundle{} to bundle{}",
from.index(),
to.index()
);
// Both bundles must deal with the same RegClass.
let from_rc = self.spillsets[self.bundles[from.index()].spillset.index()].class;
let to_rc = self.spillsets[self.bundles[to.index()].spillset.index()].class;
if from_rc != to_rc {
log::trace!(" -> mismatching reg classes");
return false;
}
// If either bundle is already assigned (due to a pinned vreg), don't merge.
if self.bundles[from.index()].allocation.is_some()
|| self.bundles[to.index()].allocation.is_some()
{
log::trace!("one of the bundles is already assigned (pinned)");
return false;
}
#[cfg(debug_assertions)]
{
// Sanity check: both bundles should contain only ranges with appropriate VReg classes.
for entry in &self.bundles[from.index()].ranges {
let vreg = self.ranges[entry.index.index()].vreg;
assert_eq!(from_rc, self.vreg_regs[vreg.index()].class());
}
for entry in &self.bundles[to.index()].ranges {
let vreg = self.ranges[entry.index.index()].vreg;
assert_eq!(to_rc, self.vreg_regs[vreg.index()].class());
}
}
// Check for overlap in LiveRanges and for conflicting
// requirements.
let ranges_from = &self.bundles[from.index()].ranges[..];
let ranges_to = &self.bundles[to.index()].ranges[..];
let mut idx_from = 0;
let mut idx_to = 0;
let mut range_count = 0;
while idx_from < ranges_from.len() && idx_to < ranges_to.len() {
range_count += 1;
if range_count > 200 {
log::trace!(
"reached merge complexity (range_count = {}); exiting",
range_count
);
// Limit merge complexity.
return false;
}
if ranges_from[idx_from].range.from >= ranges_to[idx_to].range.to {
idx_to += 1;
} else if ranges_to[idx_to].range.from >= ranges_from[idx_from].range.to {
idx_from += 1;
} else {
// Overlap -- cannot merge.
log::trace!(
" -> overlap between {:?} and {:?}, exiting",
ranges_from[idx_from].index,
ranges_to[idx_to].index
);
return false;
}
}
// Check for a requirements conflict.
if self.bundles[from.index()].cached_stack()
|| self.bundles[from.index()].cached_fixed()
|| self.bundles[to.index()].cached_stack()
|| self.bundles[to.index()].cached_fixed()
{
let req = self
.compute_requirement(from)
.merge(self.compute_requirement(to));
if req == Requirement::Conflict {
log::trace!(" -> conflicting requirements; aborting merge");
return false;
}
}
log::trace!(" -> committing to merge");
// If we reach here, then the bundles do not overlap -- merge
// them! We do this with a merge-sort-like scan over both
// lists, building a new range list and replacing the list on
// `to` when we're done.
if ranges_from.is_empty() {
// `from` bundle is empty -- trivial merge.
log::trace!(" -> from bundle{} is empty; trivial merge", from.index());
return true;
}
if ranges_to.is_empty() {
// `to` bundle is empty -- just move the list over from
// `from` and set `bundle` up-link on all ranges.
log::trace!(" -> to bundle{} is empty; trivial merge", to.index());
let list = std::mem::replace(&mut self.bundles[from.index()].ranges, smallvec![]);
for entry in &list {
self.ranges[entry.index.index()].bundle = to;
if self.annotations_enabled {
self.annotate(
entry.range.from,
format!(
" MERGE range{} v{} from bundle{} to bundle{}",
entry.index.index(),
self.ranges[entry.index.index()].vreg.index(),
from.index(),
to.index(),
),
);
}
}
self.bundles[to.index()].ranges = list;
if self.bundles[from.index()].cached_stack() {
self.bundles[to.index()].set_cached_stack();
}
if self.bundles[from.index()].cached_fixed() {
self.bundles[to.index()].set_cached_fixed();
}
return true;
}
log::trace!(
"merging: ranges_from = {:?} ranges_to = {:?}",
ranges_from,
ranges_to
);
// Two non-empty lists of LiveRanges: concatenate and
// sort. This is faster than a mergesort-like merge into a new
// list, empirically.
let from_list = std::mem::replace(&mut self.bundles[from.index()].ranges, smallvec![]);
for entry in &from_list {
self.ranges[entry.index.index()].bundle = to;
}
self.bundles[to.index()]
.ranges
.extend_from_slice(&from_list[..]);
self.bundles[to.index()]
.ranges
.sort_unstable_by_key(|entry| entry.range.from);
if self.annotations_enabled {
log::trace!("merging: merged = {:?}", self.bundles[to.index()].ranges);
let mut last_range = None;
for i in 0..self.bundles[to.index()].ranges.len() {
let entry = self.bundles[to.index()].ranges[i];
if last_range.is_some() {
assert!(last_range.unwrap() < entry.range);
}
last_range = Some(entry.range);
if self.ranges[entry.index.index()].bundle == from {
self.annotate(
entry.range.from,
format!(
" MERGE range{} v{} from bundle{} to bundle{}",
entry.index.index(),
self.ranges[entry.index.index()].vreg.index(),
from.index(),
to.index(),
),
);
}
log::trace!(
" -> merged result for bundle{}: range{}",
to.index(),
entry.index.index(),
);
}
}
if self.bundles[from.index()].spillset != self.bundles[to.index()].spillset {
let from_vregs = std::mem::replace(
&mut self.spillsets[self.bundles[from.index()].spillset.index()].vregs,
smallvec![],
);
let to_vregs = &mut self.spillsets[self.bundles[to.index()].spillset.index()].vregs;
for vreg in from_vregs {
if !to_vregs.contains(&vreg) {
to_vregs.push(vreg);
}
}
}
if self.bundles[from.index()].cached_stack() {
self.bundles[to.index()].set_cached_stack();
}
if self.bundles[from.index()].cached_fixed() {
self.bundles[to.index()].set_cached_fixed();
}
true
}
pub fn merge_vreg_bundles(&mut self) {
// Create a bundle for every vreg, initially.
log::trace!("merge_vreg_bundles: creating vreg bundles");
for vreg in 0..self.vregs.len() {
let vreg = VRegIndex::new(vreg);
if self.vregs[vreg.index()].ranges.is_empty() {
continue;
}
// If this is a pinned vreg, go ahead and add it to the
// commitment map, and avoid creating a bundle entirely.
if self.vregs[vreg.index()].is_pinned {
for entry in &self.vregs[vreg.index()].ranges {
let preg = self
.func
.is_pinned_vreg(self.vreg_regs[vreg.index()])
.unwrap();
let key = LiveRangeKey::from_range(&entry.range);
self.pregs[preg.index()]
.allocations
.btree
.insert(key, LiveRangeIndex::invalid());
}
continue;
}
let bundle = self.create_bundle();
self.bundles[bundle.index()].ranges = self.vregs[vreg.index()].ranges.clone();
log::trace!("vreg v{} gets bundle{}", vreg.index(), bundle.index());
for entry in &self.bundles[bundle.index()].ranges {
log::trace!(
" -> with LR range{}: {:?}",
entry.index.index(),
entry.range
);
self.ranges[entry.index.index()].bundle = bundle;
}
let mut fixed = false;
let mut stack = false;
for entry in &self.bundles[bundle.index()].ranges {
for u in &self.ranges[entry.index.index()].uses {
if let OperandConstraint::FixedReg(_) = u.operand.constraint() {
fixed = true;
}
if let OperandConstraint::Stack = u.operand.constraint() {
stack = true;
}
if fixed && stack {
break;
}
}
}
if fixed {
self.bundles[bundle.index()].set_cached_fixed();
}
if stack {
self.bundles[bundle.index()].set_cached_stack();
}
// Create a spillslot for this bundle.
let ssidx = SpillSetIndex::new(self.spillsets.len());
let reg = self.vreg_regs[vreg.index()];
let size = self.func.spillslot_size(reg.class()) as u8;
self.spillsets.push(SpillSet {
vregs: smallvec![vreg],
slot: SpillSlotIndex::invalid(),
size,
required: false,
class: reg.class(),
reg_hint: PReg::invalid(),
spill_bundle: LiveBundleIndex::invalid(),
});
self.bundles[bundle.index()].spillset = ssidx;
}
for inst in 0..self.func.num_insts() {
let inst = Inst::new(inst);
// Attempt to merge Reuse-constraint operand outputs with the
// corresponding inputs.
for op in self.func.inst_operands(inst) {
if let OperandConstraint::Reuse(reuse_idx) = op.constraint() {
let src_vreg = op.vreg();
let dst_vreg = self.func.inst_operands(inst)[reuse_idx].vreg();
if self.vregs[src_vreg.vreg()].is_pinned
|| self.vregs[dst_vreg.vreg()].is_pinned
{
continue;
}
log::trace!(
"trying to merge reused-input def: src {} to dst {}",
src_vreg,
dst_vreg
);
let src_bundle =
self.ranges[self.vregs[src_vreg.vreg()].ranges[0].index.index()].bundle;
assert!(src_bundle.is_valid());
let dest_bundle =
self.ranges[self.vregs[dst_vreg.vreg()].ranges[0].index.index()].bundle;
assert!(dest_bundle.is_valid());
self.merge_bundles(/* from */ dest_bundle, /* to */ src_bundle);
}
}
}
// Attempt to merge blockparams with their inputs.
for i in 0..self.blockparam_outs.len() {
let (from_vreg, _, _, to_vreg) = self.blockparam_outs[i];
log::trace!(
"trying to merge blockparam v{} with input v{}",
to_vreg.index(),
from_vreg.index()
);
let to_bundle = self.ranges[self.vregs[to_vreg.index()].ranges[0].index.index()].bundle;
assert!(to_bundle.is_valid());
let from_bundle =
self.ranges[self.vregs[from_vreg.index()].ranges[0].index.index()].bundle;
assert!(from_bundle.is_valid());
log::trace!(
" -> from bundle{} to bundle{}",
from_bundle.index(),
to_bundle.index()
);
self.merge_bundles(from_bundle, to_bundle);
}
// Attempt to merge move srcs/dsts.
for i in 0..self.prog_move_merges.len() {
let (src, dst) = self.prog_move_merges[i];
log::trace!("trying to merge move src LR {:?} to dst LR {:?}", src, dst);
let src = self.resolve_merged_lr(src);
let dst = self.resolve_merged_lr(dst);
log::trace!(
"resolved LR-construction merging chains: move-merge is now src LR {:?} to dst LR {:?}",
src,
dst
);
let dst_vreg = self.vreg_regs[self.ranges[dst.index()].vreg.index()];
let src_vreg = self.vreg_regs[self.ranges[src.index()].vreg.index()];
if self.vregs[src_vreg.vreg()].is_pinned && self.vregs[dst_vreg.vreg()].is_pinned {
continue;
}
if self.vregs[src_vreg.vreg()].is_pinned {
let dest_bundle = self.ranges[dst.index()].bundle;
let spillset = self.bundles[dest_bundle.index()].spillset;
self.spillsets[spillset.index()].reg_hint =
self.func.is_pinned_vreg(src_vreg).unwrap();
continue;
}
if self.vregs[dst_vreg.vreg()].is_pinned {
let src_bundle = self.ranges[src.index()].bundle;
let spillset = self.bundles[src_bundle.index()].spillset;
self.spillsets[spillset.index()].reg_hint =
self.func.is_pinned_vreg(dst_vreg).unwrap();
continue;
}
let src_bundle = self.ranges[src.index()].bundle;
assert!(src_bundle.is_valid());
let dest_bundle = self.ranges[dst.index()].bundle;
assert!(dest_bundle.is_valid());
self.stats.prog_move_merge_attempt += 1;
if self.merge_bundles(/* from */ dest_bundle, /* to */ src_bundle) {
self.stats.prog_move_merge_success += 1;
}
}
log::trace!("done merging bundles");
}
pub fn resolve_merged_lr(&self, mut lr: LiveRangeIndex) -> LiveRangeIndex {
let mut iter = 0;
while iter < 100 && self.ranges[lr.index()].merged_into.is_valid() {
lr = self.ranges[lr.index()].merged_into;
iter += 1;
}
lr
}
pub fn compute_bundle_prio(&self, bundle: LiveBundleIndex) -> u32 {
// The priority is simply the total "length" -- the number of
// instructions covered by all LiveRanges.
let mut total = 0;
for entry in &self.bundles[bundle.index()].ranges {
total += entry.range.len() as u32;
}
total
}
pub fn queue_bundles(&mut self) {
for bundle in 0..self.bundles.len() {
log::trace!("enqueueing bundle{}", bundle);
if self.bundles[bundle].ranges.is_empty() {
log::trace!(" -> no ranges; skipping");
continue;
}
let bundle = LiveBundleIndex::new(bundle);
let prio = self.compute_bundle_prio(bundle);
log::trace!(" -> prio {}", prio);
self.bundles[bundle.index()].prio = prio;
self.recompute_bundle_properties(bundle);
self.allocation_queue
.insert(bundle, prio as usize, PReg::invalid());
}
self.stats.merged_bundle_count = self.allocation_queue.heap.len();
}
}

145
src/ion/mod.rs Normal file
View File

@@ -0,0 +1,145 @@
/*
* The following license applies to this file, which was initially
* derived from the files `js/src/jit/BacktrackingAllocator.h` and
* `js/src/jit/BacktrackingAllocator.cpp` in Mozilla Firefox:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Since the initial port, the design has been substantially evolved
* and optimized.
*/
//! Backtracking register allocator. See doc/DESIGN.md for details of
//! its design.
use crate::cfg::CFGInfo;
use crate::{Function, MachineEnv, Output, PReg, ProgPoint, RegAllocError, RegClass};
use std::collections::HashMap;
pub(crate) mod data_structures;
pub use data_structures::Stats;
use data_structures::*;
pub(crate) mod reg_traversal;
use reg_traversal::*;
pub(crate) mod requirement;
use requirement::*;
pub(crate) mod redundant_moves;
use redundant_moves::*;
pub(crate) mod liveranges;
use liveranges::*;
pub(crate) mod merge;
pub(crate) mod process;
use process::*;
pub(crate) mod dump;
pub(crate) mod moves;
pub(crate) mod spill;
pub(crate) mod stackmap;
impl<'a, F: Function> Env<'a, F> {
pub(crate) fn new(
func: &'a F,
env: &'a MachineEnv,
cfginfo: CFGInfo,
annotations_enabled: bool,
) -> Self {
let n = func.num_insts();
Self {
func,
env,
cfginfo,
liveins: Vec::with_capacity(func.num_blocks()),
liveouts: Vec::with_capacity(func.num_blocks()),
blockparam_outs: vec![],
blockparam_ins: vec![],
blockparam_allocs: vec![],
bundles: Vec::with_capacity(n),
ranges: Vec::with_capacity(4 * n),
spillsets: Vec::with_capacity(n),
vregs: Vec::with_capacity(n),
vreg_regs: Vec::with_capacity(n),
pregs: vec![],
allocation_queue: PrioQueue::new(),
clobbers: vec![],
safepoints: vec![],
safepoints_per_vreg: HashMap::new(),
spilled_bundles: vec![],
spillslots: vec![],
slots_by_size: vec![],
allocated_bundle_count: 0,
extra_spillslot: vec![None, None],
prog_move_srcs: Vec::with_capacity(n / 2),
prog_move_dsts: Vec::with_capacity(n / 2),
prog_move_merges: Vec::with_capacity(n / 2),
multi_fixed_reg_fixups: vec![],
inserted_moves: vec![],
edits: Vec::with_capacity(n),
allocs: Vec::with_capacity(4 * n),
inst_alloc_offsets: vec![],
num_spillslots: 0,
safepoint_slots: vec![],
stats: Stats::default(),
debug_annotations: std::collections::HashMap::new(),
annotations_enabled,
}
}
pub(crate) fn init(&mut self) -> Result<(), RegAllocError> {
self.create_pregs_and_vregs();
self.compute_liveness()?;
self.merge_vreg_bundles();
self.queue_bundles();
if log::log_enabled!(log::Level::Trace) {
self.dump_state();
}
Ok(())
}
pub(crate) fn run(&mut self) -> Result<(), RegAllocError> {
self.process_bundles()?;
self.try_allocating_regs_for_spilled_bundles();
self.allocate_spillslots();
self.apply_allocations_and_insert_moves();
self.resolve_inserted_moves();
self.compute_stackmaps();
Ok(())
}
}
pub fn run<F: Function>(
func: &F,
mach_env: &MachineEnv,
enable_annotations: bool,
) -> Result<Output, RegAllocError> {
let cfginfo = CFGInfo::new(func)?;
let mut env = Env::new(func, mach_env, cfginfo, enable_annotations);
env.init()?;
env.run()?;
if enable_annotations {
env.dump_results();
}
Ok(Output {
edits: env
.edits
.into_iter()
.map(|(pos, _, edit)| (ProgPoint::from_index(pos), edit))
.collect(),
allocs: env.allocs,
inst_alloc_offsets: env.inst_alloc_offsets,
num_spillslots: env.num_spillslots as usize,
debug_locations: vec![],
safepoint_slots: env.safepoint_slots,
stats: env.stats,
})
}

1164
src/ion/moves.rs Normal file

File diff suppressed because it is too large Load Diff

1079
src/ion/process.rs Normal file

File diff suppressed because it is too large Load Diff

142
src/ion/redundant_moves.rs Normal file
View File

@@ -0,0 +1,142 @@
//! Redundant-move elimination.
use crate::{Allocation, VReg};
use fxhash::FxHashMap;
use smallvec::{smallvec, SmallVec};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RedundantMoveState {
Copy(Allocation, Option<VReg>),
Orig(VReg),
None,
}
#[derive(Clone, Debug, Default)]
pub struct RedundantMoveEliminator {
allocs: FxHashMap<Allocation, RedundantMoveState>,
reverse_allocs: FxHashMap<Allocation, SmallVec<[Allocation; 4]>>,
}
#[derive(Copy, Clone, Debug)]
pub struct RedundantMoveAction {
pub elide: bool,
pub def_alloc: Option<(Allocation, VReg)>,
}
impl RedundantMoveEliminator {
pub fn process_move(
&mut self,
from: Allocation,
to: Allocation,
to_vreg: Option<VReg>,
) -> RedundantMoveAction {
// Look up the src and dest.
let from_state = self
.allocs
.get(&from)
.map(|&p| p)
.unwrap_or(RedundantMoveState::None);
let to_state = self
.allocs
.get(&to)
.map(|&p| p)
.unwrap_or(RedundantMoveState::None);
log::trace!(
" -> redundant move tracker: from {} to {} to_vreg {:?}",
from,
to,
to_vreg
);
log::trace!(
" -> from_state {:?} to_state {:?}",
from_state,
to_state
);
if from == to && to_vreg.is_some() {
self.clear_alloc(to);
self.allocs
.insert(to, RedundantMoveState::Orig(to_vreg.unwrap()));
return RedundantMoveAction {
elide: true,
def_alloc: Some((to, to_vreg.unwrap())),
};
}
let src_vreg = match from_state {
RedundantMoveState::Copy(_, opt_r) => opt_r,
RedundantMoveState::Orig(r) => Some(r),
_ => None,
};
log::trace!(" -> src_vreg {:?}", src_vreg);
let dst_vreg = to_vreg.or(src_vreg);
log::trace!(" -> dst_vreg {:?}", dst_vreg);
let existing_dst_vreg = match to_state {
RedundantMoveState::Copy(_, opt_r) => opt_r,
RedundantMoveState::Orig(r) => Some(r),
_ => None,
};
log::trace!(" -> existing_dst_vreg {:?}", existing_dst_vreg);
let elide = match (from_state, to_state) {
(_, RedundantMoveState::Copy(orig_alloc, _)) if orig_alloc == from => true,
(RedundantMoveState::Copy(new_alloc, _), _) if new_alloc == to => true,
_ => false,
};
log::trace!(" -> elide {}", elide);
let def_alloc = if dst_vreg != existing_dst_vreg && dst_vreg.is_some() {
Some((to, dst_vreg.unwrap()))
} else {
None
};
log::trace!(" -> def_alloc {:?}", def_alloc);
// Invalidate all existing copies of `to` if `to` actually changed value.
if !elide {
self.clear_alloc(to);
}
// Set up forward and reverse mapping. Don't track stack-to-stack copies.
if from.is_reg() || to.is_reg() {
self.allocs
.insert(to, RedundantMoveState::Copy(from, dst_vreg));
log::trace!(
" -> create mapping {} -> {:?}",
to,
RedundantMoveState::Copy(from, dst_vreg)
);
self.reverse_allocs
.entry(from)
.or_insert_with(|| smallvec![])
.push(to);
}
RedundantMoveAction { elide, def_alloc }
}
pub fn clear(&mut self) {
log::trace!(" redundant move eliminator cleared");
self.allocs.clear();
self.reverse_allocs.clear();
}
pub fn clear_alloc(&mut self, alloc: Allocation) {
log::trace!(" redundant move eliminator: clear {:?}", alloc);
if let Some(ref mut existing_copies) = self.reverse_allocs.get_mut(&alloc) {
for to_inval in existing_copies.iter() {
log::trace!(" -> clear existing copy: {:?}", to_inval);
if let Some(val) = self.allocs.get_mut(to_inval) {
match val {
RedundantMoveState::Copy(_, Some(vreg)) => {
*val = RedundantMoveState::Orig(*vreg);
}
_ => *val = RedundantMoveState::None,
}
}
self.allocs.remove(to_inval);
}
existing_copies.clear();
}
self.allocs.remove(&alloc);
}
}

123
src/ion/reg_traversal.rs Normal file
View File

@@ -0,0 +1,123 @@
use crate::{MachineEnv, PReg, RegClass};
/// This iterator represents a traversal through all allocatable
/// registers of a given class, in a certain order designed to
/// minimize allocation contention.
///
/// The order in which we try registers is somewhat complex:
/// - First, if there is a hint, we try that.
/// - Then, we try registers in a traversal order that is based on an
/// "offset" (usually the bundle index) spreading pressure evenly
/// among registers to reduce commitment-map contention.
/// - Within that scan, we try registers in two groups: first,
/// prferred registers; then, non-preferred registers. (In normal
/// usage, these consist of caller-save and callee-save registers
/// respectively, to minimize clobber-saves; but they need not.)
pub struct RegTraversalIter<'a> {
env: &'a MachineEnv,
class: usize,
hints: [Option<PReg>; 2],
hint_idx: usize,
pref_idx: usize,
non_pref_idx: usize,
offset_pref: usize,
offset_non_pref: usize,
is_fixed: bool,
fixed: Option<PReg>,
}
impl<'a> RegTraversalIter<'a> {
pub fn new(
env: &'a MachineEnv,
class: RegClass,
hint_reg: PReg,
hint2_reg: PReg,
offset: usize,
fixed: Option<PReg>,
) -> Self {
let mut hint_reg = if hint_reg != PReg::invalid() {
Some(hint_reg)
} else {
None
};
let mut hint2_reg = if hint2_reg != PReg::invalid() {
Some(hint2_reg)
} else {
None
};
if hint_reg.is_none() {
hint_reg = hint2_reg;
hint2_reg = None;
}
let hints = [hint_reg, hint2_reg];
let class = class as u8 as usize;
let offset_pref = if env.preferred_regs_by_class[class].len() > 0 {
offset % env.preferred_regs_by_class[class].len()
} else {
0
};
let offset_non_pref = if env.non_preferred_regs_by_class[class].len() > 0 {
offset % env.non_preferred_regs_by_class[class].len()
} else {
0
};
Self {
env,
class,
hints,
hint_idx: 0,
pref_idx: 0,
non_pref_idx: 0,
offset_pref,
offset_non_pref,
is_fixed: fixed.is_some(),
fixed,
}
}
}
impl<'a> std::iter::Iterator for RegTraversalIter<'a> {
type Item = PReg;
fn next(&mut self) -> Option<PReg> {
if self.is_fixed {
let ret = self.fixed;
self.fixed = None;
return ret;
}
fn wrap(idx: usize, limit: usize) -> usize {
if idx >= limit {
idx - limit
} else {
idx
}
}
if self.hint_idx < 2 && self.hints[self.hint_idx].is_some() {
let h = self.hints[self.hint_idx];
self.hint_idx += 1;
return h;
}
while self.pref_idx < self.env.preferred_regs_by_class[self.class].len() {
let arr = &self.env.preferred_regs_by_class[self.class][..];
let r = arr[wrap(self.pref_idx + self.offset_pref, arr.len())];
self.pref_idx += 1;
if Some(r) == self.hints[0] || Some(r) == self.hints[1] {
continue;
}
return Some(r);
}
while self.non_pref_idx < self.env.non_preferred_regs_by_class[self.class].len() {
let arr = &self.env.non_preferred_regs_by_class[self.class][..];
let r = arr[wrap(self.non_pref_idx + self.offset_non_pref, arr.len())];
self.non_pref_idx += 1;
if Some(r) == self.hints[0] || Some(r) == self.hints[1] {
continue;
}
return Some(r);
}
None
}
}

94
src/ion/requirement.rs Normal file
View File

@@ -0,0 +1,94 @@
//! Requirements computation.
use super::{Env, LiveBundleIndex};
use crate::{Function, Operand, OperandConstraint, PReg, RegClass};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Requirement {
Unknown,
Fixed(PReg),
Register(RegClass),
Stack(RegClass),
Any(RegClass),
Conflict,
}
impl Requirement {
#[inline(always)]
pub fn class(self) -> RegClass {
match self {
Requirement::Unknown => panic!("No class for unknown Requirement"),
Requirement::Fixed(preg) => preg.class(),
Requirement::Register(class) | Requirement::Any(class) | Requirement::Stack(class) => {
class
}
Requirement::Conflict => panic!("No class for conflicted Requirement"),
}
}
#[inline(always)]
pub fn merge(self, other: Requirement) -> Requirement {
match (self, other) {
(Requirement::Unknown, other) | (other, Requirement::Unknown) => other,
(Requirement::Conflict, _) | (_, Requirement::Conflict) => Requirement::Conflict,
(other, Requirement::Any(rc)) | (Requirement::Any(rc), other) => {
if other.class() == rc {
other
} else {
Requirement::Conflict
}
}
(Requirement::Stack(rc1), Requirement::Stack(rc2)) => {
if rc1 == rc2 {
self
} else {
Requirement::Conflict
}
}
(Requirement::Register(rc), Requirement::Fixed(preg))
| (Requirement::Fixed(preg), Requirement::Register(rc)) => {
if rc == preg.class() {
Requirement::Fixed(preg)
} else {
Requirement::Conflict
}
}
(Requirement::Register(rc1), Requirement::Register(rc2)) => {
if rc1 == rc2 {
self
} else {
Requirement::Conflict
}
}
(Requirement::Fixed(a), Requirement::Fixed(b)) if a == b => self,
_ => Requirement::Conflict,
}
}
#[inline(always)]
pub fn from_operand(op: Operand) -> Requirement {
match op.constraint() {
OperandConstraint::FixedReg(preg) => Requirement::Fixed(preg),
OperandConstraint::Reg | OperandConstraint::Reuse(_) => {
Requirement::Register(op.class())
}
OperandConstraint::Stack => Requirement::Stack(op.class()),
_ => Requirement::Any(op.class()),
}
}
}
impl<'a, F: Function> Env<'a, F> {
pub fn compute_requirement(&self, bundle: LiveBundleIndex) -> Requirement {
let mut req = Requirement::Unknown;
log::trace!("compute_requirement: {:?}", bundle);
for entry in &self.bundles[bundle.index()].ranges {
log::trace!(" -> LR {:?}", entry.index);
for u in &self.ranges[entry.index.index()].uses {
log::trace!(" -> use {:?}", u);
let r = Requirement::from_operand(u.operand);
req = req.merge(r);
log::trace!(" -> req {:?}", req);
}
}
log::trace!(" -> final: {:?}", req);
req
}
}

218
src/ion/spill.rs Normal file
View File

@@ -0,0 +1,218 @@
/*
* The following license applies to this file, which was initially
* derived from the files `js/src/jit/BacktrackingAllocator.h` and
* `js/src/jit/BacktrackingAllocator.cpp` in Mozilla Firefox:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Since the initial port, the design has been substantially evolved
* and optimized.
*/
//! Spillslot allocation.
use super::{
AllocRegResult, Env, LiveRangeKey, LiveRangeSet, PReg, PRegIndex, RegClass, RegTraversalIter,
SpillSetIndex, SpillSlotData, SpillSlotIndex, SpillSlotList,
};
use crate::{Allocation, Function, SpillSlot};
impl<'a, F: Function> Env<'a, F> {
pub fn try_allocating_regs_for_spilled_bundles(&mut self) {
log::trace!("allocating regs for spilled bundles");
for i in 0..self.spilled_bundles.len() {
let bundle = self.spilled_bundles[i]; // don't borrow self
let class = self.spillsets[self.bundles[bundle.index()].spillset.index()].class;
let hint = self.spillsets[self.bundles[bundle.index()].spillset.index()].reg_hint;
// This may be an empty-range bundle whose ranges are not
// sorted; sort all range-lists again here.
self.bundles[bundle.index()]
.ranges
.sort_unstable_by_key(|entry| entry.range.from);
let mut success = false;
self.stats.spill_bundle_reg_probes += 1;
for preg in
RegTraversalIter::new(self.env, class, hint, PReg::invalid(), bundle.index(), None)
{
log::trace!("trying bundle {:?} to preg {:?}", bundle, preg);
let preg_idx = PRegIndex::new(preg.index());
if let AllocRegResult::Allocated(_) =
self.try_to_allocate_bundle_to_reg(bundle, preg_idx, None)
{
self.stats.spill_bundle_reg_success += 1;
success = true;
break;
}
}
if !success {
log::trace!(
"spilling bundle {:?}: marking spillset {:?} as required",
bundle,
self.bundles[bundle.index()].spillset
);
self.spillsets[self.bundles[bundle.index()].spillset.index()].required = true;
}
}
}
pub fn spillslot_can_fit_spillset(
&mut self,
spillslot: SpillSlotIndex,
spillset: SpillSetIndex,
) -> bool {
for &vreg in &self.spillsets[spillset.index()].vregs {
for entry in &self.vregs[vreg.index()].ranges {
if self.spillslots[spillslot.index()]
.ranges
.btree
.contains_key(&LiveRangeKey::from_range(&entry.range))
{
return false;
}
}
}
true
}
pub fn allocate_spillset_to_spillslot(
&mut self,
spillset: SpillSetIndex,
spillslot: SpillSlotIndex,
) {
self.spillsets[spillset.index()].slot = spillslot;
for i in 0..self.spillsets[spillset.index()].vregs.len() {
// don't borrow self
let vreg = self.spillsets[spillset.index()].vregs[i];
log::trace!(
"spillslot {:?} alloc'ed to spillset {:?}: vreg {:?}",
spillslot,
spillset,
vreg,
);
for entry in &self.vregs[vreg.index()].ranges {
log::trace!(
"spillslot {:?} getting range {:?} from LR {:?} from vreg {:?}",
spillslot,
entry.range,
entry.index,
vreg,
);
self.spillslots[spillslot.index()]
.ranges
.btree
.insert(LiveRangeKey::from_range(&entry.range), entry.index);
}
}
}
pub fn allocate_spillslots(&mut self) {
for spillset in 0..self.spillsets.len() {
log::trace!("allocate spillslot: {}", spillset);
let spillset = SpillSetIndex::new(spillset);
if !self.spillsets[spillset.index()].required {
continue;
}
// Get or create the spillslot list for this size.
let size = self.spillsets[spillset.index()].size as usize;
if size >= self.slots_by_size.len() {
self.slots_by_size.resize(
size + 1,
SpillSlotList {
first_spillslot: SpillSlotIndex::invalid(),
last_spillslot: SpillSlotIndex::invalid(),
},
);
}
// Try a few existing spillslots.
let mut spillslot_iter = self.slots_by_size[size].first_spillslot;
let mut first_slot = SpillSlotIndex::invalid();
let mut prev = SpillSlotIndex::invalid();
let mut success = false;
for _attempt in 0..10 {
if spillslot_iter.is_invalid() {
break;
}
if spillslot_iter == first_slot {
// We've started looking at slots we placed at the end; end search.
break;
}
if first_slot.is_invalid() {
first_slot = spillslot_iter;
}
if self.spillslot_can_fit_spillset(spillslot_iter, spillset) {
self.allocate_spillset_to_spillslot(spillset, spillslot_iter);
success = true;
break;
}
// Remove the slot and place it at the end of the respective list.
let next = self.spillslots[spillslot_iter.index()].next_spillslot;
if prev.is_valid() {
self.spillslots[prev.index()].next_spillslot = next;
} else {
self.slots_by_size[size].first_spillslot = next;
}
if !next.is_valid() {
self.slots_by_size[size].last_spillslot = prev;
}
let last = self.slots_by_size[size].last_spillslot;
if last.is_valid() {
self.spillslots[last.index()].next_spillslot = spillslot_iter;
} else {
self.slots_by_size[size].first_spillslot = spillslot_iter;
}
self.slots_by_size[size].last_spillslot = spillslot_iter;
prev = spillslot_iter;
spillslot_iter = next;
}
if !success {
// Allocate a new spillslot.
let spillslot = SpillSlotIndex::new(self.spillslots.len());
let next = self.slots_by_size[size].first_spillslot;
self.spillslots.push(SpillSlotData {
ranges: LiveRangeSet::new(),
next_spillslot: next,
alloc: Allocation::none(),
class: self.spillsets[spillset.index()].class,
});
self.slots_by_size[size].first_spillslot = spillslot;
if !next.is_valid() {
self.slots_by_size[size].last_spillslot = spillslot;
}
self.allocate_spillset_to_spillslot(spillset, spillslot);
}
}
// Assign actual slot indices to spillslots.
for i in 0..self.spillslots.len() {
self.spillslots[i].alloc = self.allocate_spillslot(self.spillslots[i].class);
}
log::trace!("spillslot allocator done");
}
pub fn allocate_spillslot(&mut self, class: RegClass) -> Allocation {
let size = self.func.spillslot_size(class) as u32;
let mut offset = self.num_spillslots;
// Align up to `size`.
debug_assert!(size.is_power_of_two());
offset = (offset + size - 1) & !(size - 1);
let slot = if self.func.multi_spillslot_named_by_last_slot() {
offset + size - 1
} else {
offset
};
offset += size;
self.num_spillslots = offset;
Allocation::stack(SpillSlot::new(slot as usize, class))
}
}

73
src/ion/stackmap.rs Normal file
View File

@@ -0,0 +1,73 @@
/*
* The following license applies to this file, which was initially
* derived from the files `js/src/jit/BacktrackingAllocator.h` and
* `js/src/jit/BacktrackingAllocator.cpp` in Mozilla Firefox:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Since the initial port, the design has been substantially evolved
* and optimized.
*/
//! Stackmap computation.
use super::{Env, ProgPoint, VRegIndex};
use crate::Function;
impl<'a, F: Function> Env<'a, F> {
pub fn compute_stackmaps(&mut self) {
// For each ref-typed vreg, iterate through ranges and find
// safepoints in-range. Add the SpillSlot to the stackmap.
if self.func.reftype_vregs().is_empty() {
return;
}
// Given `safepoints_per_vreg` from the liveness computation,
// all we have to do is, for each vreg in this map, step
// through the LiveRanges along with a sorted list of
// safepoints; and for each safepoint in the current range,
// emit the allocation into the `safepoint_slots` list.
log::trace!("safepoints_per_vreg = {:?}", self.safepoints_per_vreg);
for vreg in self.func.reftype_vregs() {
log::trace!("generating safepoint info for vreg {}", vreg);
let vreg = VRegIndex::new(vreg.vreg());
let mut safepoints: Vec<ProgPoint> = self
.safepoints_per_vreg
.get(&vreg.index())
.unwrap()
.iter()
.map(|&inst| ProgPoint::before(inst))
.collect();
safepoints.sort_unstable();
log::trace!(" -> live over safepoints: {:?}", safepoints);
let mut safepoint_idx = 0;
for entry in &self.vregs[vreg.index()].ranges {
let range = entry.range;
let alloc = self.get_alloc_for_range(entry.index);
log::trace!(" -> range {:?}: alloc {}", range, alloc);
while safepoint_idx < safepoints.len() && safepoints[safepoint_idx] < range.to {
if safepoints[safepoint_idx] < range.from {
safepoint_idx += 1;
continue;
}
log::trace!(" -> covers safepoint {:?}", safepoints[safepoint_idx]);
let slot = alloc
.as_stack()
.expect("Reference-typed value not in spillslot at safepoint");
self.safepoint_slots.push((safepoints[safepoint_idx], slot));
safepoint_idx += 1;
}
}
}
self.safepoint_slots.sort_unstable();
log::trace!("final safepoint slots info: {:?}", self.safepoint_slots);
}
}

1262
src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

206
src/moves.rs Normal file
View File

@@ -0,0 +1,206 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
use crate::Allocation;
use smallvec::{smallvec, SmallVec};
pub type MoveVec<T> = SmallVec<[(Allocation, Allocation, T); 16]>;
/// A `ParallelMoves` represents a list of alloc-to-alloc moves that
/// must happen in parallel -- i.e., all reads of sources semantically
/// happen before all writes of destinations, and destinations are
/// allowed to overwrite sources. It can compute a list of sequential
/// moves that will produce the equivalent data movement, possibly
/// using a scratch register if one is necessary.
pub struct ParallelMoves<T: Clone + Copy + Default> {
parallel_moves: MoveVec<T>,
scratch: Allocation,
}
impl<T: Clone + Copy + Default> ParallelMoves<T> {
pub fn new(scratch: Allocation) -> Self {
Self {
parallel_moves: smallvec![],
scratch,
}
}
pub fn add(&mut self, from: Allocation, to: Allocation, t: T) {
self.parallel_moves.push((from, to, t));
}
fn sources_overlap_dests(&self) -> bool {
// Assumes `parallel_moves` has already been sorted in `resolve()` below.
for &(_, dst, _) in &self.parallel_moves {
if self
.parallel_moves
.binary_search_by_key(&dst, |&(src, _, _)| src)
.is_ok()
{
return true;
}
}
false
}
pub fn resolve(mut self) -> MoveVec<T> {
// Easy case: zero or one move. Just return our vec.
if self.parallel_moves.len() <= 1 {
return self.parallel_moves;
}
// Sort moves by source so that we can efficiently test for
// presence.
self.parallel_moves.sort_by_key(|&(src, dst, _)| (src, dst));
// Do any dests overlap sources? If not, we can also just
// return the list.
if !self.sources_overlap_dests() {
return self.parallel_moves;
}
// General case: some moves overwrite dests that other moves
// read as sources. We'll use a general algorithm.
//
// *Important property*: because we expect that each register
// has only one writer (otherwise the effect of the parallel
// move is undefined), each move can only block one other move
// (with its one source corresponding to the one writer of
// that source). Thus, we *can only have simple cycles* (those
// that are a ring of nodes, i.e., with only one path from a
// node back to itself); there are no SCCs that are more
// complex than that. We leverage this fact below to avoid
// having to do a full Tarjan SCC DFS (with lowest-index
// computation, etc.): instead, as soon as we find a cycle, we
// know we have the full cycle and we can do a cyclic move
// sequence and continue.
// Sort moves by destination and check that each destination
// has only one writer.
self.parallel_moves.sort_by_key(|&(_, dst, _)| dst);
if cfg!(debug_assertions) {
let mut last_dst = None;
for &(_, dst, _) in &self.parallel_moves {
if last_dst.is_some() {
debug_assert!(last_dst.unwrap() != dst);
}
last_dst = Some(dst);
}
}
// Construct a mapping from move indices to moves they must
// come before. Any given move must come before a move that
// overwrites its destination; we have moves sorted by dest
// above so we can efficiently find such a move, if any.
let mut must_come_before: SmallVec<[Option<usize>; 16]> =
smallvec![None; self.parallel_moves.len()];
for (i, &(src, _, _)) in self.parallel_moves.iter().enumerate() {
if let Ok(move_to_dst_idx) = self
.parallel_moves
.binary_search_by_key(&src, |&(_, dst, _)| dst)
{
must_come_before[i] = Some(move_to_dst_idx);
}
}
// Do a simple stack-based DFS and emit moves in postorder,
// then reverse at the end for RPO. Unlike Tarjan's SCC
// algorithm, we can emit a cycle as soon as we find one, as
// noted above.
let mut ret: MoveVec<T> = smallvec![];
let mut stack: SmallVec<[usize; 16]> = smallvec![];
let mut visited: SmallVec<[bool; 16]> = smallvec![false; self.parallel_moves.len()];
let mut onstack: SmallVec<[bool; 16]> = smallvec![false; self.parallel_moves.len()];
stack.push(0);
onstack[0] = true;
loop {
if stack.is_empty() {
if let Some(next) = visited.iter().position(|&flag| !flag) {
stack.push(next);
onstack[next] = true;
} else {
break;
}
}
let top = *stack.last().unwrap();
visited[top] = true;
match must_come_before[top] {
None => {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
stack.pop();
while let Some(top) = stack.pop() {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
}
}
Some(next) if visited[next] && !onstack[next] => {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
stack.pop();
while let Some(top) = stack.pop() {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
}
}
Some(next) if !visited[next] && !onstack[next] => {
stack.push(next);
onstack[next] = true;
continue;
}
Some(next) => {
// Found a cycle -- emit a cyclic-move sequence
// for the cycle on the top of stack, then normal
// moves below it. Recall that these moves will be
// reversed in sequence, so from the original
// parallel move set
//
// { B := A, C := B, A := B }
//
// we will generate something like:
//
// A := scratch
// B := A
// C := B
// scratch := C
//
// which will become:
//
// scratch := C
// C := B
// B := A
// A := scratch
let mut last_dst = None;
let mut scratch_src = None;
while let Some(move_idx) = stack.pop() {
onstack[move_idx] = false;
let (mut src, dst, dst_t) = self.parallel_moves[move_idx];
if last_dst.is_none() {
scratch_src = Some(src);
src = self.scratch;
} else {
assert_eq!(last_dst.unwrap(), src);
}
ret.push((src, dst, dst_t));
last_dst = Some(dst);
if move_idx == next {
break;
}
}
if let Some(src) = scratch_src {
ret.push((src, self.scratch, T::default()));
}
}
}
}
ret.reverse();
ret
}
}

56
src/postorder.rs Normal file
View File

@@ -0,0 +1,56 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
//! Fast postorder computation.
use crate::Block;
use smallvec::{smallvec, SmallVec};
pub fn calculate<'a, SuccFn: Fn(Block) -> &'a [Block]>(
num_blocks: usize,
entry: Block,
succ_blocks: SuccFn,
) -> Vec<Block> {
let mut ret = vec![];
// State: visited-block map, and explicit DFS stack.
let mut visited = vec![];
visited.resize(num_blocks, false);
struct State<'a> {
block: Block,
succs: &'a [Block],
next_succ: usize,
}
let mut stack: SmallVec<[State; 64]> = smallvec![];
visited[entry.index()] = true;
stack.push(State {
block: entry,
succs: succ_blocks(entry),
next_succ: 0,
});
while let Some(ref mut state) = stack.last_mut() {
// Perform one action: push to new succ, skip an already-visited succ, or pop.
if state.next_succ < state.succs.len() {
let succ = state.succs[state.next_succ];
state.next_succ += 1;
if !visited[succ.index()] {
visited[succ.index()] = true;
stack.push(State {
block: succ,
succs: succ_blocks(succ),
next_succ: 0,
});
}
} else {
ret.push(state.block);
stack.pop();
}
}
ret
}

98
src/ssa.rs Normal file
View File

@@ -0,0 +1,98 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
//! SSA-related utilities.
use crate::cfg::CFGInfo;
use crate::{Block, Function, Inst, OperandKind, RegAllocError};
pub fn validate_ssa<F: Function>(f: &F, cfginfo: &CFGInfo) -> Result<(), RegAllocError> {
// Walk the blocks in arbitrary order. Check, for every use, that
// the def is either in the same block in an earlier inst, or is
// defined (by inst or blockparam) in some other block that
// dominates this one. Also check that for every block param and
// inst def, that this is the only def.
let mut defined = vec![false; f.num_vregs()];
for block in 0..f.num_blocks() {
let block = Block::new(block);
for blockparam in f.block_params(block) {
if defined[blockparam.vreg()] {
return Err(RegAllocError::SSA(*blockparam, Inst::invalid()));
}
defined[blockparam.vreg()] = true;
}
for iix in f.block_insns(block).iter() {
let operands = f.inst_operands(iix);
for operand in operands {
match operand.kind() {
OperandKind::Use => {
let def_block = if cfginfo.vreg_def_inst[operand.vreg().vreg()].is_valid() {
cfginfo.insn_block[cfginfo.vreg_def_inst[operand.vreg().vreg()].index()]
} else {
cfginfo.vreg_def_blockparam[operand.vreg().vreg()].0
};
if def_block.is_invalid() {
return Err(RegAllocError::SSA(operand.vreg(), iix));
}
if !cfginfo.dominates(def_block, block) {
return Err(RegAllocError::SSA(operand.vreg(), iix));
}
}
OperandKind::Def => {
if defined[operand.vreg().vreg()] {
return Err(RegAllocError::SSA(operand.vreg(), iix));
}
defined[operand.vreg().vreg()] = true;
}
OperandKind::Mod => {
// Mod (modify) operands are not used in SSA,
// but can be used by non-SSA code (e.g. with
// the regalloc.rs compatibility shim).
return Err(RegAllocError::SSA(operand.vreg(), iix));
}
}
}
}
}
// Check that the length of branch args matches the sum of the
// number of blockparams in their succs, and that the end of every
// block ends in this branch or in a ret, and that there are no
// other branches or rets in the middle of the block.
for block in 0..f.num_blocks() {
let block = Block::new(block);
let insns = f.block_insns(block);
for insn in insns.iter() {
if insn == insns.last() {
if !(f.is_branch(insn) || f.is_ret(insn)) {
return Err(RegAllocError::BB(block));
}
if f.is_branch(insn) {
let expected = f
.block_succs(block)
.iter()
.map(|&succ| f.block_params(succ).len())
.sum();
if f.inst_operands(insn).len() != expected {
return Err(RegAllocError::Branch(insn));
}
}
} else {
if f.is_branch(insn) || f.is_ret(insn) {
return Err(RegAllocError::BB(block));
}
}
}
}
// Check that the entry block has no block args: otherwise it is
// undefined what their value would be.
if f.block_params(f.entry_block()).len() > 0 {
return Err(RegAllocError::BB(f.entry_block()));
}
Ok(())
}

28
test/Cargo.toml Normal file
View File

@@ -0,0 +1,28 @@
[package]
name = "regalloc2-test"
version = "0.0.1"
authors = ["Chris Fallin <chris@cfallin.org>", "Mozilla SpiderMonkey Developers"]
edition = "2018"
license = "Apache-2.0 WITH LLVM-exception AND MPL-2.0"
description = "small test driver for benchmarking regalloc2"
repository = "https://github.com/bytecodealliance/regalloc2"
[dependencies]
regalloc2 = { version = "*", path = "../", features = ["fuzzing"] }
# Keep this in sync with libfuzzer_sys's crate version:
arbitrary = { version = "^0.4.6" }
rand = { version = "0.8" }
rand_chacha = { version = "0.3" }
env_logger = { version = "*" }
[dev-dependencies]
criterion = "0.3"
[profile.release]
debug = true
[[bench]]
name = "regalloc"
harness = false

56
test/benches/regalloc.rs Normal file
View File

@@ -0,0 +1,56 @@
//! Criterion-based benchmark target that computes insts/second for
//! arbitrary inputs.
use arbitrary::{Arbitrary, Unstructured};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use regalloc2::fuzzing::func::{machine_env, Func};
use regalloc2::ion;
use regalloc2::Function;
fn create_random_func(seed: u64, size: usize) -> Func {
let mut bytes: Vec<u8> = vec![];
bytes.resize(size, 0);
let mut rng = ChaCha8Rng::seed_from_u64(seed);
rng.fill(&mut bytes[..]);
loop {
let mut u = Unstructured::new(&bytes[..]);
match Func::arbitrary(&mut u) {
Ok(f) => {
return f;
}
Err(arbitrary::Error::NotEnoughData) => {
let len = bytes.len();
bytes.resize(len + 1024, 0);
rng.fill(&mut bytes[len..]);
}
Err(e) => panic!("unexpected error: {:?}", e),
}
}
}
fn run_regalloc(c: &mut Criterion) {
const SIZE: usize = 1000 * 1000;
env_logger::init();
let env = machine_env();
let mut group = c.benchmark_group("benches");
for iter in 0..3 {
let func = create_random_func(iter, SIZE);
eprintln!("==== {} instructions", func.insts());
group.throughput(Throughput::Elements(func.insts() as u64));
group.bench_with_input(BenchmarkId::from_parameter(iter), &iter, |b, _| {
b.iter(|| {
// For fair comparison with regalloc.rs, which needs
// to clone its Func on every alloc, we clone
// too. Seems to make a few percent difference.
let func = func.clone();
ion::run(&func, &env).expect("regalloc did not succeed");
});
});
}
group.finish();
}
criterion_group!(benches, run_regalloc);
criterion_main!(benches);

50
test/src/main.rs Normal file
View File

@@ -0,0 +1,50 @@
/*
* Released under the terms of the Apache 2.0 license with LLVM
* exception. See `LICENSE` for details.
*/
use arbitrary::{Arbitrary, Unstructured};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use regalloc2::fuzzing::func::{machine_env, Func};
use regalloc2::ion;
use regalloc2::Function;
fn create_random_func(seed: u64, size: usize) -> Func {
let mut bytes: Vec<u8> = vec![];
bytes.resize(size, 0);
let mut rng = ChaCha8Rng::seed_from_u64(seed);
rng.fill(&mut bytes[..]);
loop {
let mut u = Unstructured::new(&bytes[..]);
match Func::arbitrary(&mut u) {
Ok(f) => {
return f;
}
Err(arbitrary::Error::NotEnoughData) => {
let len = bytes.len();
bytes.resize(len + 1024, 0);
rng.fill(&mut bytes[len..]);
}
Err(e) => panic!("unexpected error: {:?}", e),
}
}
}
fn main() {
const SIZE: usize = 1000 * 1000;
env_logger::init();
let env = machine_env();
for iter in 0..3 {
let func = create_random_func(iter, SIZE);
eprintln!("==== {} instructions", func.insts());
let mut stats: ion::Stats = ion::Stats::default();
for i in 0..1000 {
let out = ion::run(&func, &env).expect("regalloc did not succeed");
if i == 0 {
stats = out.stats;
}
}
eprintln!("Stats: {:?}", stats);
}
}