Skip to content

Commit 10012d3

Browse files
authored
compiler: report only panicking binary operations as unwinding (#5986)
* compiler: report only panicking binary operations as unwinding instructionCallProperties returns early when a BinOp can panic. A BinOp that cannot panic, such as an addition or an integer comparison, falls through to the conservative default for unknown instructions, which reports that it may both panic and suspend. As a result almost every function that does arithmetic is treated as able to panic and suspend. A caller with a defer adds a checkpoint before each call to it, and with asyncify the call is wrapped as if it could suspend. This change reports a BinOp that cannot panic as neither panicking nor suspending. binOpMayUnwind already covers every BinOp that can panic: integer division and remainder, signed shifts, and comparisons of interface-like values. Every other BinOp is lowered without a runtime call, so it can neither panic nor suspend. * builder: test the size of calls made while a defer is pending No size test has a function with a defer that calls other functions. Such a call gets a panic checkpoint whenever the callee is considered able to panic, so how precisely the compiler decides that is not covered by any size test. This test adds a program in which two functions hold a lock with a deferred release and call small helpers that only do arithmetic, the pattern in the networking examples that the sizediff job shows shrinking. It imports nothing, so its size is the same on Go 1.25, 1.26 and 1.27.
1 parent a990a9d commit 10012d3

7 files changed

Lines changed: 353 additions & 5 deletions

File tree

‎builder/sizes_test.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func TestBinarySize(t *testing.T) {
8484

8585
{target: "cortex-m-qemu", path: "./testdata/size-slice", opt: "z"},
8686
{target: "cortex-m-qemu", path: "./testdata/size-slice", opt: "2"},
87+
{target: "cortex-m-qemu", path: "./testdata/size-defer", opt: "z"},
8788
{target: "pico2", path: "./testdata/size-os", opt: "z"},
8889
{target: "pico2", path: "./testdata/size-os", opt: "2"},
8990

‎builder/testdata/binary-size.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ esp32c3-generic examples/serial z 5864 768 0 8664
1010
esp32c3-generic examples/serial 2 9888 600 8 8672
1111
cortex-m-qemu ./testdata/size-slice z 3087 793 4 4168
1212
cortex-m-qemu ./testdata/size-slice 2 7331 341 4 4168
13+
cortex-m-qemu ./testdata/size-defer z 3782 818 4 4176
1314
pico2 ./testdata/size-os z 6153 1207 128 5592
1415
pico2 ./testdata/size-os 2 15240 1016 128 5592
1516
cortex-m-qemu ./testdata/size-corpus z 9907 1301 80 4188
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
var locked bool
4+
var checksum uint32
5+
6+
//go:noinline
7+
func acquire() { locked = true }
8+
9+
//go:noinline
10+
func release() { locked = false }
11+
12+
//go:noinline
13+
func rotate(x uint32) uint32 { return x<<5 | x>>27 }
14+
15+
//go:noinline
16+
func mix(a, b uint32) uint32 { return (a ^ b) * 16777619 }
17+
18+
//go:noinline
19+
func clamp(x, limit uint32) uint32 {
20+
if x > limit {
21+
return limit
22+
}
23+
return x
24+
}
25+
26+
//go:noinline
27+
func header(n uint32) uint32 {
28+
acquire()
29+
defer release()
30+
h := rotate(n)
31+
h = mix(h, n+1)
32+
return clamp(h, 1500)
33+
}
34+
35+
//go:noinline
36+
func payload(n uint32) uint32 {
37+
acquire()
38+
defer release()
39+
sum := mix(0, rotate(n))
40+
sum = mix(sum, clamp(n, 64))
41+
return mix(sum, rotate(sum))
42+
}
43+
44+
func main() {
45+
for i := uint32(0); i < 4; i++ {
46+
checksum = mix(checksum, header(i))
47+
checksum = mix(checksum, payload(i))
48+
}
49+
println(checksum)
50+
}

‎compiler/calls.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ func instructionCallProperties(instruction ssa.Instruction) callProperties {
473473
if binOpMayUnwind(instruction) {
474474
return callMayUnwind
475475
}
476+
return 0
476477
case *ssa.UnOp:
477478
var properties callProperties
478479
if instruction.Op == token.ARROW {

‎compiler/calls_test.go‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package compiler
2+
3+
import (
4+
"go/token"
5+
"go/types"
6+
"testing"
7+
8+
"golang.org/x/tools/go/ssa"
9+
)
10+
11+
func TestBinOpCallProperties(t *testing.T) {
12+
t.Parallel()
13+
14+
intType := types.Typ[types.Int]
15+
uintType := types.Typ[types.Uint]
16+
floatType := types.Typ[types.Float64]
17+
stringType := types.Typ[types.String]
18+
anyType := types.NewInterfaceType(nil, nil)
19+
intArray := types.NewArray(intType, 2)
20+
anyArray := types.NewArray(anyType, 2)
21+
22+
for _, tc := range []struct {
23+
op token.Token
24+
x, y types.Type
25+
mayUnwind bool
26+
}{
27+
{token.ADD, intType, intType, false},
28+
{token.SUB, intType, intType, false},
29+
{token.MUL, intType, intType, false},
30+
{token.AND, intType, intType, false},
31+
{token.OR, intType, intType, false},
32+
{token.XOR, intType, intType, false},
33+
{token.AND_NOT, intType, intType, false},
34+
{token.LSS, intType, intType, false},
35+
{token.LEQ, intType, intType, false},
36+
{token.GTR, intType, intType, false},
37+
{token.GEQ, intType, intType, false},
38+
{token.EQL, intType, intType, false},
39+
{token.NEQ, intType, intType, false},
40+
{token.ADD, stringType, stringType, false},
41+
{token.EQL, stringType, stringType, false},
42+
{token.QUO, floatType, floatType, false},
43+
{token.SHL, intType, uintType, false},
44+
{token.SHR, intType, uintType, false},
45+
{token.EQL, intArray, intArray, false},
46+
{token.QUO, intType, intType, true},
47+
{token.REM, intType, intType, true},
48+
{token.QUO, uintType, uintType, true},
49+
{token.SHL, intType, intType, true},
50+
{token.SHR, intType, intType, true},
51+
{token.EQL, anyType, anyType, true},
52+
{token.NEQ, anyType, anyType, true},
53+
{token.EQL, anyArray, anyArray, true},
54+
} {
55+
instr := &ssa.BinOp{Op: tc.op, X: ssa.NewConst(nil, tc.x), Y: ssa.NewConst(nil, tc.y)}
56+
want := callProperties(0)
57+
if tc.mayUnwind {
58+
want = callMayUnwind
59+
}
60+
if got := instructionCallProperties(instr); got != want {
61+
t.Errorf("%s %s %s: got properties %d, want %d", tc.x, tc.op, tc.y, got, want)
62+
}
63+
}
64+
}

‎compiler/testdata/defer-cortex-m-qemu.ll‎

Lines changed: 214 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ rundefers.end1: ; preds = %rundefers.loophead4
9898
br label %recover
9999
}
100100

101-
; Function Attrs: nocallback nofree nosync nounwind willreturn
101+
; Function Attrs: nocallback nofree nounwind willreturn
102102
declare ptr @llvm.stacksave.p0() #2
103103

104104
declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(28), ptr, ptr) #1
@@ -209,6 +209,218 @@ entry:
209209
ret void
210210
}
211211

212+
; Function Attrs: nounwind
213+
define hidden i1 @main.noPanicArithmetic(i32 %a, i32 %b, ptr %context) unnamed_addr #0 {
214+
entry:
215+
%0 = add i32 %a, %b
216+
%1 = icmp sgt i32 %0, 3
217+
ret i1 %1
218+
}
219+
220+
; Function Attrs: nounwind
221+
define hidden i1 @main.deferNoPanicArithmetic(i32 %a, i32 %b, ptr %context) unnamed_addr #0 {
222+
entry:
223+
%defer.alloca = alloca { i32, ptr }, align 4
224+
%deferframe.buf = alloca %runtime.deferFrame, align 4
225+
%deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24
226+
%0 = call ptr @llvm.stacksave.p0()
227+
call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4
228+
%1 = call align 1 dereferenceable(1) ptr @runtime.alloc(i32 1, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #4
229+
%defer.next = load ptr, ptr %deferPtr, align 4
230+
store i32 0, ptr %defer.alloca, align 4
231+
%defer.alloca.repack11 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4
232+
store ptr %defer.next, ptr %defer.alloca.repack11, align 4
233+
store ptr %defer.alloca, ptr %deferPtr, align 4
234+
%2 = call i1 @main.noPanicArithmetic(i32 %a, i32 %b, ptr undef)
235+
store i1 %2, ptr %1, align 1
236+
br label %rundefers.block
237+
238+
rundefers.after: ; preds = %rundefers.end
239+
%3 = load i1, ptr %1, align 1
240+
call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4
241+
ret i1 %3
242+
243+
rundefers.block: ; preds = %entry
244+
br label %rundefers.loophead
245+
246+
rundefers.loophead: ; preds = %rundefers.callback0, %rundefers.block
247+
%4 = load ptr, ptr %deferPtr, align 4
248+
%stackIsNil = icmp eq ptr %4, null
249+
br i1 %stackIsNil, label %rundefers.end, label %rundefers.loop
250+
251+
rundefers.loop: ; preds = %rundefers.loophead
252+
%stack.next.gep = getelementptr inbounds nuw i8, ptr %4, i32 4
253+
%stack.next = load ptr, ptr %stack.next.gep, align 4
254+
store ptr %stack.next, ptr %deferPtr, align 4
255+
%callback = load i32, ptr %4, align 4
256+
switch i32 %callback, label %rundefers.default [
257+
i32 0, label %rundefers.callback0
258+
]
259+
260+
rundefers.callback0: ; preds = %rundefers.loop
261+
call void @"main.deferNoPanicArithmetic$1"(ptr undef)
262+
br label %rundefers.loophead
263+
264+
rundefers.default: ; preds = %rundefers.loop
265+
unreachable
266+
267+
rundefers.end: ; preds = %rundefers.loophead
268+
br label %rundefers.after
269+
270+
recover: ; preds = %rundefers.end1
271+
ret i1 poison
272+
273+
lpad: ; No predecessors!
274+
br label %rundefers.loophead4
275+
276+
rundefers.loophead4: ; preds = %rundefers.callback010, %lpad
277+
br i1 poison, label %rundefers.end1, label %rundefers.loop3
278+
279+
rundefers.loop3: ; preds = %rundefers.loophead4
280+
switch i32 poison, label %rundefers.default2 [
281+
i32 0, label %rundefers.callback010
282+
]
283+
284+
rundefers.callback010: ; preds = %rundefers.loop3
285+
br label %rundefers.loophead4
286+
287+
rundefers.default2: ; preds = %rundefers.loop3
288+
unreachable
289+
290+
rundefers.end1: ; preds = %rundefers.loophead4
291+
br label %recover
292+
}
293+
294+
; Function Attrs: allockind("alloc,zeroed") allocsize(0)
295+
declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #3
296+
297+
; Function Attrs: nounwind
298+
define internal void @"main.deferNoPanicArithmetic$1"(ptr %context) unnamed_addr #0 {
299+
entry:
300+
call void @runtime.printlock(ptr undef) #4
301+
call void @runtime.printint32(i32 4, ptr undef) #4
302+
call void @runtime.printunlock(ptr undef) #4
303+
ret void
304+
}
305+
306+
; Function Attrs: nounwind
307+
define hidden i32 @main.panicDivide(i32 %a, i32 %b, ptr %context) unnamed_addr #0 {
308+
entry:
309+
%0 = icmp eq i32 %b, 0
310+
br i1 %0, label %divbyzero.throw, label %divbyzero.next
311+
312+
divbyzero.next: ; preds = %entry
313+
%1 = icmp eq i32 %b, -1
314+
%2 = icmp eq i32 %a, -2147483648
315+
%3 = and i1 %1, %2
316+
%4 = select i1 %3, i32 1, i32 %b
317+
%5 = sdiv i32 %a, %4
318+
ret i32 %5
319+
320+
divbyzero.throw: ; preds = %entry
321+
call void @runtime.divideByZeroPanic(ptr undef) #4
322+
unreachable
323+
}
324+
325+
declare void @runtime.divideByZeroPanic(ptr) #1
326+
327+
; Function Attrs: nounwind
328+
define hidden i32 @main.deferPanicDivide(i32 %a, i32 %b, ptr %context) unnamed_addr #0 {
329+
entry:
330+
%defer.alloca = alloca { i32, ptr }, align 4
331+
%deferframe.buf = alloca %runtime.deferFrame, align 4
332+
%deferPtr = getelementptr inbounds nuw i8, ptr %deferframe.buf, i32 24
333+
%0 = call ptr @llvm.stacksave.p0()
334+
call void @runtime.setupDeferFrame(ptr nonnull %deferframe.buf, ptr %0, ptr undef) #4
335+
%1 = call align 4 dereferenceable(4) ptr @runtime.alloc(i32 4, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #4
336+
%defer.next = load ptr, ptr %deferPtr, align 4
337+
store i32 0, ptr %defer.alloca, align 4
338+
%defer.alloca.repack11 = getelementptr inbounds nuw i8, ptr %defer.alloca, i32 4
339+
store ptr %defer.next, ptr %defer.alloca.repack11, align 4
340+
store ptr %defer.alloca, ptr %deferPtr, align 4
341+
%setjmp = call i32 asm "\0Amovs r0, #0\0Amov r2, pc\0Astr r2, [r1, #4]", "={r0},{r1},~{r1},~{r2},~{r3},~{r4},~{r5},~{r6},~{r7},~{r8},~{r9},~{r10},~{r11},~{r12},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{cpsr},~{memory}"(ptr nonnull %deferframe.buf) #5
342+
%setjmp.result = icmp eq i32 %setjmp, 0
343+
br i1 %setjmp.result, label %2, label %lpad
344+
345+
2: ; preds = %entry
346+
%3 = call i32 @main.panicDivide(i32 %a, i32 %b, ptr undef)
347+
store i32 %3, ptr %1, align 4
348+
br label %rundefers.block
349+
350+
rundefers.after: ; preds = %rundefers.end
351+
%4 = load i32, ptr %1, align 4
352+
call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4
353+
ret i32 %4
354+
355+
rundefers.block: ; preds = %2
356+
br label %rundefers.loophead
357+
358+
rundefers.loophead: ; preds = %rundefers.callback0, %rundefers.block
359+
%5 = load ptr, ptr %deferPtr, align 4
360+
%stackIsNil = icmp eq ptr %5, null
361+
br i1 %stackIsNil, label %rundefers.end, label %rundefers.loop
362+
363+
rundefers.loop: ; preds = %rundefers.loophead
364+
%stack.next.gep = getelementptr inbounds nuw i8, ptr %5, i32 4
365+
%stack.next = load ptr, ptr %stack.next.gep, align 4
366+
store ptr %stack.next, ptr %deferPtr, align 4
367+
%callback = load i32, ptr %5, align 4
368+
switch i32 %callback, label %rundefers.default [
369+
i32 0, label %rundefers.callback0
370+
]
371+
372+
rundefers.callback0: ; preds = %rundefers.loop
373+
call void @"main.deferPanicDivide$1"(ptr undef)
374+
br label %rundefers.loophead
375+
376+
rundefers.default: ; preds = %rundefers.loop
377+
unreachable
378+
379+
rundefers.end: ; preds = %rundefers.loophead
380+
br label %rundefers.after
381+
382+
recover: ; preds = %rundefers.end1
383+
%6 = load i32, ptr %1, align 4
384+
call void @runtime.destroyDeferFrame(ptr nonnull %deferframe.buf, ptr undef) #4
385+
ret i32 %6
386+
387+
lpad: ; preds = %entry
388+
br label %rundefers.loophead4
389+
390+
rundefers.loophead4: ; preds = %rundefers.callback010, %lpad
391+
%7 = load ptr, ptr %deferPtr, align 4
392+
%stackIsNil5 = icmp eq ptr %7, null
393+
br i1 %stackIsNil5, label %rundefers.end1, label %rundefers.loop3
394+
395+
rundefers.loop3: ; preds = %rundefers.loophead4
396+
%stack.next.gep6 = getelementptr inbounds nuw i8, ptr %7, i32 4
397+
%stack.next7 = load ptr, ptr %stack.next.gep6, align 4
398+
store ptr %stack.next7, ptr %deferPtr, align 4
399+
%callback9 = load i32, ptr %7, align 4
400+
switch i32 %callback9, label %rundefers.default2 [
401+
i32 0, label %rundefers.callback010
402+
]
403+
404+
rundefers.callback010: ; preds = %rundefers.loop3
405+
call void @"main.deferPanicDivide$1"(ptr undef)
406+
br label %rundefers.loophead4
407+
408+
rundefers.default2: ; preds = %rundefers.loop3
409+
unreachable
410+
411+
rundefers.end1: ; preds = %rundefers.loophead4
412+
br label %recover
413+
}
414+
415+
; Function Attrs: nounwind
416+
define internal void @"main.deferPanicDivide$1"(ptr %context) unnamed_addr #0 {
417+
entry:
418+
call void @runtime.printlock(ptr undef) #4
419+
call void @runtime.printint32(i32 6, ptr undef) #4
420+
call void @runtime.printunlock(ptr undef) #4
421+
ret void
422+
}
423+
212424
; Function Attrs: nounwind
213425
define hidden void @main.deferMultiple(ptr %context) unnamed_addr #0 {
214426
entry:
@@ -370,9 +582,6 @@ rundefers.end: ; preds = %rundefers.loophead
370582
br label %recover
371583
}
372584

