还咋优化?我是说Go程序

Go语言是一个极容易上手的语言,而且Go程序的优化套路基本上被大家莫得清清楚楚的,如果你有心,可以在互联网上搜出很多Go程序优化的技巧,有些文章可能只介绍了几个优化点,有些文章从CPU的架构到Slice预分配,再到通过pprof找性能的瓶颈点等等全面介绍Go程序的优化,所以说可见的手段基本上被大家摸得门清,最近老貘出了一道题,如下所示,可以看到大家对Go语言的优化已经摸的多深了。

const N = 1000
var a [N]int

//go:noinline
func g0(a *[N]int) {
 for i := range a {
  a[i] = i // line 12
 }
}

//go:noinline
func g1(a *[N]int) {
 _ = *a // line 18
 for i := range a {
  a[i] = i // line 20
 }
}

Go 官方也没闲着。虽然Go语言创立之初也并没有目标要和C++语言打平性能,但是Go团队对Go语言的编译和运行时优化也一直在进行着。

最近,Go语言也正在新加两个性能优化的特性,一个是cmd/compile: profile-guided optimization[1], 这个提案[2]已经被接受, 后续功能初步成型后我们再介绍。另外一个增加memory arena[3]

除了大家常见的通用语言的优化外,影响Go程序性能最大的问题之一就是垃圾回收,所以使用C++、Rust开发的程序员diss Go程序的原因之一。不过这也是垃圾回收编程语言无法绕开的特性,基本上无可避免的带有STW的开销,即使没有STW,垃圾回收时也会耗资源进行对象的便利和检查,所以理论上来说Go性能相比较C+/Rust语言性能总会差一些,除非你禁用垃圾回收、纯粹做CPU计算。

Debian的 benchmark's game网站测试和公布了好多语言的一些场景的性能比较,比如下面这个是Rust和Go的几个实现版本的性能比较:

1720abe58d641f3a9ba6965ec4234002.png

可以看到在这个二叉树的场景下Go的性能比Rust的也差很多。不过性能最好的Rust实现使用arena的内存分配:

use bumpalo::Bump;
use rayon::prelude::*;

#[derive(Debug, PartialEq, Clone, Copy)]
struct Tree<'a> {
    left: Option<&'a Tree<'a>>,
    right: Option<&'a Tree<'a>>,
}

fn item_check(tree: &Tree) -> i32 {
    if let (Some(left), Some(right)) = (tree.left, tree.right) {
        1 + item_check(right) + item_check(left)
    } else {
        1
    }
}

fn bottom_up_tree<'r>(arena: &'r Bump, depth: i32) -> &'r Tree<'r> {
    let tree = arena.alloc(Tree { left: None, right: None });
    if depth > 0 {
        tree.right = Some(bottom_up_tree(arena, depth - 1));
        tree.left = Some(bottom_up_tree(arena, depth - 1));
    }
    tree
}
...

arena是一个内存池的技术,一般来说arena会创建一个大的连续内存块,该内存块只需要预先分配一次,在这块内存上的创建和释放都是手工执行的。

Go语言准备新加入 arena 的功能,并在标准库提供一个新的包: arena。当前这个提案[4]还是holding的状态,但是相关的代码已经陆陆续续地提到master分支了,所以说配批准也基本跑不了了,应该在Go 1.20,也就是明年春季的版本中尝试使用了。(当然也有开发者对Go的这种做法不满,因为外部开发者提出这种想法基本上被驳回或者不被关注,而Go团队的人有这想法就可以立马实现,甚至提案还没批准)。

arena当前提供了几个方法:

  • NewArena(): 创建一个Arena, 你可以创建多个Arena, 批量创建一批对象,统一手工释放。它不是线程安全的。

  • Free(): 释放Arena以及它上面创建出来的所有的对象。释放的对象你不应该再使用了,否则可能会导致意想不到的错误。

  • New[T any](a *Arena "T any") *T:创建一个对象

  • MakeSlice[T any](a *Arena, len, cap int "T any") []T: 在Arena创建一个Slice。

  • Clone[T any](s T "T any"): 克隆一个Arena上对象,只能是指针、slice或者字符串。如果传入的对象不是在Arena分配的,直接原对象返回,否则脱离Arena创建新的对象。

当前还没有实现MakeMapMakeChan这样在Arena上创建map和channel的方法,后续可能会加上。

arena的功能为一组Go对象创建一块内存,手工整体一次性的释放,可以避免垃圾回收。毕竟,我们也提到了,垃圾回收是Go程序的最大的性能杀手之一。

官方建议在批量创建大量的Go对象的时候,每次能以Mib分配内存的场景下使用更有效,甚至他们找到了一个场景:protobuf的反序列化。

因为涉及到垃圾回收、内存分配的问题,所以这个功能实现起来也并不简单,涉及到对运行时代码的改造。不考虑垃圾回收对arena的处理, arena主要的实现在在运行时的arena.go[5]中。因为这个功能还在开发之中,或许这个文件还会有变更。

接下来,我们使用debian benchmark's game的二叉树的例子,对使用arena和不使用arena的情况下做一个比较:

