每三日一GO -- 应该使用指针代替结构体吗

原文地址:https://medium.com/a-journey-with-go

对于许多 Go 开发人员来说,用指针传递结构体来代替直接传递结构体(直接传递会copy一份结构体),能提高程序的性能。

为了理解使用指针而不是结构副本的影响,我们看一下两个用例。

密集的数据分配

让我们举一个简单的例子,说明何时要为其值共享结构体:

type S struct {
   a, b, c int64
   d, e, f string
   g, h, i float64
}

这是一个可以通过复制或指针共享的基本结构:

func byCopy() S {
   return S{
      a: 1, b: 1, c: 1,
      e: "foo", f: "foo",
      g: 1.0, h: 1.0, i: 1.0,
   }
}

func byPointer() *S {
   return &S{
      a: 1, b: 1, c: 1,
      e: "foo", f: "foo",
      g: 1.0, h: 1.0, i: 1.0,
   }
}

基于这 2 种方法,我们现在可以编写 2 个基准测试,其中一个通过复制传递结构:

func BenchmarkMemoryStack(b *testing.B) {
   var s S

   f, err := os.Create("stack.out")
   if err != nil {
      panic(err)
   }
   defer f.Close()

   err = trace.Start(f)
   if err != nil {
      panic(err)
   }

   for i := 0; i < b.N; i++ {
      s = byCopy()
   }

   trace.Stop()

   b.StopTimer()

   _ = fmt.Sprintf("%v", s.a)
}

另一个非常相似,当它通过指针传递时:

func BenchmarkMemoryHeap(b *testing.B) {
   var s *S

   f, err := os.Create("heap.out")
   if err != nil {
      panic(err)
   }
   defer f.Close()

   err = trace.Start(f)
   if err != nil {
      panic(err)
   }

   for i := 0; i < b.N; i++ {
      s = byPointer()
   }

   trace.Stop()

   b.StopTimer()

   _ = fmt.Sprintf("%v", s.a)
}

让我们运行基准测试:

go test *.go -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > heap.txt && benchstat heap.txt
go test *.go -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt

第一个测试结果

name          time/op
MemoryHeap-8  45.6ns ± 7%

name          alloc/op
MemoryHeap-8   96.0B ± 0%

name          allocs/op
MemoryHeap-8    1.00 ± 0%

goos: darwin
goarch: amd64
BenchmarkMemoryHeap-8   	24628110	        45.7 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	25671453	        45.4 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	26333338	        43.3 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	28119717	        48.6 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	22179019	        48.6 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	27263764	        43.8 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	26222460	        47.5 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	22571803	        44.9 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	26250232	        45.8 ns/op	      96 B/op	       1 allocs/op
BenchmarkMemoryHeap-8   	27823093	        42.7 ns/op	      96 B/op	       1 allocs/op
PASS
ok  	command-line-arguments	12.247s

第二个测试结果

name           time/op
MemoryStack-8  7.67ns ± 5%

name           alloc/op
MemoryStack-8   0.00B     

name           allocs/op
MemoryStack-8    0.00
goos: darwin
goarch: amd64
BenchmarkMemoryStack-8   	157662150	         7.83 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	152284056	         7.51 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	159412387	         7.67 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	143585290	         9.64 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	155647315	         7.64 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	158821363	         7.57 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	133672618	         8.04 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	154818850	         7.58 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	157884108	         7.70 ns/op	       0 B/op	       0 allocs/op
BenchmarkMemoryStack-8   	135765003	         7.52 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	command-line-arguments	19.972s

从结果来看第二种比第一种快了将近7倍,不是说指针传递会更快吗。从测试结果来看,主要是因为在堆上分配内存,导致更长的耗时。

在继续看trace的测试分析

go tool trace stack.out

在这里插入图片描述

很明显stack Heap行数据只有开始的时候有一点,这应该是测试代码自带的一些内存分配。而通过值返回结构体并不会在堆上分配内存。也不会有goroutine去回收内存

go tool trace heap.out

在这里插入图片描述

对于第二个图,指针的使用迫使 go 编译器将变量分配到堆中,并触发了多次的垃圾回收。 如果我们放大图表,我们可以看到垃圾回收占据了进程的重要部分。

在这里插入图片描述
可以看出使用指针的话会导致GC和内存分配,这是最终导致测试结果比较慢的原因。

蓝色、粉色和红色是垃圾收集器的阶段,而棕色是与堆上的分配相关的(图中用“runtime.bgsweep”标记):

即使这个例子有点极端,我们也可以看到在堆上而不是在堆栈上分配一个变量是多么昂贵。 在我们的示例中,代码在堆栈上分配结构并复制它比在堆上分配它并共享其地址要快得多。

如果我们将处理器限制为 1 且 GOMAXPROCS=1,情况可能会更糟:

stack:
name           time/op
MemoryStack-8  7.71ns ± 1%

name           alloc/op
MemoryStack-8   0.00B     

name           allocs/op
MemoryStack-8    0.00

---

heap:
name          time/op
MemoryHeap-8  80.5ns ± 2%

name          alloc/op
MemoryHeap-8   96.0B ± 0%

name          allocs/op
MemoryHeap-8    1.00 ± 0%

差距变成了10倍

密集的函数调用

对于第二个用例,我们将向我们的结构添加两个空方法,并对我们的基准测试稍作调整:

func (s S) stack(s1 S) {}

func (s *S) heap(s1 *S) {}

在堆栈上分配的基准测试将创建一个结构并通过复制传递它:

func BenchmarkMemoryStackFunc(b *testing.B) {
	var s S
	var s1 S

	s = byCopy()
	s1 = byCopy()
	for i := 0; i < b.N; i++ {
		for j := 0; j < 1000000; j++ {
			s.stack(s1)
		}
	}
}

func BenchmarkMemoryHeapFunc(b *testing.B) {
	var s *S
	var s1 *S

	s = byPointer()
	s1 = byPointer()
	for i := 0; i < b.N; i++ {
		for j := 0; j < 1000000; j++ {
			s.heap(s1)
		}
	}
}

正如预期的那样,通过指针传递参数会更快:

name               time/op
MemoryStackFunc-8  548µs ± 2%

name               alloc/op
MemoryStackFunc-8  0.00B     

name               allocs/op
MemoryStackFunc-8   0.00

------

name              time/op
MemoryHeapFunc-8  278µs ± 2%

name              alloc/op
MemoryHeapFunc-8  0.00B     

name              allocs/op
MemoryHeapFunc-8   0.00

总结

在 go 中使用指针而不是结构体的副本并不总是一件好事。在频繁申请变量的地方可以使用结构体副本,而传递参数时可以使用指针。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值