@@ -3448,6 +3448,34 @@ impl FuncEnvironment<'_> {
34483448 /// epochs are enabled to break up the copy into a loop of chunks with
34493449 /// preemption checks between them.
34503450 fn raw_bulk_memory_operation ( & mut self , builder : & mut FunctionBuilder < ' _ > , mut op : BulkOp ) {
3451+ // Fast path: a copy whose byte length is a small compile-time constant is
3452+ // expanded inline (see `emit_inline_memcpy`), skipping the libcall's fixed
3453+ // per-call cost (a wasm/host transition and an indirect call) that
3454+ // dominates tiny copies. Larger or dynamic copies, and all fills, use the
3455+ // libcall below, whose `memmove` amortizes that cost.
3456+ //
3457+ // The bound is empirical: measured on aarch64, inline is ~1.7-2.7x faster
3458+ // than the libcall through 128 bytes and ties by 256 (cost grows with the
3459+ // length, since every chunk is loaded before any is stored).
3460+ const INLINE_COPY_MAX_BYTES : u64 = 128 ;
3461+ if let BulkOp :: MemoryCopy {
3462+ dst,
3463+ src,
3464+ len,
3465+ flags,
3466+ } = op
3467+ {
3468+ if let Some ( bytes) = Self :: value_as_const_int ( builder, len) {
3469+ if ( 1 ..=INLINE_COPY_MAX_BYTES ) . contains ( & bytes) {
3470+ if self . tunables . consume_fuel {
3471+ self . fuel_consumed += bytes as i64 ;
3472+ }
3473+ self . emit_inline_memcpy ( builder, dst, src, bytes, flags) ;
3474+ return ;
3475+ }
3476+ }
3477+ }
3478+
34513479 // Very scientifically chosen. Or, more seriously, this is just an
34523480 // arbitrary number for now. 100k copies of this size locally takes half
34533481 // a second, so seems like a reasonably large chunk size to not hit perf
@@ -3467,7 +3495,7 @@ impl FuncEnvironment<'_> {
34673495 env. epoch_check ( builder) ;
34683496 }
34693497 match * op {
3470- BulkOp :: MemoryCopy { dst, src, len } => {
3498+ BulkOp :: MemoryCopy { dst, src, len, .. } => {
34713499 if env. tunables . consume_fuel {
34723500 // Note that fuel is always a 64-bit counter.
34733501 let fuel_consumed = match env. pointer_type ( ) {
@@ -3530,7 +3558,7 @@ impl FuncEnvironment<'_> {
35303558 } ;
35313559 let has_chunk = builder. ins ( ) . icmp ( IntCC :: UnsignedGreaterThan , len, chunk) ;
35323560 match * op {
3533- BulkOp :: MemoryCopy { dst, src, len } => {
3561+ BulkOp :: MemoryCopy { dst, src, len, .. } => {
35343562 builder. ins ( ) . brif (
35353563 has_chunk,
35363564 chunk_block,
@@ -3553,7 +3581,7 @@ impl FuncEnvironment<'_> {
35533581 has_chunk_branch ( builder, & op) ;
35543582
35553583 let append_block_params = |builder : & mut FunctionBuilder < ' _ > , block, op : & mut _ | match op {
3556- BulkOp :: MemoryCopy { dst, src, len } => {
3584+ BulkOp :: MemoryCopy { dst, src, len, .. } => {
35573585 * dst = builder. append_block_param ( block, pointer_type) ;
35583586 * src = builder. append_block_param ( block, pointer_type) ;
35593587 * len = builder. append_block_param ( block, pointer_type) ;
@@ -3577,7 +3605,7 @@ impl FuncEnvironment<'_> {
35773605 * op_len = chunk;
35783606 raw_call ( self , builder, & op) ;
35793607 match & mut op {
3580- BulkOp :: MemoryCopy { dst, src, len } => {
3608+ BulkOp :: MemoryCopy { dst, src, len, .. } => {
35813609 * dst = builder. ins ( ) . iadd ( * dst, chunk) ;
35823610 * src = builder. ins ( ) . iadd ( * src, chunk) ;
35833611 * len = builder. ins ( ) . isub ( remaining_len, chunk) ;
@@ -3983,12 +4011,18 @@ impl FuncEnvironment<'_> {
39834011 src_entity,
39844012 CheckedEntity :: Memory ( _) | CheckedEntity :: Data { .. }
39854013 ) ) ;
4014+ // Linear-memory access flags (little-endian, heap alias region),
4015+ // as in `prepare_addr`.
4016+ let flags = ir:: MemFlagsData :: new ( )
4017+ . with_endianness ( ir:: Endianness :: Little )
4018+ . with_alias_region ( Some ( ir:: AliasRegion :: Heap ) ) ;
39864019 self . raw_bulk_memory_operation (
39874020 builder,
39884021 BulkOp :: MemoryCopy {
39894022 dst : dst_raw_addr,
39904023 src : src_raw_addr,
39914024 len : len_ptr,
4025+ flags,
39924026 } ,
39934027 ) ;
39944028 Ok ( ( ) )
@@ -4300,45 +4334,23 @@ impl FuncEnvironment<'_> {
43004334 }
43014335
43024336 // For memcpy, that's easy, just call the intrinsic with the right
4303- // parameters.
4337+ // parameters. The inline path in `raw_bulk_memory_operation` uses these
4338+ // per-entity flags: GC arrays trap on heap corruption, tables use the
4339+ // table alias region.
43044340 if !type_forbids_memcpy && dst_element_size == src_element_size {
4305- // Expand small, statically-sized copies inline to skip the
4306- // `memory_copy` libcall's fixed per-call cost (a wasm/host transition
4307- // and indirect call), which dominates for tiny copies. Dynamic or
4308- // larger copies keep the libcall, whose `memmove` amortizes it. Only
4309- // arrays (in the GC heap) qualify; tables stay on the libcall.
4310- const INLINE_ARRAY_COPY_MAX_ELEMS : u64 = 8 ;
4311- if let CheckedEntity :: Array { .. } = dst_entity {
4312- let elem_ty = match dst_element_size {
4313- 1 => Some ( ir:: types:: I8 ) ,
4314- 2 => Some ( ir:: types:: I16 ) ,
4315- 4 => Some ( ir:: types:: I32 ) ,
4316- 8 => Some ( ir:: types:: I64 ) ,
4317- 16 => Some ( ir:: types:: I8X16 ) ,
4318- _ => None ,
4319- } ;
4320- if let ( Some ( elem_ty) , Some ( n) ) =
4321- ( elem_ty, Self :: value_as_const_int ( builder, copy_len) )
4322- {
4323- if ( 1 ..=INLINE_ARRAY_COPY_MAX_ELEMS ) . contains ( & n) {
4324- self . emit_inline_array_copy (
4325- builder,
4326- dst_elem_addr,
4327- src_elem_addr,
4328- elem_ty,
4329- dst_element_size,
4330- n,
4331- ) ;
4332- return Ok ( ( ) ) ;
4333- }
4341+ let flags = match dst_entity {
4342+ CheckedEntity :: Array { .. } => {
4343+ ir:: MemFlagsData :: new ( ) . with_trap_code ( Some ( TRAP_GC_HEAP_CORRUPT ) )
43344344 }
4335- }
4345+ _ => ir:: MemFlagsData :: new ( ) . with_alias_region ( Some ( ir:: AliasRegion :: Table ) ) ,
4346+ } ;
43364347 self . raw_bulk_memory_operation (
43374348 builder,
43384349 BulkOp :: MemoryCopy {
43394350 dst : dst_elem_addr,
43404351 src : src_elem_addr,
43414352 len : dst_copy_byte_len,
4353+ flags,
43424354 } ,
43434355 ) ;
43444356 return Ok ( ( ) ) ;
@@ -4415,57 +4427,70 @@ impl FuncEnvironment<'_> {
44154427 Ok ( ( ) )
44164428 }
44174429
4418- /// If `value` is a compile-time constant integer (possibly behind a widening
4419- /// cast of one, as inserted for array indices), return its raw bits.
4420- /// Out-of-range values are left for the caller to reject.
4421- fn value_as_const_int ( builder : & FunctionBuilder < ' _ > , mut value : ir:: Value ) -> Option < u64 > {
4422- loop {
4423- let inst = builder. func . dfg . value_def ( value) . inst ( ) ?;
4424- match builder. func . dfg . insts [ inst] {
4425- ir:: InstructionData :: UnaryImm {
4426- opcode : ir:: Opcode :: Iconst ,
4427- imm,
4428- } => return Some ( imm. bits ( ) . cast_unsigned ( ) ) ,
4429- ir:: InstructionData :: Unary {
4430- opcode : ir:: Opcode :: Uextend ,
4431- arg,
4432- } => value = arg,
4433- _ => return None ,
4434- }
4435- }
4430+ /// If `value` is a compile-time constant integer — possibly behind the
4431+ /// widening/narrowing casts and constant multiply that length computations
4432+ /// insert (a byte length is `element_count * element_size`) — return its raw
4433+ /// bits. Out-of-range values are left for the caller to reject.
4434+ fn value_as_const_int ( builder : & FunctionBuilder < ' _ > , value : ir:: Value ) -> Option < u64 > {
4435+ let inst = builder. func . dfg . value_def ( value) . inst ( ) ?;
4436+ Some ( match builder. func . dfg . insts [ inst] {
4437+ ir:: InstructionData :: UnaryImm {
4438+ opcode : ir:: Opcode :: Iconst ,
4439+ imm,
4440+ } => imm. bits ( ) . cast_unsigned ( ) ,
4441+ ir:: InstructionData :: Unary {
4442+ opcode : ir:: Opcode :: Uextend | ir:: Opcode :: Ireduce ,
4443+ arg,
4444+ } => Self :: value_as_const_int ( builder, arg) ?,
4445+ ir:: InstructionData :: BinaryImm64 {
4446+ opcode : ir:: Opcode :: ImulImm ,
4447+ arg,
4448+ imm,
4449+ } => Self :: value_as_const_int ( builder, arg) ?. wrapping_mul ( imm. bits ( ) . cast_unsigned ( ) ) ,
4450+ _ => return None ,
4451+ } )
44364452 }
44374453
4438- /// Expand a small, statically-sized `array.copy` into inline loads then
4439- /// stores, avoiding the `memory_copy` libcall.
4454+ /// Expand a copy of `bytes` (a small compile-time constant) into inline loads
4455+ /// then stores, avoiding the `memory_copy` libcall.
44404456 ///
4441- /// The copy is bitwise: `elem_ty` is an integer or vector type matching the
4442- /// element width (`f32`/`f64` use `i32`/`i64`, `v128` uses `i8x16`), so any
4443- /// fixed-width element works. Every element is loaded before any is stored,
4444- /// so overlapping ranges keep `array.copy`'s `memmove` semantics. The caller
4445- /// has already bounds-checked the addresses and length.
4446- fn emit_inline_array_copy (
4457+ /// The copy is bitwise and element-type agnostic: the byte range is covered
4458+ /// greedily with the widest convenient access (`i8x16` down to `i8`). Every
4459+ /// chunk is loaded before any is stored, so overlapping ranges keep `memmove`
4460+ /// semantics. `flags` must carry the entity-appropriate access flags and must
4461+ /// not assume alignment, since wide chunks may straddle element boundaries.
4462+ /// The caller has already bounds-checked the range.
4463+ fn emit_inline_memcpy (
44474464 & mut self ,
44484465 builder : & mut FunctionBuilder < ' _ > ,
44494466 dst_addr : ir:: Value ,
44504467 src_addr : ir:: Value ,
4451- elem_ty : ir:: Type ,
4452- elem_size : u32 ,
4453- n : u64 ,
4468+ bytes : u64 ,
4469+ flags : ir:: MemFlagsData ,
44544470 ) {
4455- if self . tunables . consume_fuel {
4456- self . fuel_consumed += n as i64 ;
4457- }
4458- // GC-heap access flags (trap on corruption, no alignment assumed). Not
4459- // `GC_MEMFLAGS` directly: that constant is gated to the `gc` feature.
4460- let flags = ir:: MemFlagsData :: new ( ) . with_trap_code ( Some ( TRAP_GC_HEAP_CORRUPT ) ) ;
4461- let stride = i32:: try_from ( elem_size) . unwrap ( ) ;
4462- let count = i32:: try_from ( n) . unwrap ( ) ;
4463- let mut vals: SmallVec < [ ir:: Value ; 8 ] > = smallvec ! [ ] ;
4464- for i in 0 ..count {
4465- vals. push ( builder. ins ( ) . load ( elem_ty, flags, src_addr, i * stride) ) ;
4471+ const WIDTHS : & [ ( u64 , ir:: Type ) ] = & [
4472+ ( 16 , ir:: types:: I8X16 ) ,
4473+ ( 8 , ir:: types:: I64 ) ,
4474+ ( 4 , ir:: types:: I32 ) ,
4475+ ( 2 , ir:: types:: I16 ) ,
4476+ ( 1 , ir:: types:: I8 ) ,
4477+ ] ;
4478+ let mut chunks: SmallVec < [ ( i32 , ir:: Type ) ; 8 ] > = smallvec ! [ ] ;
4479+ let mut offset = 0u64 ;
4480+ let mut remaining = bytes;
4481+ for & ( width, ty) in WIDTHS {
4482+ while remaining >= width {
4483+ chunks. push ( ( i32:: try_from ( offset) . unwrap ( ) , ty) ) ;
4484+ offset += width;
4485+ remaining -= width;
4486+ }
44664487 }
4467- for ( i, val) in ( 0 ..count) . zip ( vals) {
4468- builder. ins ( ) . store ( flags, val, dst_addr, i * stride) ;
4488+ let vals: SmallVec < [ ir:: Value ; 8 ] > = chunks
4489+ . iter ( )
4490+ . map ( |& ( off, ty) | builder. ins ( ) . load ( ty, flags, src_addr, off) )
4491+ . collect ( ) ;
4492+ for ( & ( off, _) , val) in chunks. iter ( ) . zip ( vals) {
4493+ builder. ins ( ) . store ( flags, val, dst_addr, off) ;
44694494 }
44704495 }
44714496
@@ -5521,11 +5546,14 @@ enum BulkOp {
55215546 /// A `memory.copy` operation, copying memory from `src` to `dst`.
55225547 ///
55235548 /// All of `dst`, `src`, and `len` must be pre-validated and inbounds. All
5524- /// must have type `env.pointer_type()`.
5549+ /// must have type `env.pointer_type()`. `flags` are the access flags used if
5550+ /// the copy is expanded inline (see `emit_inline_memcpy`); they vary by the
5551+ /// entity being copied (GC heap, linear memory, or table).
55255552 MemoryCopy {
55265553 dst : ir:: Value ,
55275554 src : ir:: Value ,
55285555 len : ir:: Value ,
5556+ flags : ir:: MemFlagsData ,
55295557 } ,
55305558
55315559 /// A `memory.fill` operation, setting all bytes of `dst` to `val`.
0 commit comments