Golang性能优化技巧(三)

概述

本文主要介绍闭包、channel和接口相关的性能开销

golang中提供了很多语法糖,但它们都有较复杂的实现方式,因此使用它们时需要小心,在对性能要求较高的场景尽量不要使用

匿名函数的性能开销

代码

import "testing"

func test(x int) int {
	return x * 2
}

// 普通函数调用
func BenchmarkTest(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = test(i)
	}
}

// 匿名函数调用
func BenchmarkAnomy(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = func(x int) int {
			return x * 2
		}(i)
	}
}

// 闭包
func BenchmarkClosure(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = func() int {
			return i * 2
		}()
	}
}

运行

% go test -v -bench . -benchmem
goos: darwin
goarch: amd64
BenchmarkTest
BenchmarkTest-8      	1000000000	         0.542 ns/op	       0 B/op	       0 allocs/op
BenchmarkAnomy
BenchmarkAnomy-8     	1000000000	         0.543 ns/op	       0 B/op	       0 allocs/op
BenchmarkClosure
BenchmarkClosure-8   	830409748	         1.44 ns/op	       0 B/op	       0 allocs/op

结论

  1. 匿名函数的时间开销和普通函数相近,但使用闭包后性能变差了很多。在不同环境下,单独使用匿名函数的性能需要评估;
  2. 无论是匿名函数还是闭包,在本例中并不存在内存逃逸的情形;

闭包引起的gc和数据竞争

代码

  5 func closure_test() {
  6         x := 100
  7         go func() {
  8                 x += 1
  9         }()
 10         x += 1
 11         fmt.Println(x)
 12 }

编译

% go build -gcflags "-m"
# _/Users/didi/test/go/5
./main.go:11:13: inlining call to fmt.Println
./main.go:7:5: can inline closure_test.func1
./main.go:14:6: can inline main
./main.go:6:2: moved to heap: x。// 重点
./main.go:7:5: func literal escapes to heap
./main.go:11:13: x escapes to heap
./main.go:11:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape

查看数据竞争

% go build -race -o test && ./test
101
==================
WARNING: DATA RACE
Read at 0x00c00012c008 by goroutine 7:
  main.closure_test.func1()
      /Users/didi/test/go/5/main.go:8 +0x38

Previous write at 0x00c00012c008 by main goroutine:
  main.closure_test()
      /Users/didi/test/go/5/main.go:10 +0x9e
  main.main()
      /Users/didi/test/go/5/main.go:15 +0x2f

Goroutine 7 (running) created at:
  main.closure_test()
      /Users/didi/test/go/5/main.go:7 +0x7a
  main.main()
      /Users/didi/test/go/5/main.go:15 +0x2f
==================
Found 1 data race(s)

结论

  1. 闭包常伴随着内存逃逸,给gc增加负担;
  2. 不良好的编码习惯使得闭包容易引起数据竞争;

Channel的性能开销

Channel, mutex, atomic均有起到同步的功能,但它们的性能开销差异较大

代码

func chanCounter() chan int {
	ch := make(chan int)
	go func() {
		for x := 1; ; x++ {
			ch <- x
		}
	}()
	return ch
}

func mutexCounter() func() int {
	var m sync.Mutex
	var x int
	return func() (n int) {
		m.Lock()
		x++
		n = x
		m.Unlock()
		return
	}
}

func atomicCounter() func() int32 {
	var x int32
	return func() int32 {
		return atomic.AddInt32(&x, 1)
	}
}

func BenchmarkChanCounter(b *testing.B) {
	c := chanCounter()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = <-c
	}
}

func BenchmarkMutexCounter(b *testing.B) {
	f := mutexCounter()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = f()
	}
}

func BenchmarkAtomicCounter(b *testing.B) {
	f := atomicCounter()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_ = f()
	}
}

运行

% go test -v -bench . -benchmem
goos: darwin
goarch: amd64
BenchmarkChanCounter
BenchmarkChanCounter-8     	 6286567	       191 ns/op	       0 B/op	       0 allocs/op
BenchmarkMutexCounter
BenchmarkMutexCounter-8    	88040665	        14.0 ns/op	       0 B/op	       0 allocs/op
BenchmarkAtomicCounter
BenchmarkAtomicCounter-8   	174345294	         6.84 ns/op	       0 B/op	       0 allocs/op

结论

  1. channel的内部实现机制最复杂,因而性能开销也最大;
  2. channel通常作为两个模块间的同步,对于简单场景尽量使用mutex和atomic;
  3. 在有关键区域的地方使用mutex,仅仅计数的话使用atomic就够了,性能也更好;

接口的性能开销

代码

// 定义接口
type Tester interface {
	Test(int)
}

// 实现接口的类型
type Data struct {
	x int
}

func (o *Data) Test(a int) {
	o.x += a
}

// 函数通过具体类型调用
func call(d *Data) {
	d.Test(100)
}

// 函数通过接口类型调用
func ifaceCall(t Tester) {
	t.Test(100)
}

// 压力测试直接通过具体类型调用的性能开销
func BenchmarkCall(b *testing.B) {
	for i := 0; i < b.N; i++ {
		call(&Data{x: 100})
	}
}

// 压力测试通过接口调用的性能开销
func BenchmarkIfaceCall(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ifaceCall(&Data{x: 100})
	}
}

运行

% go test -v -bench . -benchmem
goos: darwin
goarch: amd64
BenchmarkCall-8            	1000000000	         0.275 ns/op	       0 B/op	       0 allocs/op
BenchmarkIfaceCall
BenchmarkIfaceCall-8       	77994258	        14.5 ns/op	       8 B/op	       1 allocs/op

结论

  1. 使用接口调用比具体类型调用性能差五十倍之多。主要原因包括:程序指令由静态绑定改变为动态绑定,同时编译期间的一些优化措施无法在接口上实现,比如函数內联;
  2. 使用接口调用时,会在堆上分配内存空间,说明发生了内存逃逸,给gc增加了负担;另一方面,使用具体类型调用时,虽然也使用了指针,但是对象被分配到了栈上而非堆上;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值