| // run |
| |
| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Test that locks don't go quadratic due to runtime hash table collisions. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "fmt" |
| "log" |
| "os" |
| "runtime" |
| "runtime/pprof" |
| "sync" |
| "time" |
| ) |
| |
| const debug = false |
| |
| // checkLinear asserts that the running time of f(n) is at least linear but sub-quadratic. |
| // tries is the initial number of iterations. |
| func checkLinear(typ string, tries int, f func(n int)) { |
| // Depending on the machine and OS, this test might be too fast |
| // to measure with accurate enough granularity. On failure, |
| // make it run longer, hoping that the timing granularity |
| // is eventually sufficient. |
| |
| timeF := func(n int) time.Duration { |
| t1 := time.Now() |
| f(n) |
| return time.Since(t1) |
| } |
| |
| n := tries |
| fails := 0 |
| var buf bytes.Buffer |
| inversions := 0 |
| for { |
| t1 := timeF(n) |
| t2 := timeF(2 * n) |
| if debug { |
| println(n, t1.String(), 2*n, t2.String()) |
| } |
| fmt.Fprintf(&buf, "%d %v %d %v (%.1fX)\n", n, t1, 2*n, t2, float64(t2)/float64(t1)) |
| // should be 2x (linear); allow up to 3x |
| if t1*3/2 < t2 && t2 < t1*3 { |
| return |
| } |
| if t2 < t1 { |
| if inversions++; inversions >= 5 { |
| // The system must be overloaded (some builders). Give up. |
| return |
| } |
| continue // try again; don't increment fails |
| } |
| // Once the test runs long enough for n ops, |
| // try to get the right ratio at least once. |
| // If many in a row all fail, give up. |
| if fails++; fails >= 5 { |
| // If 2n ops run in under a second and the ratio |
| // doesn't work out, make n bigger, trying to reduce |
| // the effect that a constant amount of overhead has |
| // on the computed ratio. |
| if t2 < time.Second*4/10 { |
| fails = 0 |
| n *= 2 |
| continue |
| } |
| panic(fmt.Sprintf("%s: too slow: %d ops: %v; %d ops: %v\n\n%s", |
| typ, n, t1, 2*n, t2, buf.String())) |
| } |
| } |
| } |
| |
| const offset = 251 // known size of runtime hash table |
| |
| const profile = false |
| |
| func main() { |
| if profile { |
| f, err := os.Create("lock.prof") |
| if err != nil { |
| log.Fatal(err) |
| } |
| pprof.StartCPUProfile(f) |
| defer pprof.StopCPUProfile() |
| } |
| |
| checkLinear("lockone", 1000, func(n int) { |
| ch := make(chan int) |
| locks := make([]sync.RWMutex, offset+1) |
| for i := 0; i < n; i++ { |
| go func() { |
| locks[0].Lock() |
| ch <- 1 |
| }() |
| } |
| time.Sleep(1 * time.Millisecond) |
| |
| go func() { |
| for j := 0; j < n; j++ { |
| locks[1].Lock() |
| locks[offset].Lock() |
| locks[1].Unlock() |
| runtime.Gosched() |
| locks[offset].Unlock() |
| } |
| }() |
| |
| for j := 0; j < n; j++ { |
| locks[1].Lock() |
| locks[offset].Lock() |
| locks[1].Unlock() |
| runtime.Gosched() |
| locks[offset].Unlock() |
| } |
| |
| for i := 0; i < n; i++ { |
| <-ch |
| locks[0].Unlock() |
| } |
| }) |
| |
| if runtime.GOARCH == "arm" && os.Getenv("GOARM") == "5" { |
| // lockmany reliably fails on the linux-arm-arm5spacemonkey |
| // builder. See https://golang.org/issue/24221. |
| return |
| } |
| |
| checkLinear("lockmany", 1000, func(n int) { |
| locks := make([]sync.RWMutex, n*offset+1) |
| |
| var wg sync.WaitGroup |
| for i := 0; i < n; i++ { |
| wg.Add(1) |
| go func(i int) { |
| locks[(i+1)*offset].Lock() |
| wg.Done() |
| locks[(i+1)*offset].Lock() |
| locks[(i+1)*offset].Unlock() |
| }(i) |
| } |
| wg.Wait() |
| |
| go func() { |
| for j := 0; j < n; j++ { |
| locks[1].Lock() |
| locks[0].Lock() |
| locks[1].Unlock() |
| runtime.Gosched() |
| locks[0].Unlock() |
| } |
| }() |
| |
| for j := 0; j < n; j++ { |
| locks[1].Lock() |
| locks[0].Lock() |
| locks[1].Unlock() |
| runtime.Gosched() |
| locks[0].Unlock() |
| } |
| |
| for i := 0; i < n; i++ { |
| locks[(i+1)*offset].Unlock() |
| } |
| }) |
| } |