Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
266 changes: 110 additions & 156 deletions crates/cranelift/src/alias_region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use cranelift_codegen::{
cursor::FuncCursor,
ir::{self, InstBuilder as _},
};
use std::hash::{Hash as _, Hasher};
use wasmtime_environ::{
BuiltinFunctionIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, GetPtrSize,
ModuleInternedTypeIndex, NUM_COMPONENT_CONTEXT_SLOTS, PtrSize as _, RuntimeDataIndex,
Expand Down Expand Up @@ -85,9 +86,6 @@ enum VmType {
///
/// This is used to assign stable `user_id`s to `AliasRegionData` entries so
/// that alias regions can be deduplicated during inlining.
///
/// The key encodes into a single `u32` with the following layout:
/// `[ kind: 6 bits | data: 26 bits ]`
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum AliasRegionKey {
/// An access of a field within a VM data structure of type `ty`.
Expand Down Expand Up @@ -161,143 +159,109 @@ enum AliasRegionKey {
}

impl AliasRegionKey {
const KIND_BITS: u32 = 6;
const KIND_OFFSET: u32 = 32 - Self::KIND_BITS;
const KIND_MASK: u32 = ((1 << Self::KIND_BITS) - 1) << Self::KIND_OFFSET;
/// Encode this key into a raw `u32` suitable for use as an
/// `AliasRegionData::user_id`.
///
/// We lossily encode the key into a `user_id` via a single-byte hash, so
/// there are at most 256 alias regions. This avoids compile-time blowups
/// associated with many alias regions.
///
/// Note that mapping multiple alias regions onto the same `user_id` is
/// always okay: by collapsing two logical regions into one actual region,
/// we are letting Cranelift believe they *might* alias even when they
/// actually cannot. At worst this prevents some optimization. The opposite,
/// however -- if we were to place accesses that *could* alias in two
/// different regions -- is not sound. That would let Cranelift optimize
/// based on a false premise, leading to misoptimizations and symptoms like
/// memory writes "disappearing".
///
/// Simultaneously, this mapping is independent of the particular set of
/// `AliasRegionKey`s used in a particular Wasm-to-CLIF translation, which
/// is necessary for inlining. If we assigned `user_id = 1` to the first
/// `AliasRegionKey`, `user_id = 2` to the second, etc... then inlining one
/// function into another would mean that the `user_id`s for the same
/// `AliasRegionKey` could be inconsistent with each other, which also leads
/// to the miscompilation scenario described above.
pub(crate) fn into_raw(self) -> u32 {
/// A version of `rustc_hash::FxHasher` that is guaranteed to provide
/// deterministic hashing across Wasmtime builds and processes.
struct AliasRegionHasher {
hash: u64,
}

const OFFSET_MASK: u32 = !Self::KIND_MASK;
impl AliasRegionHasher {
fn new(seed: u64) -> Self {
AliasRegionHasher { hash: seed }
}

const MODULE_BITS: u32 = 8;
const MODULE_OFFSET: u32 = Self::KIND_OFFSET - Self::MODULE_BITS;
const MODULE_MASK: u32 = ((1 << Self::MODULE_BITS) - 1) << Self::MODULE_OFFSET;
fn add(&mut self, i: u64) {
const K: u64 = 0x517c_c1b7_2722_0a95;
self.hash = (self.hash.rotate_left(5) ^ i).wrapping_mul(K);
}
}

const INDEX_MASK: u32 = !Self::KIND_MASK & !Self::MODULE_MASK;
impl Hasher for AliasRegionHasher {
fn finish(&self) -> u64 {
self.hash
}

const fn new_kind(kind: u32) -> u32 {
assert!(kind < (1 << Self::KIND_BITS));
kind << Self::KIND_OFFSET
}
fn write(&mut self, bytes: &[u8]) {
for b in bytes {
self.add((*b).into());
}
}

const VM_CONTEXT_KIND: u32 = Self::new_kind(0b000000);
const VM_STORE_CONTEXT_KIND: u32 = Self::new_kind(0b000001);
const IMPORTED_MEMORY_KIND: u32 = Self::new_kind(0b000010);
const DEFINED_MEMORY_KIND: u32 = Self::new_kind(0b000011);
const IMPORTED_TABLE_KIND: u32 = Self::new_kind(0b000100);
const DEFINED_TABLE_KIND: u32 = Self::new_kind(0b000101);
const IMPORTED_GLOBAL_KIND: u32 = Self::new_kind(0b000110);
const DEFINED_GLOBAL_KIND: u32 = Self::new_kind(0b000111);
const GC_HEAP_KIND: u32 = Self::new_kind(0b001000);
const VM_MEMORY_DEFINITION_KIND: u32 = Self::new_kind(0b001001);
const VM_TABLE_DEFINITION_KIND: u32 = Self::new_kind(0b001010);
const VM_COMPONENT_CONTEXT_KIND: u32 = Self::new_kind(0b001011);
const VM_DRC_HEAP_DATA_KIND: u32 = Self::new_kind(0b001100);
const VM_COPYING_HEAP_DATA_KIND: u32 = Self::new_kind(0b001101);
const VM_NULL_HEAP_DATA_KIND: u32 = Self::new_kind(0b001110);
const VM_DEFERRED_THREAD_KIND: u32 = Self::new_kind(0b001111);
const VM_CONTREF_KIND: u32 = Self::new_kind(0b010000);
const CONTINUATION_STACK_MEMORY_KIND: u32 = Self::new_kind(0b010001);
const VM_FUNCTION_IMPORT_KIND: u32 = Self::new_kind(0b010010);
const VM_MEMORY_IMPORT_KIND: u32 = Self::new_kind(0b010011);
const VM_TABLE_IMPORT_KIND: u32 = Self::new_kind(0b010100);
const VM_TAG_IMPORT_KIND: u32 = Self::new_kind(0b010101);
const VM_GLOBAL_IMPORT_KIND: u32 = Self::new_kind(0b010110);
const STACK_KIND: u32 = Self::new_kind(0b010111);
const VM_FUNC_REF_KIND: u32 = Self::new_kind(0b011000);
const TYPE_IDS_ARRAY_KIND: u32 = Self::new_kind(0b011001);
const EPOCH_COUNTER_KIND: u32 = Self::new_kind(0b011010);
const BUILTIN_FUNCTIONS_KIND: u32 = Self::new_kind(0b011011);
const COMPONENT_BUILTIN_FUNCTIONS_KIND: u32 = Self::new_kind(0b011100);
const UNSAFE_INTRINSIC_MEMORY_KIND: u32 = Self::new_kind(0b011101);
const HOST_VAL_RAW_KIND: u32 = Self::new_kind(0b011110);
const ELEMENT_SEGMENT_KIND: u32 = Self::new_kind(0b011111);
const DATA_SEGMENT_KIND: u32 = Self::new_kind(0b100000);
const VM_GLOBAL_DEFINITION_KIND: u32 = Self::new_kind(0b100001);
const VM_TAG_DEFINITION_KIND: u32 = Self::new_kind(0b100010);
const VM_LAZY_THREAD_KIND: u32 = Self::new_kind(0b100011);
const VM_STACK_LIMITS_KIND: u32 = Self::new_kind(0b100100);
const VM_COMMON_STACK_INFORMATION_KIND: u32 = Self::new_kind(0b100101);
const VM_HOST_ARRAY_KIND: u32 = Self::new_kind(0b100110);
fn write_u8(&mut self, i: u8) {
self.add(i.into());
}

/// Encode this key into a raw `u32` suitable for use as an
/// `AliasRegionData::user_id`.
pub(crate) fn into_raw(self) -> u32 {
match self {
AliasRegionKey::Vm { ty, offset } => {
debug_assert_eq!(offset & Self::KIND_MASK, 0);
let kind = match ty {
VmType::VMContext => Self::VM_CONTEXT_KIND,
VmType::VMStoreContext => Self::VM_STORE_CONTEXT_KIND,
VmType::VMMemoryDefinition => Self::VM_MEMORY_DEFINITION_KIND,
VmType::VMTableDefinition => Self::VM_TABLE_DEFINITION_KIND,
VmType::VMGlobalDefinition => Self::VM_GLOBAL_DEFINITION_KIND,
VmType::VMTagDefinition => Self::VM_TAG_DEFINITION_KIND,
VmType::VMComponentContext => Self::VM_COMPONENT_CONTEXT_KIND,
VmType::VMDrcHeapData => Self::VM_DRC_HEAP_DATA_KIND,
VmType::VMCopyingHeapData => Self::VM_COPYING_HEAP_DATA_KIND,
VmType::VMNullHeapData => Self::VM_NULL_HEAP_DATA_KIND,
VmType::VMDeferredThread => Self::VM_DEFERRED_THREAD_KIND,
VmType::VMStackLimits => Self::VM_STACK_LIMITS_KIND,
VmType::VMLazyThread => Self::VM_LAZY_THREAD_KIND,
VmType::VMContRef => Self::VM_CONTREF_KIND,
VmType::VMCommonStackInformation => Self::VM_COMMON_STACK_INFORMATION_KIND,
VmType::VMHostArray => Self::VM_HOST_ARRAY_KIND,
VmType::ContinuationStackMemory => Self::CONTINUATION_STACK_MEMORY_KIND,
VmType::VMFunctionImport => Self::VM_FUNCTION_IMPORT_KIND,
VmType::VMMemoryImport => Self::VM_MEMORY_IMPORT_KIND,
VmType::VMTableImport => Self::VM_TABLE_IMPORT_KIND,
VmType::VMTagImport => Self::VM_TAG_IMPORT_KIND,
VmType::VMGlobalImport => Self::VM_GLOBAL_IMPORT_KIND,
VmType::VMFuncRef => Self::VM_FUNC_REF_KIND,
VmType::TypeIdsArray => Self::TYPE_IDS_ARRAY_KIND,
VmType::EpochCounter => Self::EPOCH_COUNTER_KIND,
VmType::BuiltinFunctionsArray => Self::BUILTIN_FUNCTIONS_KIND,
VmType::ComponentBuiltinFunctionsArray => {
Self::COMPONENT_BUILTIN_FUNCTIONS_KIND
}
VmType::HostValRaw => Self::HOST_VAL_RAW_KIND,
};
kind | (offset & Self::OFFSET_MASK)
fn write_u16(&mut self, i: u16) {
self.add(i.into());
}
AliasRegionKey::PublicMemory => Self::IMPORTED_MEMORY_KIND,
AliasRegionKey::DefinedMemory { module, index } => {
debug_assert_eq!(
module.as_u32() & !(Self::MODULE_MASK >> Self::MODULE_OFFSET),
0
);
debug_assert_eq!(index.as_u32() & !Self::INDEX_MASK, 0);
Self::DEFINED_MEMORY_KIND
| (module.as_u32() << Self::MODULE_OFFSET)
| index.as_u32()

fn write_u32(&mut self, i: u32) {
self.add(i.into());
}
AliasRegionKey::PublicTable => Self::IMPORTED_TABLE_KIND,
AliasRegionKey::DefinedTable { module, index } => {
debug_assert_eq!(
module.as_u32() & !(Self::MODULE_MASK >> Self::MODULE_OFFSET),
0
);
debug_assert_eq!(index.as_u32() & !Self::INDEX_MASK, 0);
Self::DEFINED_TABLE_KIND | (module.as_u32() << Self::MODULE_OFFSET) | index.as_u32()

fn write_u64(&mut self, i: u64) {
self.add(i);
}
AliasRegionKey::PublicGlobal => Self::IMPORTED_GLOBAL_KIND,
AliasRegionKey::DefinedGlobal { module, index } => {
debug_assert_eq!(
module.as_u32() & !(Self::MODULE_MASK >> Self::MODULE_OFFSET),
0
);
debug_assert_eq!(index.as_u32() & !Self::INDEX_MASK, 0);
Self::DEFINED_GLOBAL_KIND
| (module.as_u32() << Self::MODULE_OFFSET)
| index.as_u32()

fn write_u128(&mut self, i: u128) {
self.add(u64::try_from(i & u128::from(u64::MAX)).unwrap());
self.add(u64::try_from(i >> 64).unwrap());
}
AliasRegionKey::GcHeap => Self::GC_HEAP_KIND,
AliasRegionKey::Stack { slot } => {
debug_assert_eq!(slot.as_u32() & Self::KIND_MASK, 0);
Self::STACK_KIND | (slot.as_u32() & Self::OFFSET_MASK)

fn write_usize(&mut self, i: usize) {
self.add(u64::try_from(i).unwrap());
}
AliasRegionKey::UnsafeIntrinsicMemory => Self::UNSAFE_INTRINSIC_MEMORY_KIND,
AliasRegionKey::ElementSegment => Self::ELEMENT_SEGMENT_KIND,
AliasRegionKey::DataSegment => Self::DATA_SEGMENT_KIND,
}

/// The `AliasRegionHasher` seed, chosen to shift which keys collide
/// with which.
///
/// Collisions are inevitable because we are lossily mapping our
/// ~"infinite" keys onto only 256 actual alias regions. However, not
/// all collisions are equal. The cost of a pair of globals colliding is
/// marginal. A collision between `VMContext::store_context` and the
/// first defined memory's base pointer, two of the most-frequently used
/// regions in essentially every function, would be expensive.
///
/// This value was picked by brute-force search as one for which none
/// of the following collide with each other: the fixed, non-indexed
/// keys appearing anywhere in `tests/disas`, and the first five defined
/// memories, tables, and globals of the first module. If a new `VM*`
/// field or `AliasRegionKey` variant introduces such a collision later,
/// the `tests/disas` expectations will show it as two regions merging
/// into one, and a new seed can be searched for in the same way.
const SEED: u64 = 15912981;

let mut hasher = AliasRegionHasher::new(SEED);
self.hash(&mut hasher);
let hash = hasher.finish();

// `xor`-fold the hash down to a single byte.
hash.to_le_bytes().into_iter().fold(0, |a, b| a ^ b).into()
}
}

Expand Down Expand Up @@ -326,25 +290,14 @@ impl fmt::Debug for AliasRegionKey {
}
}

impl From<AliasRegionKey> for ir::AliasRegionData {
fn from(key: AliasRegionKey) -> ir::AliasRegionData {
ir::AliasRegionData {
user_id: key.into_raw(),
description: format!("{key:?}").into(),
}
}
}

/// Alias region cache and load/store helper type.
/// Alias region bookkeeping and load/store helper type.
///
/// This is scoped to a single function: it hands out `ir::AliasRegion`s, which
/// are indices into one function's `ir::AliasRegionSet`, so a given
/// `AliasRegions` must not be reused across functions.
pub struct AliasRegions<Offsets> {
pointer_type: ir::Type,
offsets: Offsets,

/// Cached alias regions for alias analysis.
///
/// Avoids allocating a string for the debug formatting of `AliasRegionKey`
/// as the `ir::AliasRegionData::description` string repeatedly.
cache: std::collections::HashMap<AliasRegionKey, ir::AliasRegion>,
}

impl<Offsets> AliasRegions<Offsets> {
Expand All @@ -360,13 +313,7 @@ impl<Offsets> AliasRegions<Offsets> {
slot: ir::StackSlot,
_offset: u32,
) -> Option<ir::AliasRegion> {
let key = AliasRegionKey::Stack { slot };
let id = key.into_raw();
if let Some(region) = regions.get(id) {
Some(region)
} else {
Some(regions.insert(key.into()))
}
Some(Self::insert_region(regions, AliasRegionKey::Stack { slot }))
}

/// Get the alias region for a stack slot.
Expand All @@ -380,10 +327,18 @@ impl<Offsets> AliasRegions<Offsets> {

/// Get the alias region for the given key.
fn region(&mut self, func: &mut ir::Function, key: AliasRegionKey) -> ir::AliasRegion {
*self
.cache
.entry(key)
.or_insert_with(|| func.dfg.alias_regions.insert(key.into()))
Self::insert_region(&mut func.dfg.alias_regions, key)
}

/// Get or create the alias region for the given key.
fn insert_region(regions: &mut ir::AliasRegionSet, key: AliasRegionKey) -> ir::AliasRegion {
let user_id = key.into_raw();
let region = regions.insert(ir::AliasRegionData {
user_id,
description: "".into(),
});
log::trace!("alias region: {key:?} => {region} (user_id = {user_id})");
region
}
}

Expand Down Expand Up @@ -1177,7 +1132,6 @@ where
pointer_type: ir::Type::int_with_byte_size(offsets.get_ptr_size().size().into())
.unwrap(),
offsets,
cache: std::collections::HashMap::default(),
}
}

Expand Down
10 changes: 5 additions & 5 deletions tests/disas/alias-region-globals.wat
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
)
)
;; function u0:0(i64 vmctx, i64, i32) -> i32 tail {
;; region0 = 8 "VMContext+0x8"
;; region1 = 67108888 "VMStoreContext+0x18"
;; region2 = 1476395008 "VMGlobalImport+0x0"
;; region3 = 402653184 "PublicGlobal"
;; region4 = 469762048 "DefinedGlobal(StaticModuleIndex(0), DefinedGlobalIndex(0))"
;; region0 = 123 ""
;; region1 = 160 ""
;; region2 = 44 ""
;; region3 = 87 ""
;; region4 = 250 ""
;; gv0 = vmctx
;; gv1 = load.i64 notrap aligned readonly can_move region0 gv0+8
;; gv2 = load.i64 notrap aligned region1 gv1+24
Expand Down
Loading
Loading