diff --git a/changelog.d/8676-packed-method-shape-guard.md b/changelog.d/8676-packed-method-shape-guard.md new file mode 100644 index 0000000000..a2227da7c6 --- /dev/null +++ b/changelog.d/8676-packed-method-shape-guard.md @@ -0,0 +1,9 @@ +--- +category: Performance +title: Pack monomorphic method shape guards +--- + +Monomorphic direct-method guards now validate the contiguous GC, class, and +ShapeId header fields with two packed loads while retaining prototype +invalidation, pointer-range checks, ShapeId validation, and the generic +fallback. This reduces repeated dispatch proof work in hot method-call loops. diff --git a/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs b/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs index bd80b0ef30..a6bb8193fc 100644 --- a/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs +++ b/crates/perry-codegen/src/collectors/proven_this_routing_tests.rs @@ -737,17 +737,21 @@ fn single_arm_method_shape_guard_is_inlined_with_the_runtime_contract() { probe.contains("method_direct.inline_deref") && probe.contains("getelementptr i8, ptr") && probe.contains("i64 -8") - && probe.contains("i64 -7") - && probe.contains("i64 -6") - && probe.contains("and i16") - && probe.contains(", 2048") - && probe.contains("icmp ne i32") - && probe.contains("i64 4") + && probe.contains("and i32") + && probe.contains(", 134250751") + && probe.contains("icmp eq i32") + && probe.contains(", 2") + && probe.contains("load i64, ptr") + && probe.contains("zext i32") + && probe.contains("shl i64") + && probe.contains(", 32") + && probe.contains("or i64") + && probe.contains("icmp eq i64") && probe.contains("add i32") && probe.contains(", -2147483648") && probe.contains("icmp ult i32") && probe.contains(", 1073741824"), - "the header block must check the GC type, forwarding flag, own-descriptor bit, nonzero class id, ShapeId domain, and live ShapeId:\n{probe}" + "the packed header block must check the GC type, forwarding flag, own-descriptor bit, exact class/ShapeId pair, and ShapeId domain:\n{probe}" ); } diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index 6d912f8039..8cbf5922c4 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -11,12 +11,21 @@ use crate::expr::{ }; use crate::nanbox::double_literal; use crate::native_value::LoweredValue; -use crate::types::{DOUBLE, I1, I16, I32, I64, I8}; +use crate::types::{DOUBLE, I1, I32, I64, I8}; const POINTER_TAG_HI16: &str = "32765"; // 0x7FFD const GC_TYPE_OBJECT: &str = "2"; -const GC_FLAG_FORWARDED_I8: &str = "-128"; // 0x80 as i8 -const OBJ_FLAG_HAS_DESCRIPTORS_I16: &str = "2048"; // 0x0800 +// The first four bytes before an ObjectHeader are, in little-endian order, +// `gtype: u8`, `flags: u8`, and `reserved: u16`. One masked i32 load can +// therefore prove the three fields the direct-method contract consumes: +// +// gtype == GC_TYPE_OBJECT +// flags & GC_FLAG_FORWARDED == 0 +// reserved & OBJ_FLAG_HAS_DESCRIPTORS == 0 +// +// Mask: 0x0800_0000 (descriptor bit) | 0x0000_8000 (forwarded bit) | +// 0x0000_00ff (the complete gtype byte). +const GC_OBJECT_METHOD_GUARD_MASK_I32: &str = "134250751"; // 0x0800_80ff const SHAPE_ID_BASE_NEG_I32: &str = "-2147483648"; // subtract 0x8000_0000 const SHAPE_ID_RANGE_LEN: &str = "1073741824"; // 0x4000_0000 @@ -87,44 +96,82 @@ fn emit_inline_direct_method_shape_guard( let recv_handle = blk.and(I64, &recv_bits, crate::nanbox::POINTER_MASK_I64); let obj_ptr = blk.inttoptr(I64, &recv_handle); - let gtype_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-8")]); - let gtype = blk.load(I8, >ype_ptr); - let gtype_ok = blk.icmp_eq(I8, >ype, GC_TYPE_OBJECT); + let gc_header_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-8")]); + let gc_header = blk.load(I32, &gc_header_ptr); + let guarded_gc_bits = blk.and(I32, &gc_header, GC_OBJECT_METHOD_GUARD_MASK_I32); + let gc_header_ok = blk.icmp_eq(I32, &guarded_gc_bits, GC_TYPE_OBJECT); - let gflags_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-7")]); - let gflags = blk.load(I8, &gflags_ptr); - let forwarded = blk.and(I8, &gflags, GC_FLAG_FORWARDED_I8); - let not_forwarded = blk.icmp_eq(I8, &forwarded, "0"); + // ObjectHeader begins with adjacent `class_id: u32` and + // `shape_id: u32`. Compare them as one packed word. The expected + // ShapeId is still range-checked below, so equality proves the live + // receiver's class is non-zero and its ShapeId is in-domain without + // four separate field predicates. + let class_shape = blk.load(I64, &obj_ptr); + let expected_shape_i64 = blk.zext(I32, expected_shape_id, I64); + let expected_shape_high = blk.shl(I64, &expected_shape_i64, "32"); + let expected_class_shape = blk.or(I64, &expected_shape_high, expected_class_id); + let class_shape_ok = blk.icmp_eq(I64, &class_shape, &expected_class_shape); - let reserved_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-6")]); - let reserved = blk.load(I16, &reserved_ptr); - let descriptor_bits = blk.and(I16, &reserved, OBJ_FLAG_HAS_DESCRIPTORS_I16); - let no_own_descriptors = blk.icmp_eq(I16, &descriptor_bits, "0"); - - let class_ptr = blk.gep(I8, &obj_ptr, &[(I64, "0")]); - let class_id = blk.load(I32, &class_ptr); - let class_valid = blk.icmp_ne(I32, &class_id, "0"); - let class_ok = blk.icmp_eq(I32, &class_id, expected_class_id); - - let shape_ptr = blk.gep(I8, &obj_ptr, &[(I64, "4")]); - let shape_id = blk.load(I32, &shape_ptr); // `is_shape_id` is `[0x8000_0000, 0xC000_0000)`. Subtract the base // modulo i32 and compare with the range length, matching the runtime - // helper without a call. - let shape_id_rel = blk.add(I32, &shape_id, SHAPE_ID_BASE_NEG_I32); + // helper. Equality above transfers this proof to the live header. + let shape_id_rel = blk.add(I32, expected_shape_id, SHAPE_ID_BASE_NEG_I32); let shape_valid = blk.icmp_ult(I32, &shape_id_rel, SHAPE_ID_RANGE_LEN); - let shape_ok = blk.icmp_eq(I32, &shape_id, expected_shape_id); - let mut pass = blk.and(I1, >ype_ok, ¬_forwarded); - pass = blk.and(I1, &pass, &no_own_descriptors); - pass = blk.and(I1, &pass, &class_valid); - pass = blk.and(I1, &pass, &class_ok); - pass = blk.and(I1, &pass, &shape_valid); - pass = blk.and(I1, &pass, &shape_ok); + let pass = blk.and(I1, &gc_header_ok, &class_shape_ok); + let pass = blk.and(I1, &pass, &shape_valid); blk.cond_br(&pass, fast_label, fallback_label); } } +#[cfg(test)] +mod packed_guard_tests { + use super::*; + + /// Anti-drift gate for the runtime fields packed into the i32 load at + /// `obj - 8`. Keep this alongside the emitter so a flag move changes a + /// failing test instead of silently weakening a generated guard. + #[test] + fn gc_header_mask_preserves_the_direct_method_contract() { + let obj_type_mask = 0x0000_00ffu32; + let forwarded = u32::from(0x80u8) << 8; + let has_descriptors = 0x0800u32 << 16; + let mask = obj_type_mask | forwarded | has_descriptors; + let expected = u32::from(2u8); + + assert_eq!(GC_OBJECT_METHOD_GUARD_MASK_I32, mask.to_string()); + assert_eq!(expected & mask, expected); + assert_ne!((expected | forwarded) & mask, expected); + assert_ne!((expected | has_descriptors) & mask, expected); + assert_ne!((expected ^ 1) & mask, expected); + } + + /// `ObjectHeader::{class_id,parent_class_id}` are adjacent u32 fields. + /// Perry's supported native targets are little-endian, so one i64 load + /// sees class in the low word and ShapeId in the high word. + #[test] + fn class_shape_word_uses_the_supported_little_endian_layout() { + let class_id = 0x1234_5678u32; + let shape_id = 0x89ab_cdefu32; + let mut bytes = [0u8; 8]; + bytes[..4].copy_from_slice(&class_id.to_le_bytes()); + bytes[4..].copy_from_slice(&shape_id.to_le_bytes()); + assert_eq!( + u64::from_le_bytes(bytes), + (u64::from(shape_id) << 32) | u64::from(class_id) + ); + + for triple in [ + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-linux-android", + "x86_64-pc-windows-msvc", + ] { + assert!(!triple.starts_with("s390") && !triple.starts_with("powerpc64-")); + } + } +} + fn typed_i1_method_signature_note(reps: &[crate::codegen::TypedParamRep]) -> String { let first = reps.first().map(|rep| rep.label()).unwrap_or("void"); if reps.len() <= 1 {