package main

import (
 "arena"
 "flag"
 "fmt"
 "strconv"
 "time"
)

// gotip run -tags "goexperiment.arenas" main.go -arena 21
// GOEXPERIMENT=arenas gotip run main.go -arena 21

var n = 0

type Node struct {
 left, right *Node
 value       []byte
}

func bottomUpTree(depth int) *Node {
 if depth <= 0 {
  return &Node{}
 }
 return &Node{bottomUpTree(depth - 1), bottomUpTree(depth - 1), make([]byte, 128, 128)}
}

func bottomUpTreeWithArena(depth int, a *arena.Arena) *Node {
 node := arena.New[Node](a "Node")
 node.value = arena.MakeSlice[byte](a, 128, 128 "byte")

 if depth <= 0 {
  return node
 }

 node.left = bottomUpTreeWithArena(depth-1, a)
 node.right = bottomUpTreeWithArena(depth-1, a)

 return node
}

func (n *Node) itemCheck() int {
 if n.left == nil {
  return 1
 }
 return 1 + n.left.itemCheck() + n.right.itemCheck()
}

const minDepth = 4

var useArena = flag.Bool("arena", false, "use arena")

func main() {
 flag.Parse()
 if flag.NArg() > 0 {
  n, _ = strconv.Atoi(flag.Arg(0))
 }

 appStart := time.Now()
 defer func() {
  fmt.Printf("benchmark took: %v\n", time.Since(appStart))
 }()

 if *useArena {
  maxDepth := n
  if minDepth+2 > n {
   maxDepth = minDepth + 2
  }
  stretchDepth := maxDepth + 1

  a := arena.NewArena()
  start := time.Now()
  check := bottomUpTreeWithArena(stretchDepth, a).itemCheck()
  a.Free()
  fmt.Printf("stretch tree of depth %d\t check: %d, took: %v\n", stretchDepth, check, time.Since(start))

  a = arena.NewArena()
  longLiveStart := time.Now()
  longLivedTree := bottomUpTreeWithArena(maxDepth, a)
  defer a.Free()

  for depth := minDepth; depth <= maxDepth; depth += 2 {
   iterations := 1 << uint(maxDepth-depth+minDepth)
   check = 0

   start := time.Now()
   for i := 1; i <= iterations; i++ {
    a := arena.NewArena()
    check += bottomUpTreeWithArena(depth, a).itemCheck()
    a.Free()
   }
   fmt.Printf("%d\t trees of depth %d\t check: %d, took: %v\n", iterations, depth, check, time.Since(start))
  }
  fmt.Printf("long lived tree of depth %d\t check: %d, took: %v\n", maxDepth, longLivedTree.itemCheck(), time.Since(longLiveStart))
 } else {
  maxDepth := n
  if minDepth+2 > n {
   maxDepth = minDepth + 2
  }
  stretchDepth := maxDepth + 1

  start := time.Now()
  check := bottomUpTree(stretchDepth).itemCheck()
  fmt.Printf("stretch tree of depth %d\t check: %d, took: %v\n", stretchDepth, check, time.Since(start))

  longLiveStart := time.Now()
  longLivedTree := bottomUpTree(maxDepth)

  for depth := minDepth; depth <= maxDepth; depth += 2 {
   iterations := 1 << uint(maxDepth-depth+minDepth)
   check = 0

   start := time.Now()
   for i := 1; i <= iterations; i++ {
    check += bottomUpTree(depth).itemCheck()
   }
   fmt.Printf("%d\t trees of depth %d\t check: %d, took: %v\n", iterations, depth, check, time.Since(start))
  }
  fmt.Printf("long lived tree of depth %d\t check: %d, took: %v\n", maxDepth, longLivedTree.itemCheck(), time.Since(longLiveStart))
 }
}

这段程序中我们使用-arena参数控制要不要使用arena。首先你必须安装或者更新gotip到最新版(如果你已经安装了gotip, 执行gotip downloamd,如果还未安装,请先go install golang.org/dl/gotip@latest)。

  • 启用-arena: 运行GOEXPERIMENT=arenas gotip run -arena main.go 21

  • 不启用-arena: 运行GOEXPERIMENT=arenas gotip run -arena=false main.go 21

不过这个特性还在开发之中,功能还不完善。

我在MacOS上测试,使用arena性能会有明显的提升,而在windows下测试,性能反而下降了。

cdff332a0ce4e46ebd6d3089e7fbbab9.png

参考资料

[1]

cmd/compile: profile-guided optimization: https://github.com/golang/go/issues/55022#

[2]

提案: https://github.com/golang/proposal/blob/master/design/55022-pgo-implementation.md

[3]

memory arena: https://github.com/golang/go/issues/51317

[4]

提案: https://github.com/golang/go/issues/51317

[5]

arena.go: https://github.com/golang/go/blob/26b48442569102226baba1d9b4a83aaee3d06611/src/runtime/arena.go


2022 GopherChina大会报名火热进行中!

扫描下方二维码即可报名参与哦~大会合作、现场招聘及企业购票等事宜请联系微信:18516100522

戳这里 Go !
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值