go 结构体 转string_Go: 应该使用指针还是结构体副本?

26edd3a23f7938704d86a8ad64825cad.png

对于许多Go开发人员而言,就性能而言,系统地使用指针共享结构体而不是副本本身似乎是最佳选择。

为了理解使用指针而不是结构体副本的影响,我们将回顾两个用例。

密集分配数据

以下面结构体为例

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 ./... -bench=BenchmarkMemoryHeap -benchmem -run=^$ -count=10 > head.txt && benchstat head.txt
go test ./... -bench=BenchmarkMemoryStack -benchmem -run=^$ -count=10 > stack.txt && benchstat stack.txt

测试结果如下

name          time/op
MemoryHeap-4  75.0ns ± 5%
name          alloc/op
MemoryHeap-4   96.0B ± 0%
name          allocs/op
MemoryHeap-4    1.00 ± 0%
------------------
name           time/op
MemoryStack-4  8.93ns ± 4%
name           alloc/op
MemoryStack-4   0.00B
name           allocs/op
MemoryStack-4    0.00

这里使用副本比指针快8倍。

为了理解,让我们看看基于trace生成的图:

3154f18b63d72c80c85980d3a3100333.png

使用结构体副本时

a8db18f3390d90c49cbfe005e8258c8a.png

使用指针时

第一张图非常简单。因为它没有使用堆,也就没有gc和其他额外的goroutine。对于第二张图,指针的使用迫使go编译器将变量逃逸到堆中并产生gc压力。如果放大图,我们可以发现gc占据了进程的重要部分:

0012ceb7cd052f032ed1ba6c0ba7246b.png

我们可以发现gc每4ms执行一次。如果我们再次放大,我们可以发现更多细节:

8d064a103c8553a9683cd5f21d0adde6.png

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

即使这个例子有点极端,我们也可以看到在堆而不是栈上分配变量的代价是多么昂贵。在我们的示例中,在栈上复制结构体比堆上共享指针快的多。

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

name        time/op
MemoryHeap  114ns ± 4%
name        alloc/op
MemoryHeap  96.0B ± 0%
name        allocs/op
MemoryHeap   1.00 ± 0%
------------------
name         time/op
MemoryStack  8.77ns ± 5%
name         alloc/op
MemoryStack   0.00B
name         allocs/op
MemoryStack    0.00

堆分配测试从75ns/op 降低到 114ns/op。

密集方法调用

对于第二个用例,我们将向结构中添加两个空方法:

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

使用副本方式调用:

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

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

使用指针方式调用:

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

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

和预期大相径庭的结果:

name          time/op
MemoryHeap-4  301µs ± 4%
name          alloc/op
MemoryHeap-4  0.00B
name          allocs/op
MemoryHeap-4   0.00
------------------
name           time/op
MemoryStack-4  595µs ± 2%
name           alloc/op
MemoryStack-4  0.00B
name           allocs/op
MemoryStack-4   0.00

结论

在go中使用指针而不是结构的副本并不总是件好事。

本文编译自 Go: Should I Use a Pointer instead of a Copy of my Struct? https:// medium.com/a-journey-wi th-go/go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct-44b43b104963

8e2ea83b8e2df37e8c9d9bb7774fff70.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值