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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/11589-byte-view-element-access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
perf(buffer): `Uint8Array` and `Buffer` element reads and writes are answered from the byte-view admission cache (now two-way, and admitting Node Buffers) inline at typed sites and at the top of the runtime byte accessors, instead of the buffer-registry probes (#10515, #10694). On qb2: the #10515 `Uint8Array`-parameter loop goes from 876 to 76 instructions per element; nanoid/generate −34.4%, uuid/v7 −12.6%.

Key material (CryptoKey, secret and asymmetric keys), `ArrayBuffer`, `SharedArrayBuffer` and `DataView` buffers are never admitted to that cache. The perry-stdlib test `thread_exit_releases_the_threads_external_buffer_registrations` (#11555) primed the cache through a CryptoKey buffer and probed it with the old one-way slot formula, so it failed on this change. It now primes the cache with an ordinary `Buffer` on the same thread, asserts the key is refused, and reads the cache through the new `perry_runtime::buffer::u8_inline_cache_holds_for_test` probe. The cache's thread-exit release was already correct: it clears every slot, both ways. With that release removed, the test fails with "PERRY_U8_INLINE_CACHE outlived the thread".
49 changes: 25 additions & 24 deletions crates/perry-codegen/src/expr/arrays_finds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,10 @@ pub(crate) fn lower_uint8array_get_i32(

let idx_i32 = lower_index_i32(ctx, index)?;
let a = lower_expr(ctx, array)?;
let blk = ctx.block();
let handle = unbox_to_i64(blk, &a);
let byte_i32 = blk.call(I32, "js_uint8array_get", &[(I64, &handle), (I32, &idx_i32)]);
// #10515: admitted owning byte views load inline; misses (and priming)
// stay on the runtime accessor.
let byte_i32 =
super::u8_buffer_read::emit_u8_cached_get_i32(ctx, &a, &idx_i32, "js_uint8array_get");
let slow = LoweredValue {
semantic: SemanticKind::JsNumber,
rep: NativeRep::I32,
Expand Down Expand Up @@ -381,9 +382,8 @@ pub(crate) fn lower_buffer_index_get_i32(

let idx_i32 = lower_index_i32(ctx, index)?;
let a = lower_expr(ctx, buffer)?;
let blk = ctx.block();
let handle = unbox_to_i64(blk, &a);
let byte_i32 = blk.call(I32, "js_buffer_get", &[(I64, &handle), (I32, &idx_i32)]);
let byte_i32 =
super::u8_buffer_read::emit_u8_cached_get_i32(ctx, &a, &idx_i32, "js_buffer_get");
let slow = LoweredValue {
semantic: SemanticKind::JsNumber,
rep: NativeRep::I32,
Expand Down Expand Up @@ -997,12 +997,11 @@ pub(crate) fn lower(
&[index],
|ctx| lower_index_i32(ctx, index),
|ctx, vals, idx_i32| {
let blk = ctx.block();
let handle = unbox_to_i64(blk, &vals[0]);
Ok(blk.call(
DOUBLE,
Ok(super::u8_buffer_read::emit_u8_cached_get_value(
ctx,
&vals[0],
&idx_i32,
"js_uint8array_index_get_value",
&[(I64, &handle), (I32, &idx_i32)],
))
},
)
Expand All @@ -1026,12 +1025,11 @@ pub(crate) fn lower(
&[index],
|ctx| lower_index_i32(ctx, index),
|ctx, vals, idx_i32| {
let blk = ctx.block();
let handle = unbox_to_i64(blk, &vals[0]);
Ok(blk.call(
DOUBLE,
Ok(super::u8_buffer_read::emit_u8_cached_get_value(
ctx,
&vals[0],
&idx_i32,
"js_buffer_index_get_value",
&[(I64, &handle), (I32, &idx_i32)],
))
},
)
Expand Down Expand Up @@ -1134,11 +1132,13 @@ pub(crate) fn lower(
// Slow path accepts either BufferHeader-backed Uint8Arrays or
// NativeArena typed views.
let a = lower_expr(ctx, array)?;
let blk = ctx.block();
let handle = unbox_to_i64(blk, &a);
blk.call_void(
// #10515: admitted owning byte views store inline.
super::u8_buffer_read::emit_u8_cached_set_i32(
ctx,
&a,
&idx_i32,
&val_i32,
"js_uint8array_set",
&[(I64, &handle), (I32, &idx_i32), (I32, &val_i32)],
);
let reason = buffer_access_materialization_reason(ctx, array);
let slow = LoweredValue {
Expand Down Expand Up @@ -1227,11 +1227,12 @@ pub(crate) fn lower(
ctx.toint32_wrap(&v)
};
let a = lower_expr(ctx, buffer)?;
let blk = ctx.block();
let handle = unbox_to_i64(blk, &a);
blk.call_void(
super::u8_buffer_read::emit_u8_cached_set_i32(
ctx,
&a,
&idx_i32,
&val_i32,
"js_buffer_set",
&[(I64, &handle), (I32, &idx_i32), (I32, &val_i32)],
);
let reason = buffer_access_materialization_reason(ctx, buffer);
let slow = LoweredValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ pub(super) fn lower_inline_dyn_typed_array_get(
ctx.ic_globals.push(cache_name.clone());
let slot_ref = format!("@{cache_name}");

let u8_brand_idx = ctx.new_block("arrlike.u8.brand");
let u8_bounds_idx = ctx.new_block("arrlike.u8.bounds");
let u8_load_idx = ctx.new_block("arrlike.u8.load");
let u8_brand_label = ctx.block_label(u8_brand_idx);
let u8_bounds_label = ctx.block_label(u8_bounds_idx);
let u8_load_label = ctx.block_label(u8_load_idx);
let object_header_idx = ctx.new_block("arrlike.ic.header");
let object_brand_idx = ctx.new_block("arrlike.ic.brand");
let object_array_guard_idx = ctx.new_block("arrlike.ic.array_guard");
Expand Down Expand Up @@ -476,7 +482,47 @@ pub(super) fn lower_inline_dyn_typed_array_get(
ctx.current_block = elem_kind_idx;
let elem_is_object = ctx.block().icmp_eq(I8, &gc_type, "2");
ctx.block()
.cond_br(&elem_is_object, &elem_meta_label, &object_miss_label);
.cond_br(&elem_is_object, &elem_meta_label, &u8_brand_label);

// ---- #10515: an admitted owning byte view (`Uint8Array` / `Buffer`) ----
//
// A `GC_TYPE_BUFFER` receiver whose full address is in
// `PERRY_U8_INLINE_CACHE` is, by that cache's contract, a live registered
// byte view with `length` at offset 0 and its bytes inline at `+8` — the
// same proof `u8_buffer_read.rs` loads on for a `Uint8Array`-typed
// receiver. Untyped `b[i]` over a Buffer used to leave through the exit
// and the typed-array + buffer registry probes on every element. The
// cache is primed by the runtime byte accessors on a miss, and anything
// it does not hold (views, ArrayBuffers, DataViews, foreign spans, an
// out-of-range index) still leaves through the exit.
ctx.current_block = u8_brand_idx;
{
let blk = ctx.block();
let is_buffer = blk.icmp_eq(I8, &gc_type, "10"); // GC_TYPE_BUFFER
let admitted = crate::expr::u8_buffer_read::emit_u8_cache_holds(blk, &object_raw);
let hit = blk.and(I1, &is_buffer, &admitted);
blk.cond_br(&hit, &u8_bounds_label, &object_miss_label);
}
ctx.current_block = u8_bounds_idx;
{
let blk = ctx.block();
let len_ptr = blk.inttoptr(I64, &object_raw);
let len = blk.load(I32, &len_ptr);
let len_i64 = blk.zext(I32, &len, I64);
let in_bounds = blk.icmp_ult(I64, &object_idx_i64, &len_i64);
blk.cond_br(&in_bounds, &u8_load_label, &object_miss_label);
}
ctx.current_block = u8_load_idx;
let u8_value = {
let blk = ctx.block();
let data = blk.add(I64, &object_raw, "8");
let addr = blk.add(I64, &data, &object_idx_i64);
let ptr = blk.inttoptr(I64, &addr);
let byte = blk.load(I8, &ptr);
blk.uitofp(I8, &byte, DOUBLE)
};
let u8_end_label = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = elem_meta_idx;
let meta_ptr_size: u64 = if crate::target_layout::target_is_ilp32(ctx.target_triple) {
Expand Down Expand Up @@ -596,6 +642,7 @@ pub(super) fn lower_inline_dyn_typed_array_get(
(ta_w1_value.as_str(), ta_w1_end.as_str()),
(array_value.as_str(), array_end_label.as_str()),
(elem_value.as_str(), elem_end_label.as_str()),
(u8_value.as_str(), u8_end_label.as_str()),
(slow_val.as_str(), slow_end_label.as_str()),
],
)
Expand Down
21 changes: 18 additions & 3 deletions crates/perry-codegen/src/expr/index_get_claim_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ fn unknown_numeric_read_is_one_inline_hit_and_one_out_of_line_exit() {
assert_eq!(
dynamic_index_site_blocks(&ir),
vec![
// #10515: the admitted byte-view (`Uint8Array` / `Buffer`) arm.
"arrlike.u8.brand",
"arrlike.u8.bounds",
"arrlike.u8.load",
"arrlike.ic.header",
"arrlike.ic.brand",
"arrlike.ic.array_guard",
Expand Down Expand Up @@ -370,7 +374,7 @@ fn the_number_context_coercion_is_coupled_across_every_arm() {
);
assert_eq!(
dynamic_index_site_blocks(&ir).len(),
21,
24,
"{name}: a number context must not change the emitted block shape:\n{ir}"
);
let miss = super::class_field_barrier_tests::block_body(&ir, "arrlike.ic.miss.")
Expand Down Expand Up @@ -715,9 +719,20 @@ fn any_typed_dynamic_key_takes_the_numeric_tiers_when_it_is_an_array_index() {
let kind = super::class_field_barrier_tests::block_body(&ir, "arrlike.elem.kind.")
.expect("the object-kind guard exists");
assert!(
kind.contains("icmp eq i8") && kind.contains(", 2") && kind.contains("arrlike.ic.miss"),
kind.contains("icmp eq i8") && kind.contains(", 2") && kind.contains("arrlike.u8.brand"),
"only GC_TYPE_OBJECT may reach the ObjectMeta.elements load; everything \
else must leave through the single exit:\n{kind}"
else goes to the byte-view arm:\n{kind}"
);
// #10515: the byte-view arm admits only a `GC_TYPE_BUFFER` whose address
// the admission cache holds; everything else leaves through the exit.
let u8_brand = super::class_field_barrier_tests::block_body(&ir, "arrlike.u8.brand.")
.expect("the byte-view brand guard exists");
assert!(
u8_brand.contains(", 10")
&& u8_brand.contains("@PERRY_U8_INLINE_CACHE")
&& u8_brand.contains("arrlike.ic.miss"),
"the byte-view arm must test GC_TYPE_BUFFER and the admission cache, \
and exit on a miss:\n{u8_brand}"
);
// The elements-backed subclass probe, the lazy-JSON-array probe and the
// dense-tail family token now live behind that exit rather than at every
Expand Down
Loading
Loading