Skip to content

Commit 020e6cc

Browse files
authored
[WasmGC] Heap2Local: Optimize RefCast failures (#6727)
Previously we just did not optimize cases where our escape analysis showed an allocation flowed into a cast that failed. However, after inlining there can be real-world cases where that happens, even in traps-never-happen mode (if the cast is behind a conditional branch), so it seems worth optimizing.
1 parent 5613981 commit 020e6cc

2 files changed

Lines changed: 111 additions & 37 deletions

File tree

src/passes/Heap2Local.cpp

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -379,11 +379,13 @@ struct EscapeAnalyzer {
379379
}
380380

381381
void visitRefCast(RefCast* curr) {
382-
// As it is our allocation that flows through here, we need to
383-
// check that the cast will not trap, so that we can continue
384-
// to (hopefully) optimize this allocation.
385-
if (Type::isSubType(allocation->type, curr->type)) {
386-
escapes = false;
382+
// Whether the cast succeeds or fails, it does not escape.
383+
escapes = false;
384+
385+
// If the cast fails then the allocation is fully consumed and does not
386+
// flow any further (instead, we trap).
387+
if (!Type::isSubType(allocation->type, curr->type)) {
388+
fullyConsumes = true;
387389
}
388390
}
389391

@@ -783,24 +785,22 @@ struct Struct2Local : PostWalker<Struct2Local> {
783785
return;
784786
}
785787

786-
// It is safe to optimize out this RefCast, since we proved it
787-
// contains our allocation and we have checked that the type of
788-
// the allocation is a subtype of the type of the cast, and so
789-
// cannot trap.
790-
replaceCurrent(curr->ref);
788+
// We know this RefCast receives our allocation, so we can see whether it
789+
// succeeds or fails.
790+
if (Type::isSubType(allocation->type, curr->type)) {
791+
// The cast succeeds, so it is a no-op, and we can skip it, since after we
792+
// remove the allocation it will not even be needed for validation.
793+
replaceCurrent(curr->ref);
794+
} else {
795+
// The cast fails, so this must trap.
796+
replaceCurrent(builder.makeSequence(builder.makeDrop(curr->ref),
797+
builder.makeUnreachable()));
798+
}
791799

792-
// We need to refinalize after this, as while we know the cast is not
793-
// logically needed - the value flowing through will not be used - we do
794-
// need validation to succeed even before other optimizations remove the
795-
// code. For example:
796-
//
797-
// (block (result $B)
798-
// (ref.cast $B
799-
// (block (result $A)
800-
//
801-
// Without the cast this does not validate, so we need to refinalize
802-
// (which will fix this, as we replace the unused value with a null, so
803-
// that type will propagate out).
800+
// Either way, we need to refinalize here (we either added an unreachable,
801+
// or we replaced a cast with the value being cast, which may have a less-
802+
// refined type - it will not be used after we remove the allocation, but we
803+
// must still fix that up for validation).
804804
refinalize = true;
805805
}
806806

test/lit/passes/heap2local.wast

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2635,24 +2635,34 @@
26352635
(type $A (sub (struct (field (ref null $A)))))
26362636
;; CHECK: (type $1 (func (result anyref)))
26372637

2638-
;; CHECK: (type $B (sub $A (struct (field (ref $A)))))
2639-
(type $B (sub $A (struct (field (ref $A)))))
2638+
;; CHECK: (type $B (sub $A (struct (field (ref $A)) (field i32))))
2639+
(type $B (sub $A (struct (field (ref $A)) (field i32))))
2640+
2641+
;; CHECK: (type $3 (func (result i32)))
26402642

26412643
;; CHECK: (func $func (type $1) (result anyref)
26422644
;; CHECK-NEXT: (local $a (ref $A))
26432645
;; CHECK-NEXT: (local $1 (ref $A))
2644-
;; CHECK-NEXT: (local $2 (ref $A))
2646+
;; CHECK-NEXT: (local $2 i32)
2647+
;; CHECK-NEXT: (local $3 (ref $A))
2648+
;; CHECK-NEXT: (local $4 i32)
26452649
;; CHECK-NEXT: (ref.cast (ref $B)
26462650
;; CHECK-NEXT: (block (result (ref $A))
26472651
;; CHECK-NEXT: (drop
26482652
;; CHECK-NEXT: (block (result nullref)
2649-
;; CHECK-NEXT: (local.set $2
2653+
;; CHECK-NEXT: (local.set $3
26502654
;; CHECK-NEXT: (struct.new $A
26512655
;; CHECK-NEXT: (ref.null none)
26522656
;; CHECK-NEXT: )
26532657
;; CHECK-NEXT: )
2658+
;; CHECK-NEXT: (local.set $4
2659+
;; CHECK-NEXT: (i32.const 1)
2660+
;; CHECK-NEXT: )
26542661
;; CHECK-NEXT: (local.set $1
2655-
;; CHECK-NEXT: (local.get $2)
2662+
;; CHECK-NEXT: (local.get $3)
2663+
;; CHECK-NEXT: )
2664+
;; CHECK-NEXT: (local.set $2
2665+
;; CHECK-NEXT: (local.get $4)
26562666
;; CHECK-NEXT: )
26572667
;; CHECK-NEXT: (ref.null none)
26582668
;; CHECK-NEXT: )
@@ -2675,6 +2685,7 @@
26752685
(struct.new $A
26762686
(ref.null none)
26772687
)
2688+
(i32.const 1)
26782689
)
26792690
)
26802691
)
@@ -2683,16 +2694,24 @@
26832694

26842695
;; CHECK: (func $cast-success (type $1) (result anyref)
26852696
;; CHECK-NEXT: (local $0 (ref $A))
2686-
;; CHECK-NEXT: (local $1 (ref $A))
2697+
;; CHECK-NEXT: (local $1 i32)
2698+
;; CHECK-NEXT: (local $2 (ref $A))
2699+
;; CHECK-NEXT: (local $3 i32)
26872700
;; CHECK-NEXT: (drop
26882701
;; CHECK-NEXT: (block (result nullref)
2689-
;; CHECK-NEXT: (local.set $1
2702+
;; CHECK-NEXT: (local.set $2
26902703
;; CHECK-NEXT: (struct.new $A
26912704
;; CHECK-NEXT: (ref.null none)
26922705
;; CHECK-NEXT: )
26932706
;; CHECK-NEXT: )
2707+
;; CHECK-NEXT: (local.set $3
2708+
;; CHECK-NEXT: (i32.const 1)
2709+
;; CHECK-NEXT: )
26942710
;; CHECK-NEXT: (local.set $0
2695-
;; CHECK-NEXT: (local.get $1)
2711+
;; CHECK-NEXT: (local.get $2)
2712+
;; CHECK-NEXT: )
2713+
;; CHECK-NEXT: (local.set $1
2714+
;; CHECK-NEXT: (local.get $3)
26962715
;; CHECK-NEXT: )
26972716
;; CHECK-NEXT: (ref.null none)
26982717
;; CHECK-NEXT: )
@@ -2706,25 +2725,80 @@
27062725
(struct.new $A
27072726
(ref.null none)
27082727
)
2728+
(i32.const 1)
27092729
)
27102730
)
27112731
)
27122732
)
27132733
;; CHECK: (func $cast-failure (type $1) (result anyref)
2714-
;; CHECK-NEXT: (struct.get $B 0
2715-
;; CHECK-NEXT: (ref.cast (ref $B)
2716-
;; CHECK-NEXT: (struct.new $A
2717-
;; CHECK-NEXT: (struct.new $A
2718-
;; CHECK-NEXT: (ref.null none)
2734+
;; CHECK-NEXT: (local $0 (ref null $A))
2735+
;; CHECK-NEXT: (local $1 (ref null $A))
2736+
;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit)
2737+
;; CHECK-NEXT: (drop
2738+
;; CHECK-NEXT: (block
2739+
;; CHECK-NEXT: (drop
2740+
;; CHECK-NEXT: (block (result nullref)
2741+
;; CHECK-NEXT: (local.set $1
2742+
;; CHECK-NEXT: (struct.new $A
2743+
;; CHECK-NEXT: (ref.null none)
2744+
;; CHECK-NEXT: )
2745+
;; CHECK-NEXT: )
2746+
;; CHECK-NEXT: (local.set $0
2747+
;; CHECK-NEXT: (local.get $1)
2748+
;; CHECK-NEXT: )
2749+
;; CHECK-NEXT: (ref.null none)
2750+
;; CHECK-NEXT: )
27192751
;; CHECK-NEXT: )
2752+
;; CHECK-NEXT: (unreachable)
27202753
;; CHECK-NEXT: )
27212754
;; CHECK-NEXT: )
2755+
;; CHECK-NEXT: (unreachable)
27222756
;; CHECK-NEXT: )
27232757
;; CHECK-NEXT: )
27242758
(func $cast-failure (result anyref)
27252759
(struct.get $B 0
2726-
;; The allocated $A arrives here, but the cast will fail, so we do not
2727-
;; optimize.
2760+
;; The allocated $A arrives here, but the cast will fail. We can remove
2761+
;; the allocation and put an unreachable here. (Note that the inner
2762+
;; struct.new survives, which would take another cycle to remove.)
2763+
(ref.cast (ref $B)
2764+
(struct.new $A
2765+
(struct.new $A
2766+
(ref.null none)
2767+
)
2768+
)
2769+
)
2770+
)
2771+
)
2772+
2773+
;; CHECK: (func $cast-failure-nofield (type $3) (result i32)
2774+
;; CHECK-NEXT: (local $0 (ref null $A))
2775+
;; CHECK-NEXT: (local $1 (ref null $A))
2776+
;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit)
2777+
;; CHECK-NEXT: (drop
2778+
;; CHECK-NEXT: (block
2779+
;; CHECK-NEXT: (drop
2780+
;; CHECK-NEXT: (block (result nullref)
2781+
;; CHECK-NEXT: (local.set $1
2782+
;; CHECK-NEXT: (struct.new $A
2783+
;; CHECK-NEXT: (ref.null none)
2784+
;; CHECK-NEXT: )
2785+
;; CHECK-NEXT: )
2786+
;; CHECK-NEXT: (local.set $0
2787+
;; CHECK-NEXT: (local.get $1)
2788+
;; CHECK-NEXT: )
2789+
;; CHECK-NEXT: (ref.null none)
2790+
;; CHECK-NEXT: )
2791+
;; CHECK-NEXT: )
2792+
;; CHECK-NEXT: (unreachable)
2793+
;; CHECK-NEXT: )
2794+
;; CHECK-NEXT: )
2795+
;; CHECK-NEXT: (unreachable)
2796+
;; CHECK-NEXT: )
2797+
;; CHECK-NEXT: )
2798+
(func $cast-failure-nofield (result i32)
2799+
;; As above, but we read from a field that only exists in $B, despite the
2800+
;; allocation that flows here being an $A. We should not error on that.
2801+
(struct.get $B 1 ;; this changes from 0 to 1
27282802
(ref.cast (ref $B)
27292803
(struct.new $A
27302804
(struct.new $A

0 commit comments

Comments
 (0)