Go语言中的性能优化

性能优化,简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。

正确高效使用数据结构

正确使用Slice

1
2
3
4
5
type slice {
    array unsafe.Pointer
    len   int
    cap   int
}

在通过函数传递slice类型的参数时,需要注意由于Go语言默认按值传递参数,调用函数传入的参数与函数内部得到的参数实际上是不相同的。但是slice类型中包含一个指向真正数据存储位置的指针,因而在调用参数类型为slice的函数时,对其进行修改也会导致原slice的数据变更。 但是,如果在调用函数中,对该slice进行了append操作导致slice扩容进而导致slice的array指向其他位置时,对函数内的slice进行任意修改都不会影响到函数外的slice。

在函数参数有slice类型时,应尽量避免对slice进行append操作。

高效使用slice

  1. 预先分配内存可以提升性能。

  2. 直接使用index赋值而非append操作可以提升性能。

  3. Bounds Checking Elimination

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    func normal(s []int) {
    i := 0
    i += s[0]
    i += s[1]
    i += s[2]
    i += s[3]
    println(i)
    }
    // Better Performance!
    func bce(s []int) {
    _ = s[3]
    
    i := 0
    i += s[0]
    i += s[1]
    i += s[2]
    i += s[3]
    println(i)
    }
    

在normal函数中,每次从slice取数据时,都会进行一次Bounds Check,导致性能下降。在bce函数中,我们一开始取s[3],进行了一次bounds check,此时编译器便可确定下标小于等于3的位置不需要进行bounds check,可以拥有较好的性能。

正确使用map

  1. map并发读写问题。 Go语言中的map是线程不安全的,因而如果有多个协程同时对一个map进行读写的话,会报出fatal级的错误。
  2. map的内存占用。 map在delete操作时不会释放底层的存储。

正确使用string

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func SliceByteToString(b []byte) string {
   return *(*string)(unsafe.Pointer(&b))
}

func StringToSliceByte(s string) []byte {
   l := len(s)
   return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
      Data: (*(*reflect.StringHeader)(unsafe.Pointer(&s))).Data,
      Len: l,
      Cap: l,
   }))
}

在Go语言的定义中,string是不可变的,[]byte是可变的,当把string转换为[]byte后,不能进行变更。

高效使用string

拼接string时,尽量使用strings.Builder,在使用strings.Builder时,我们可以使用(b *Builder) Grow(n int)函数对Builder进行容量的预分配。

正确使用channel

在Go语言中,select语句用于监听和channel有关的IO操作,当IO操作发生时,触发对应case的操作。

1
2
3
4
5
6
7
8
select {
    case <-ch1 :        // 检测有没有数据可读
        // 一旦成功读取到数据,则进行该case处理语句
    case ch2 <- 1 :     // 检测有没有数据可写
        // 一旦成功写入数据,则进行该case处理语句
    default:
        // 如果以上都没有符合条件,则进入default处理流程。
}

在使用select语句时,需要注意:

  1. 如果不设置default条件,当没有IO操作发生则会一直阻塞。
  2. 当有多个IO操作时,会随机选择一个case执行,无法保证执行的顺序。
  3. 对于在for中的select操作,不能添加default,否则会出现CPU占用过高的问题。
  4. 在for语句中的select操作,执行break操作只会跳出select。

高效使用channel

  1. 并发编程中,正确性最重要。
  2. channel仅应该传递通知,而不是传递值。
  3. 注意buffered和unbuffered channel的区别(尽量用buffered)

Happen Before

https://go.dev/ref/mem