Skip to content
Closed
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog.d/8676-packed-method-shape-guard.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 11 additions & 7 deletions crates/perry-codegen/src/collectors/proven_this_routing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
);
}

Expand Down
109 changes: 78 additions & 31 deletions crates/perry-codegen/src/lower_call/method_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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, &gtype_ptr);
let gtype_ok = blk.icmp_eq(I8, &gtype, 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, &gtype_ok, &not_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 {
Expand Down
Loading