373-
; Function Attrs: allockind("alloc,zeroed") allocsize(0)
374-
declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #3
375-
376585
; Function Attrs: nounwind
377586
define hidden void @main.deferLoop(ptr %context) unnamed_addr #0 {
378587
entry:
@@ -563,7 +772,7 @@ rundefers.end4: ; preds = %rundefers.loophead7
563772

564773
attributes #0 = { nounwind "target-features"="+armv7-m,+hwdiv,+soft-float,+thumb-mode,-aes,-bf16,-cdecp0,-cdecp1,-cdecp2,-cdecp3,-cdecp4,-cdecp5,-cdecp6,-cdecp7,-crc,-crypto,-d32,-dotprod,-dsp,-fp-armv8,-fp-armv8d16,-fp-armv8d16sp,-fp-armv8sp,-fp16,-fp16fml,-fp64,-fpregs,-fullfp16,-hwdiv-arm,-i8mm,-lob,-mve,-mve.fp,-neon,-pacbti,-ras,-sb,-sha2,-vfp2,-vfp2sp,-vfp3,-vfp3d16,-vfp3d16sp,-vfp3sp,-vfp4,-vfp4d16,-vfp4d16sp,-vfp4sp" }
565774
attributes #1 = { "target-features"="+armv7-m,+hwdiv,+soft-float,+thumb-mode,-aes,-bf16,-cdecp0,-cdecp1,-cdecp2,-cdecp3,-cdecp4,-cdecp5,-cdecp6,-cdecp7,-crc,-crypto,-d32,-dotprod,-dsp,-fp-armv8,-fp-armv8d16,-fp-armv8d16sp,-fp-armv8sp,-fp16,-fp16fml,-fp64,-fpregs,-fullfp16,-hwdiv-arm,-i8mm,-lob,-mve,-mve.fp,-neon,-pacbti,-ras,-sb,-sha2,-vfp2,-vfp2sp,-vfp3,-vfp3d16,-vfp3d16sp,-vfp3sp,-vfp4,-vfp4d16,-vfp4d16sp,-vfp4sp" }
566-
attributes #2 = { nocallback nofree nosync nounwind willreturn }
775+
attributes #2 = { nocallback nofree nounwind willreturn }
567776
attributes #3 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+armv7-m,+hwdiv,+soft-float,+thumb-mode,-aes,-bf16,-cdecp0,-cdecp1,-cdecp2,-cdecp3,-cdecp4,-cdecp5,-cdecp6,-cdecp7,-crc,-crypto,-d32,-dotprod,-dsp,-fp-armv8,-fp-armv8d16,-fp-armv8d16sp,-fp-armv8sp,-fp16,-fp16fml,-fp64,-fpregs,-fullfp16,-hwdiv-arm,-i8mm,-lob,-mve,-mve.fp,-neon,-pacbti,-ras,-sb,-sha2,-vfp2,-vfp2sp,-vfp3,-vfp3d16,-vfp3d16sp,-vfp3sp,-vfp4,-vfp4d16,-vfp4d16sp,-vfp4sp" }
568777
attributes #4 = { nounwind }
569778
attributes #5 = { nounwind returns_twice }

0 commit comments

Comments
 (0)