总结自己遇过的各种问题
一些优化 循环内捕获变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" "sync" ) func main () { wg := &sync.WaitGroup{} for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func () { fmt.Println(i) wg.Done() }() } wg.Wait() }
这里不是输出0到1,可能 是10个10,因为捕获的i在后面被修改了,改成加个复制就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "sync" ) func main () { wg := &sync.WaitGroup{} for i := 0 ; i < 10 ; i++ { i := i wg.Add(1 ) go func () { fmt.Println(i) wg.Done() }() } wg.Wait() }
make slice 的两种写法 1 2 sliceA := make ([]int , 10 ) sliceB := make ([]int , 0 , 10 )
第一种是创建一个slice,容量和长度都是10,可以直接 sliceA[index]
下标访问,初始值是该类型的默认值,比如bool是false,int是0,指针是nil 第二种是创建一个slice,容量是10,长度是0,不能直接下标访问,必须要先 sliceB=append(sliceB, 1)
后,有数据了才能下标访问 slice进行append时不是协程安全的,多协程同时append需要加锁。
time.Since 优化 Since在1.16后做了一次优化,使用了runtime的纳秒计数器,少了一次调用 now(),优化了一次syscall。调用的 runtimeNano()
返回的是runtime的毫秒计数器,不需要系统调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func runtimeNano () int64 func Since (t Time) Duration { var now Time if t.wall&hasMonotonic != 0 { now = Time{hasMonotonic, runtimeNano() - startNano, nil } } else { now = Now() } return now.Sub(t) }
所以既然有内部计时器,我们可以自己整一个优化的耗时计数。
1 2 3 4 5 6 7 8 func runtimeNano () int64 func UseRuntimeNano () int64 { nw := runtimeNano() return runtimeNano() - nw }
下面是 benchmark 的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func runtimeNano () int64 func UseTimeNano () int64 { nw := time.Now() return time.Since(nw).Nanoseconds() } func UseRuntimeNano () int64 { nw := runtimeNano() return runtimeNano() - nw } func BenchmarkTimeNano (b *testing.B) { for i := 0 ; i < b.N; i++ { UseTimeNano() } } func BenchmarkRuntimeNano (b *testing.B) { for i := 0 ; i < b.N; i++ { UseRuntimeNano() } }
benchmark 结果如下:
1 2 3 cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz BenchmarkTimeNano-4 84066586 13.18 ns/op 0 B/op 0 allocs/op BenchmarkRuntimeNano-4 167052464 7.103 ns/op 0 B/op 0 allocs/op
差不多快了一倍
fmt 和 strconv strconv使用的是查表,甚至还有32位系统的取模优化,性能很不错。 fmt使用的是反射,还有各种边界检验,计算量比strconv高很多
benchmark 代码:
1 2 3 4 5 6 7 8 9 10 11 func BenchmarkUseFmt (b *testing.B) { for i := 0 ; i < b.N; i++ { _ = fmt.Sprintf("%d" , i) } } func BenchmarkUseStrconv (b *testing.B) { for i := 0 ; i < b.N; i++ { _ = strconv.Itoa(i) } }
benchmark 结果:
1 2 3 cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz BenchmarkUseFmt-4 10489794 96.20 ns/op 16 B/op 1 allocs/op BenchmarkUseStrconv-4 37340638 32.84 ns/op 7 B/op 0 allocs/op
快了差不多3倍
字符串拼接 和 strings.Builder 和 内存预申请 很常见的优化了,go的string是不可修改的。append时需要拷贝,少量拼接的话无伤大雅,但是量太大的话,建议使用strings.Builder,如果知道最终字符串会有多大,预先申请会带来更高的性能优化。
benchmark 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func BenchmarkSimpleConcat (b *testing.B) { res := "" for i := 0 ; i < b.N; i++ { res += strconv.Itoa(i) } _ = res } func BenchmarkUseStringBuilder (b *testing.B) { sb := &strings.Builder{} for i := 0 ; i < b.N; i++ { sb.WriteString(strconv.Itoa(i)) } _ = sb.String() } func BenchmarkUseStringBuilderWithPreAlloc (b *testing.B) { sb := &strings.Builder{} sb.Grow(b.N * 8 ) for i := 0 ; i < b.N; i++ { sb.WriteString(strconv.Itoa(i)) } _ = sb.String() }
结果:
1 2 3 4 cpu: Intel(R) Core(TM) i5-7300 HQ CPU @ 2.50 GHz BenchmarkSimpleConcat-4 92258 23054 ns/op 223894 B/op 1 allocs/op BenchmarkUseStringBuilder-4 25517612 45.30 ns/op 55 B/op 0 allocs/op BenchmarkUseStringBuilderWithPreAlloc-4 34258104 35.47 ns/op 15 B/op 0 allocs/op
明显快了非常多。同样的,还有bytes.Buffer
,同样的使用方法,可以优化byte的拼接性能。
内存预申请从而实现优化的,标准库里还有 url.PathEscape
和 url.QueryEscape
,两个都是调用内部方法 escape
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func escape (s string , mode encoding) string { spaceCount, hexCount := 0 , 0 for i := 0 ; i < len (s); i++ { c := s[i] if shouldEscape(c, mode) { if c == ' ' && mode == encodeQueryComponent { spaceCount++ } else { hexCount++ } } } if spaceCount == 0 && hexCount == 0 { return s } var buf [64 ]byte var t []byte required := len (s) + 2 *hexCount if required <= len (buf) { t = buf[:required] } else { t = make ([]byte , required) } ... }
sync.pool
使用lock-free的双向队列作为缓存
使用victim作为待gc缓存,每次gc开头,把victim队列的东西清空,然后把local队列转到victim队列
在获取时,优先在local,然后victim,然后去别的p的pool拿,最后使用New。local中优先使用无锁的private(pin了,不会有竞争),没有再从shared拿,可以pushHead/popHead,别的p只能popTail
每个P享有一个pool。G操作pool时,需要锁P
编译相关 race detector 1 2 go run -race go build -race
所有内联tag
所有gc tag
减小二进制文件大小
方便 dlv 调试(关闭优化、关闭内联)
某些技巧 runtime.LockOSThread runtime.LockOSThread()可以把当前goroutine绑定在物理线程上,但是如果在goroutine绑定后没有UnlockOSThread,就会导致goroutine结束时,这条线程被杀掉