fix unwind tests

This commit is contained in:
Felix Geisendörfer 2021-04-12 10:30:54 +02:00
parent 81352c678a
commit ed2fa54799
4 changed files with 3179 additions and 620 deletions

View file

@ -7,35 +7,17 @@ This directory contains benchmarks to explore which factors impact stack unwindi
The benchmark below shows that stack unwinding has O(N) complexity with regard to the number of call frames on the stack:
```
BenchmarkStackDepth/8-12 2208600 547.2 ns/op
BenchmarkStackDepth/16-12 1447922 810.8 ns/op
BenchmarkStackDepth/32-12 915291 1338 ns/op
BenchmarkStackDepth/64-12 488719 2366 ns/op
BenchmarkStackDepth/128-12 264735 4462 ns/op
BenchmarkStackDepth/256-12 137575 8643 ns/op
BenchmarkStackDepth/512-12 68355 17316 ns/op
BenchmarkStackDepth/1024-12 34710 34810 ns/op
BenchmarkStackDepth/8-12 1968214 612.2 ns/op
BenchmarkStackDepth/16-12 975457 1184 ns/op
BenchmarkStackDepth/32-12 572706 2101 ns/op
BenchmarkStackDepth/64-12 333598 3596 ns/op
BenchmarkStackDepth/128-12 182450 6561 ns/op
BenchmarkStackDepth/256-12 94783 12548 ns/op
BenchmarkStackDepth/512-12 48439 24471 ns/op
BenchmarkStackDepth/1024-12 24884 48310 ns/op
```
## Result 2: O(N) Function Size
Perhaps suprisingly, stack unwinding is also O(N) with regard to the size of the generated machine code for the function:
```
BenchmarkFunctionSize/1-12 2562176 462.8 ns/op
BenchmarkFunctionSize/2-12 2509465 484.7 ns/op
BenchmarkFunctionSize/4-12 2356609 504.6 ns/op
BenchmarkFunctionSize/8-12 2095870 568.3 ns/op
BenchmarkFunctionSize/16-12 1778889 669.7 ns/op
BenchmarkFunctionSize/32-12 1396009 856.0 ns/op
BenchmarkFunctionSize/64-12 943807 1269 ns/op
BenchmarkFunctionSize/128-12 516487 2271 ns/op
BenchmarkFunctionSize/256-12 277821 4490 ns/op
```
## Disclaimer
YMMV, and especially the function size also depends on the program counter at which the function is being unwound. All tests were done on my local machine:
Tests were done on my local machine:
```
go test -bench .

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,8 @@ import (
"os"
)
//go:generate go run .
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
@ -14,5 +16,61 @@ func main() {
}
func run() error {
return nil
file, err := os.Create("generated.go")
if err != nil {
return err
}
defer file.Close()
fmt.Fprint(file, `// Code generated by go generate; DO NOT EDIT.
package main
//go:noinline
func stackdepth1(fn func()) { fn() }
`)
max := 1024
for i := 2; i <= max; i++ {
fmt.Fprintf(
file,
"//go:noinline\nfunc stackdepth%d(fn func()) { stackdepth%d(fn) }\n",
i,
i-1,
)
}
fmt.Fprint(file, "\nvar stackdepth = map[int]func(func()) {\n")
for i := 1; i <= max; i++ {
fmt.Fprintf(file, "\t%d: stackdepth%d,\n", i, i)
}
fmt.Fprint(file, "}\n")
// Disabled: Turned out to be a deadend
//for i := 1; i <= max; i *= 2 {
//fmt.Fprintf(file, "//go:noinline\n")
//fmt.Fprintf(file, "func funcsize%d(v0, n int, fn func()) int {\n", i)
//for n := 1; n <= i; n++ {
//fmt.Fprintf(file, "\tv%d := v%d\n", n, n-1)
//fmt.Fprintf(file, "\tv%d = (^v%d + %d)\n", n, n, n)
//fmt.Fprintf(file, "\tif v%d == 23 {\n", n)
//sum := []string{}
//for j := 0; j <= n; j++ {
//sum = append(sum, fmt.Sprintf("v%d", j))
//}
//fmt.Fprintf(file, "\t\treturn %s\n", strings.Join(sum, " + "))
//fmt.Fprintf(file, "\t}\n")
//}
//fmt.Fprintf(file, "\tfn()\n")
//fmt.Fprintf(file, "\treturn -1\n")
//fmt.Fprintf(file, "}\n\n")
//}
//fmt.Fprint(file, "\nvar funcsize = map[int]func(int, int, func()) int {\n")
//for i := 1; i <= max; i *= 2 {
//fmt.Fprintf(file, "\t%d: funcsize%d,\n", i, i)
//}
//fmt.Fprint(file, "}\n")
return file.Close()
}

View file

@ -11,10 +11,10 @@ import (
func BenchmarkStackDepth(b *testing.B) {
for d := 8; d <= 1024; d = d * 2 {
b.Run(fmt.Sprintf("%d", d), func(b *testing.B) {
callers := make([]uintptr, d)
callers := make([]uintptr, d*2)
atStackDepth(d, func() {
for i := 0; i < b.N; i++ {
if n := runtime.Callers(0, callers); n != d {
if n := runtime.Callers(1, callers); n != d {
b.Fatalf("got=%d want=%d", n, d)
}
}
@ -23,598 +23,38 @@ func BenchmarkStackDepth(b *testing.B) {
}
}
// atStackDepth recursively calls itself until the number of stack frames
// reaches the desired depth, and then calls fn.
// atStackDepth calls functions that call each other until there are depth-1
// functions on the stack and then calls fn.
//go:noinline
func atStackDepth(depth int, fn func()) {
pcs := make([]uintptr, depth+10)
n := runtime.Callers(1, pcs)
if n > depth {
panic("depth exceeded")
} else if n < depth {
atStackDepth(depth, fn)
return
}
remaining := depth - runtime.Callers(1, pcs) - 1
if remaining < 1 {
panic("can't simulate desired stack depth: too low")
} else if remaining == 1 {
fn()
}
func BenchmarkFunctionSize(b *testing.B) {
m := map[int]func(func()){
1: dostuff1,
2: dostuff2,
4: dostuff4,
8: dostuff8,
16: dostuff16,
32: dostuff32,
64: dostuff64,
128: dostuff128,
256: dostuff256,
}
var callers = make([]uintptr, 32)
for s := 1; s <= 256; s = s * 2 {
b.Run(fmt.Sprintf("%d", s), func(b *testing.B) {
m[s](func() {
for i := 0; i < b.N; i++ {
runtime.Callers(0, callers)
}
})
})
} else if f, ok := stackdepth[remaining]; !ok {
panic("can't simulate desired stack depth: no map entry")
} else {
f(fn)
}
}
// dostuff is supposed to generate a bunch of machine code that will hopefully
// be inlined into its callers.
func dostuff() int {
m := map[int]string{}
m[0] = "foo"
m[100] = "bar"
return len(m)
}
func dostuff1(fn func()) {
dostuff()
fn()
}
func dostuff2(fn func()) {
dostuff()
dostuff()
fn()
}
func dostuff4(fn func()) {
dostuff()
dostuff()
dostuff()
dostuff()
fn()
}
func dostuff8(fn func()) {
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
fn()
}
func dostuff16(fn func()) {
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
fn()
}
func dostuff32(fn func()) {
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
fn()
}
func dostuff64(fn func()) {
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
fn()
}
func dostuff128(fn func()) {
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
fn()
}
func dostuff256(fn func()) {
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
dostuff()
fn()
}
// disabled: turned out to be a deadend
//func BenchmarkFunctionSize(b *testing.B) {
//var callers = make([]uintptr, 32)
//for s := 1; s <= 1024; s = s * 2 {
//b.Run(fmt.Sprintf("%d", s), func(b *testing.B) {
//called := false
//funcsize[s](0, 0, func() {
//for i := 0; i < b.N; i++ {
//runtime.Callers(0, callers)
//}
//called = true
//})
//if !called {
//b.Fatal("not called")
//}
//})
//}
//}