From 9dc2a842621ead16c566f8e476e67cad671878dd Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Thu, 22 Oct 2026 14:35:10 -0700 Subject: [PATCH 1/2] compiler: skip slice bounds checks that cannot fail When slicing a slice without a high or max index, the high bound is the length and the max bound is the capacity of the same slice. The check high > max is then always false because len <= cap. LLVM often cannot know this, so s[i:] can keep an extra comparison that blocks loop optimizations. Do not emit high > max in that case. Also do not emit max > cap when max is the capacity itself, as in s[a:b]. The remaining checks are unchanged. On a synthetic loop that consumes a byte slice with s = s[4:] this is 8x faster at -opt=2 and 8% faster at -opt=z, as the median over 6 code layouts. Code size on the testdata programs changes by less than 0.4% in either direction, measured on wasip1. Add compiler tests for the emitted checks and runtime tests for the panic cases. Update the expected binary sizes. --- builder/testdata/binary-size.txt | 20 +++++------ compiler/asserts.go | 22 ++++++++---- compiler/compiler.go | 8 ++--- compiler/testdata/slice.go | 14 ++++++++ compiler/testdata/slice.ll | 61 ++++++++++++++++++++++++++++++-- compiler/testdata/string.ll | 8 ++--- testdata/slice.go | 27 ++++++++++++++ testdata/slice.txt | 11 ++++++ 8 files changed, 144 insertions(+), 27 deletions(-) diff --git a/builder/testdata/binary-size.txt b/builder/testdata/binary-size.txt index d71db72081..12a35fd57d 100644 --- a/builder/testdata/binary-size.txt +++ b/builder/testdata/binary-size.txt @@ -1,11 +1,11 @@ target package opt code rodata data bss hifive1b examples/echo z 4558 402 0 2276 microbit examples/serial z 3001 439 8 2272 -wioterminal examples/pininterrupt z 7866 1686 136 7280 -pico examples/serial z 7737 1071 112 5616 -pico examples/serial 2 16346 862 108 5616 -pico2 examples/serial z 6532 1172 112 5608 -pico2 examples/serial 2 15957 979 108 5608 +wioterminal examples/pininterrupt z 7848 1688 136 7280 +pico examples/serial z 7721 1071 112 5616 +pico examples/serial 2 16362 862 108 5616 +pico2 examples/serial z 6530 1174 112 5608 +pico2 examples/serial 2 15975 977 108 5608 esp32c3-generic examples/serial z 5844 772 0 8664 esp32c3-generic examples/serial 2 9956 604 8 8672 cortex-m-qemu ./testdata/size-slice z 3087 793 4 4168 @@ -13,9 +13,9 @@ cortex-m-qemu ./testdata/size-slice 2 7331 341 4 4168 cortex-m-qemu ./testdata/size-defer z 3782 818 4 4176 cortex-m-qemu ./testdata/size-contains z 3093 763 4 4200 pico2 ./testdata/size-contains z 6690 1158 112 5632 -pico2 ./testdata/size-os z 6201 1207 128 5592 -pico2 ./testdata/size-os 2 15272 1016 128 5592 -cortex-m-qemu ./testdata/size-corpus z 9907 1301 80 4188 -cortex-m-qemu ./testdata/size-corpus 2 17482 862 72 4188 -riscv-qemu ./testdata/size-corpus z 17540 1028 88 16660 +pico2 ./testdata/size-os z 6199 1209 128 5592 +pico2 ./testdata/size-os 2 15290 1014 128 5592 +cortex-m-qemu ./testdata/size-corpus z 9891 1301 80 4188 +cortex-m-qemu ./testdata/size-corpus 2 17466 862 72 4188 +riscv-qemu ./testdata/size-corpus z 17508 1028 88 16660 riscv-qemu ./testdata/size-corpus 2 25160 744 80 16644 diff --git a/compiler/asserts.go b/compiler/asserts.go index 4cacd13aff..4c99f3cacb 100644 --- a/compiler/asserts.go +++ b/compiler/asserts.go @@ -41,7 +41,10 @@ func (b *builder) createLookupBoundsCheck(arrayLen, index llvm.Value) { // normal meaning) and for creating a new slice, where 'capacity' means the // biggest possible slice capacity, 'low' means len and 'high' means cap. The // logic is the same in both cases. -func (b *builder) createSliceBoundsCheck(capacity, low, high, max llvm.Value, lowType, highType, maxType *types.Basic) { +// +// highLeMax must only be true if high is the length and max is the capacity of +// the same slice, so high > max cannot happen. +func (b *builder) createSliceBoundsCheck(capacity, low, high, max llvm.Value, lowType, highType, maxType *types.Basic, highLeMax bool) { if b.info.nobounds { // The //go:nobounds pragma was added to the function to avoid bounds // checking. @@ -68,12 +71,17 @@ func (b *builder) createSliceBoundsCheck(capacity, low, high, max llvm.Value, lo high = b.extendInteger(high, highType, capacityType) max = b.extendInteger(max, maxType, capacityType) - // Now do the bounds check: low > high || high > capacity - outOfBounds1 := b.CreateICmp(llvm.IntUGT, low, high, "slice.lowhigh") - outOfBounds2 := b.CreateICmp(llvm.IntUGT, high, max, "slice.highmax") - outOfBounds3 := b.CreateICmp(llvm.IntUGT, max, capacity, "slice.maxcap") - outOfBounds := b.CreateOr(outOfBounds1, outOfBounds2, "slice.lowmax") - outOfBounds = b.CreateOr(outOfBounds, outOfBounds3, "slice.lowcap") + // Now do the bounds check: low > high || high > max || max > capacity. + // Checks that can never fail are left out. + outOfBounds := b.CreateICmp(llvm.IntUGT, low, high, "slice.lowhigh") + if !highLeMax && high != max { + outOfBounds2 := b.CreateICmp(llvm.IntUGT, high, max, "slice.highmax") + outOfBounds = b.CreateOr(outOfBounds, outOfBounds2, "slice.lowmax") + } + if max != capacity { + outOfBounds3 := b.CreateICmp(llvm.IntUGT, max, capacity, "slice.maxcap") + outOfBounds = b.CreateOr(outOfBounds, outOfBounds3, "slice.lowcap") + } b.createRuntimeAssert(outOfBounds, "slice", "slicePanic") } diff --git a/compiler/compiler.go b/compiler/compiler.go index 649d5ef39d..421a9b7676 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -2860,7 +2860,7 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) { capType := expr.Cap.Type().Underlying().(*types.Basic) maxAllocationSize := b.maxSliceAllocationSize(llvmElemType) maxSizeValue := llvm.ConstInt(b.uintptrType, maxAllocationSize, false) - b.createSliceBoundsCheck(maxSizeValue, sliceLen, sliceCap, sliceCap, lenType, capType, capType) + b.createSliceBoundsCheck(maxSizeValue, sliceLen, sliceCap, sliceCap, lenType, capType, capType, false) // Allocate the backing array. sliceCapCast, err := b.createConvert(expr.Cap.Type(), types.Typ[types.Uintptr], sliceCap, expr.Pos()) @@ -2970,7 +2970,7 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) { } b.createNilCheck(expr.X, value, "slice") - b.createSliceBoundsCheck(llvmLen, low, high, max, lowType, highType, maxType) + b.createSliceBoundsCheck(llvmLen, low, high, max, lowType, highType, maxType, false) // Truncate ints bigger than uintptr. This is after the bounds // check so it's safe. @@ -3010,7 +3010,7 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) { max = oldCap } - b.createSliceBoundsCheck(oldCap, low, high, max, lowType, highType, maxType) + b.createSliceBoundsCheck(oldCap, low, high, max, lowType, highType, maxType, expr.High == nil && expr.Max == nil) // Truncate ints bigger than uintptr. This is after the bounds // check so it's safe. @@ -3054,7 +3054,7 @@ func (b *builder) createExpr(expr ssa.Value) (llvm.Value, error) { high = oldLen } - b.createSliceBoundsCheck(oldLen, low, high, high, lowType, highType, maxType) + b.createSliceBoundsCheck(oldLen, low, high, high, lowType, highType, maxType, false) // Truncate ints bigger than uintptr. This is after the bounds // check so it's safe. diff --git a/compiler/testdata/slice.go b/compiler/testdata/slice.go index ffe1dd45de..af1cb81bac 100644 --- a/compiler/testdata/slice.go +++ b/compiler/testdata/slice.go @@ -26,6 +26,20 @@ func sliceCopy(dst, src []int) int { return copy(dst, src) } +// Slicing a slice only emits the bounds checks that can fail. + +func sliceFrom(ints []int, low int) { + _ = ints[low:] +} + +func sliceRange(ints []int, low, high int) { + _ = ints[low:high] +} + +func sliceRangeMax(ints []int, low, high, max int) { + _ = ints[low:high:max] +} + // Test bounds checking in *ssa.MakeSlice instruction. func makeByteSlice(len int) []byte { diff --git a/compiler/testdata/slice.ll b/compiler/testdata/slice.ll index 9fdf794f09..8b220c816b 100644 --- a/compiler/testdata/slice.ll +++ b/compiler/testdata/slice.ll @@ -101,6 +101,65 @@ declare i32 @llvm.umin.i32(i32, i32) #3 ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite) declare void @llvm.memmove.p0.p0.i32(ptr nocapture writeonly, ptr nocapture readonly, i32, i1 immarg) #4 +; Function Attrs: nounwind +define hidden void @main.sliceFrom(ptr %ints.data, i32 %ints.len, i32 %ints.cap, i32 %low, ptr %context) unnamed_addr #1 { +entry: + %slice.lowhigh = icmp ugt i32 %low, %ints.len + br i1 %slice.lowhigh, label %slice.throw, label %slice.next + +slice.next: ; preds = %entry + ret void + +slice.throw: ; preds = %entry + call void @runtime.slicePanic(ptr undef) #5 + br label %unwind.return + +unwind.return: ; preds = %slice.throw + ret void +} + +declare void @runtime.slicePanic(ptr) #0 + +; Function Attrs: nounwind +define hidden void @main.sliceRange(ptr %ints.data, i32 %ints.len, i32 %ints.cap, i32 %low, i32 %high, ptr %context) unnamed_addr #1 { +entry: + %slice.lowhigh = icmp ugt i32 %low, %high + %slice.highmax = icmp ugt i32 %high, %ints.cap + %slice.lowmax = or i1 %slice.lowhigh, %slice.highmax + br i1 %slice.lowmax, label %slice.throw, label %slice.next + +slice.next: ; preds = %entry + ret void + +slice.throw: ; preds = %entry + call void @runtime.slicePanic(ptr undef) #5 + br label %unwind.return + +unwind.return: ; preds = %slice.throw + ret void +} + +; Function Attrs: nounwind +define hidden void @main.sliceRangeMax(ptr %ints.data, i32 %ints.len, i32 %ints.cap, i32 %low, i32 %high, i32 %max, ptr %context) unnamed_addr #1 { +entry: + %slice.lowhigh = icmp ugt i32 %low, %high + %slice.highmax = icmp ugt i32 %high, %max + %slice.lowmax = or i1 %slice.lowhigh, %slice.highmax + %slice.maxcap = icmp ugt i32 %max, %ints.cap + %slice.lowcap = or i1 %slice.lowmax, %slice.maxcap + br i1 %slice.lowcap, label %slice.throw, label %slice.next + +slice.next: ; preds = %entry + ret void + +slice.throw: ; preds = %entry + call void @runtime.slicePanic(ptr undef) #5 + br label %unwind.return + +unwind.return: ; preds = %slice.throw + ret void +} + ; Function Attrs: nounwind define hidden { ptr, i32, i32 } @main.makeByteSlice(i32 %len, ptr %context) unnamed_addr #1 { entry: @@ -124,8 +183,6 @@ unwind.return: ; preds = %slice.throw ret { ptr, i32, i32 } undef } -declare void @runtime.slicePanic(ptr) #0 - ; Function Attrs: nounwind define hidden { ptr, i32, i32 } @main.makeInt16Slice(i32 %len, ptr %context) unnamed_addr #1 { entry: diff --git a/compiler/testdata/string.ll b/compiler/testdata/string.ll index 7d20985f1b..3d2cf0d58c 100644 --- a/compiler/testdata/string.ll +++ b/compiler/testdata/string.ll @@ -186,9 +186,9 @@ slice.next: ; preds = %entry %0 = call %runtime._string @runtime.stringFromBytes(ptr %a.data, i32 0, i32 %a.cap, ptr undef) #4 %1 = extractvalue %runtime._string %0, 0 call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #4 - br i1 false, label %slice.throw, label %slice.next5 + br i1 false, label %slice.throw, label %slice.next3 -slice.next5: ; preds = %slice.next +slice.next3: ; preds = %slice.next call void @runtime.trackPointer(ptr %b.data, ptr nonnull %stackalloc, ptr undef) #4 %2 = extractvalue %runtime._string %0, 0 %3 = extractvalue %runtime._string %0, 1 @@ -339,9 +339,9 @@ slice.next: ; preds = %entry %1 = extractvalue %runtime._string %0, 0 call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #4 %slice.highmax1 = icmp ult i32 %b.cap, 2 - br i1 %slice.highmax1, label %slice.throw, label %slice.next5 + br i1 %slice.highmax1, label %slice.throw, label %slice.next3 -slice.next5: ; preds = %slice.next +slice.next3: ; preds = %slice.next call void @runtime.trackPointer(ptr %b.data, ptr nonnull %stackalloc, ptr undef) #4 %2 = extractvalue %runtime._string %0, 0 %3 = extractvalue %runtime._string %0, 1 diff --git a/testdata/slice.go b/testdata/slice.go index dbd0024b16..c70e62ddfa 100644 --- a/testdata/slice.go +++ b/testdata/slice.go @@ -168,6 +168,7 @@ func main() { println("len:", len(rs)) testRangeLoops() + testSliceBounds() } // Range loops evaluate the slice once and keep their own copy of the index. @@ -241,6 +242,32 @@ func testRangeLoops() { println("range nested:", sum) } +var sliceSink []byte + +func slicePanics(name string, f func()) { + defer func() { + println(name, recover() != nil) + }() + f() +} + +// Test that slicing a slice still panics for every out of range case. +func testSliceBounds() { + b := make([]byte, 10, 20) + low, high, max := makeInt(5), makeInt(15), makeInt(20) + slicePanics("b[len:]", func() { sliceSink = b[makeInt(10):] }) + slicePanics("b[len+1:]", func() { sliceSink = b[makeInt(11):] }) + slicePanics("b[:cap]", func() { sliceSink = b[:makeInt(20)] }) + slicePanics("b[:cap+1]", func() { sliceSink = b[:makeInt(21)] }) + slicePanics("b[low:high]", func() { sliceSink = b[low:high] }) + slicePanics("b[high:low]", func() { sliceSink = b[high:low] }) + slicePanics("b[low:high:max]", func() { sliceSink = b[low:high:max] }) + slicePanics("b[low:high:cap+1]", func() { sliceSink = b[low:high:makeInt(21)] }) + slicePanics("b[low:max:high]", func() { sliceSink = b[low:max:high] }) + slicePanics("b[-1:]", func() { sliceSink = b[makeInt(-1):] }) + slicePanics("b[:-1]", func() { sliceSink = b[:makeInt(-1)] }) +} + func printslice(name string, s []int) { print(name, ": len=", len(s), " cap=", cap(s), " data:") for _, n := range s { diff --git a/testdata/slice.txt b/testdata/slice.txt index 70daeb705d..f58dd2abd1 100644 --- a/testdata/slice.txt +++ b/testdata/slice.txt @@ -23,3 +23,14 @@ range reassign: 70 0 range continue break: 8 range write: 51 range nested: 10 +b[len:] false +b[len+1:] true +b[:cap] false +b[:cap+1] true +b[low:high] false +b[high:low] true +b[low:high:max] false +b[low:high:cap+1] true +b[low:max:high] true +b[-1:] true +b[:-1] true From 9dec3a0053d95d00b9231e64630262409d7de7b5 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Sat, 24 Oct 2026 08:39:17 -0700 Subject: [PATCH 2/2] builder: update expected binary sizes Use the sizes measured in CI for pico2. The row for size-contains is new on dev and the slice change affects it. --- builder/testdata/binary-size.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/testdata/binary-size.txt b/builder/testdata/binary-size.txt index 12a35fd57d..ecc8226f94 100644 --- a/builder/testdata/binary-size.txt +++ b/builder/testdata/binary-size.txt @@ -4,7 +4,7 @@ microbit examples/serial z 3001 439 8 2272 wioterminal examples/pininterrupt z 7848 1688 136 7280 pico examples/serial z 7721 1071 112 5616 pico examples/serial 2 16362 862 108 5616 -pico2 examples/serial z 6530 1174 112 5608 +pico2 examples/serial z 6514 1174 112 5608 pico2 examples/serial 2 15975 977 108 5608 esp32c3-generic examples/serial z 5844 772 0 8664 esp32c3-generic examples/serial 2 9956 604 8 8672 @@ -12,7 +12,7 @@ cortex-m-qemu ./testdata/size-slice z 3087 793 4 4168 cortex-m-qemu ./testdata/size-slice 2 7331 341 4 4168 cortex-m-qemu ./testdata/size-defer z 3782 818 4 4176 cortex-m-qemu ./testdata/size-contains z 3093 763 4 4200 -pico2 ./testdata/size-contains z 6690 1158 112 5632 +pico2 ./testdata/size-contains z 6672 1160 112 5632 pico2 ./testdata/size-os z 6199 1209 128 5592 pico2 ./testdata/size-os 2 15290 1014 128 5592 cortex-m-qemu ./testdata/size-corpus z 9891 1301 80 4188