go性能优化 | 青训营笔记

181 阅读2分钟

为什么要做优化

这是一个速度决定一切的年代,只要我们的生活还在继续数字化,线下的流程与系统就在持续向线上转移,在这个转移过程中,我们会碰到持续的性能问题。

互联网公司本质是将用户共通的行为流程进行了集中化管理,通过中心化的信息交换达到效率提升的目的,同时用规模效应降低了数据交换的成本。

用人话来讲,公司希望的是用尽量少的机器成本来赚取尽量多的利润。利润的提升与业务逻辑本身相关,与技术关系不大。而降低成本则是与业务无关,纯粹的技术话题。这里面最重要的主题就是“性能优化”。

如果业务的后端服务规模足够大,那么一个程序员通过优化帮公司节省的成本,或许就可以负担他十年的工资了。

go性能优化方法与思考

内存申请/提前预估容量

  • slice/map初始化尽量估计好长度,能有效减少内存分配次数,优化很明显
  • 尽量规避使用append,因为需要值拷贝,且涉及到重新申请内存,可能会发生逃逸(Mac环境下测试:当append之后的slice长度大于8时会被分配到堆上)
  • 如果无法预估,一般场景下可以考虑申请足够大的空间,并在场景允许的情况下优先考虑复用slice
func useCap1() {
   arr := make([]int, 0, 2048)
   for i := 0; i < 2048; i++ {
      arr = append(arr, i)
   }
}

func useCap2() {
   arr := make([]int, 2048)
   for i := 0; i < 2048; i++ {
       arr[i] = i
   }
}

func noCap() {
   var arr []int
   for i := 0; i < 2048; i++ {
      arr = append(arr, i)
   }
}

defer

defer是提高可读性和避免资源未释放的非常有用的关键字。例如,当我们打开一个文件进行读取时,我们需要在结束读取时关闭它。如果没有defer关键字,我们必须确保在函数的每个返回点之前关闭文件。这样很容易出错,因为很容易在任何一个return语句前忘记关闭文件。defer则通过一行代码解决了这个问题。

func findHelloWorld(filename string) error {\
file, err := os.Open(filename)\
if err != nil {\
return err\
}\
defer file.Close()

        scanner := bufio.NewScanner(file)  
        for scanner.Scan() {  
                if scanner.Text() == "hello, world!" {  
                        return nil  
                }  
        }  

        if err := scanner.Err(); err != nil {  
                return err  
        }  
          
        return errors.New("Didn't find hello world")  

}
  • 乍一看,我们会认为defer可能会被编译器完全优化掉。如果我只是在函数的开头使用了defer语句,编译器确实可以通过在每一个return语句之前插入defer内容来实现。但是实际情况往往更复杂。比如,我们可以在条件语句或者循环中添加defer。第一种情况可能需要编译器找到应用defer语句的条件分支. 编译器还需要检查panic的情况,因为这也是函数退出执行的一种情况。通过静态编译提供这个功能(defer)似乎是不太可能的。

锁冲突严重,导致吞吐量瓶颈

进行锁优化的思路无非就一个“拆”和一个“缩”字:

  • 拆:将锁粒度进行拆分,比如全局锁,我能不能把锁粒度拆分为连接粒度的锁;如果是连接粒度的锁,那我能不能拆分为请求粒度的锁;在 logger fd 或 net fd 上加的锁不太好拆,那么我们增加一些客户端,比如从 1-> 100,降低锁的冲突是不是就可以了。
  • 缩:缩小锁的临界区,比如业务允许的前提下,可以把 syscall 移到锁外面;比如我们只是想要锁 map,但是却不小心锁了连接读写的逻辑,或许简单地用 sync.Map 来代替 map Lock,defer Unlock 就能简单地缩小临界区了。