关于Go中Slice扩容的总结

1,059 阅读6分钟

slice的数据结构:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

当slice的len==cap后,再向slice中追加元素时,会发生扩容

扩容遵循如下规则:(但结果不完全遵循此规则)

  • 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
  • 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
  • 如果扩容后的大小仍不能满足,那么直接扩容到所需的容量

在编译过程的中间代码生成阶段,会有一个函数来处理你调用的append

// append(slice, 1, 2, 3)
ptr, len, cap := slice //从slice中取出三个属性
newlen := len + 3 //预先计算追加后的长度,以用来和容量比较
if newlen > cap { //追加后的长度将大于容量,需要进行扩容
    ptr, len, cap = growslice(slice, newlen) //扩容的函数,会新开辟一片空间
    newlen = len + 3 //更新len
}
// 对实际的内存赋值
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
// 返回处理后的切片
return makeslice(ptr, newlen, cap)

扩容函数如下,扩容过后,原来的slice将被丢弃

func growslice(et *_type, old slice, cap int) slice {
    // ……
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {  //扩容规则就在这里
        newcap = cap
    } else {
        if old.len < 1024 { 
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }
    // ……
    // 如下两行做了内存对齐的操作,是经常导致结果不符合预期的原因,请记住这里
    capmem = roundupsize(uintptr(newcap) * ptrSize)
    newcap = int(capmem / ptrSize)
}

roundupsize函数如下

在大小为1, 8(64位机器), 和2的倍数时进行不同的内存对齐操作

  var overflow bool
  var lenmem, newlenmem, capmem uintptr
  switch {
  case et.size == 1:
    lenmem = uintptr(old.len)
    newlenmem = uintptr(cap)
    capmem = roundupsize(uintptr(newcap))
    overflow = uintptr(newcap) > maxAlloc
    newcap = int(capmem)
  case et.size == sys.PtrSize:
    lenmem = uintptr(old.len) * sys.PtrSize
    newlenmem = uintptr(cap) * sys.PtrSize
    capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
    overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
    newcap = int(capmem / sys.PtrSize)
  case isPowerOfTwo(et.size):
    ...
  default:
    ...
  }

可以看到有3种分支进行不同的内存对齐,在内存对齐的过程中,向上取整,这里涉及到go的内存分配方式,有一张表如下

// class  bytes/obj  bytes/span  objects  waste bytes
//     1          8        8192     1024            0
//     2         16        8192      512            0
//     3         32        8192      256            0
//     4         48        8192      170           32
//     5         64        8192      128            0
//     6         80        8192      102           32
//     7         96        8192       85           32
//     8        112        8192       73           16
//     9        128        8192       64            0
//    10        144        8192       56          128
//    11        160        8192       51           32
//    12        176        8192       46           96
//    13        192        8192       42          128
//    14        208        8192       39           80
//    15        224        8192       36          128
//    16        240        8192       34           32
//    17        256        8192       32            0
//    18        288        8192       28          128
//    19        320        8192       25          192
//    20        352        8192       23           96
//    21        384        8192       21          128
//    22        416        8192       19          288
//    23        448        8192       18          128
//    24        480        8192       17           32
//    25        512        8192       16            0
//    26        576        8192       14          128
//    27        640        8192       12          512
//    28        704        8192       11          448
//    29        768        8192       10          512
//    30        896        8192        9          128
//    31       1024        8192        8            0
//    32       1152        8192        7          128
//    33       1280        8192        6          512
//    34       1408       16384       11          896
//    35       1536        8192        5          512
//    36       1792       16384        9          256
//    37       2048        8192        4            0
//    38       2304       16384        7          256
//    39       2688        8192        3          128
//    40       3072       24576        8            0
//    41       3200       16384        5          384
//    42       3456       24576        7          384
//    43       4096        8192        2            0
//    44       4864       24576        5          256
//    45       5376       16384        3          256
//    46       6144       24576        4            0
//    47       6528       32768        5          128
//    48       6784       40960        6          256
//    49       6912       49152        7          768
//    50       8192        8192        1            0
//    51       9472       57344        6          512
//    52       9728       49152        5          512
//    53      10240       40960        4            0
//    54      10880       32768        3          128
//    55      12288       24576        2            0
//    56      13568       40960        3          256
//    57      14336       57344        4            0
//    58      16384       16384        1            0
//    59      18432       73728        4            0
//    60      19072       57344        3          128
//    61      20480       40960        2            0
//    62      21760       65536        3          256
//    63      24576       24576        1            0
//    64      27264       81920        3          128
//    65      28672       57344        2            0
//    66      32768       32768        1            0

第二列即是分配的内存大小,举个例子

a := []int{1, 2}
a = append(a, 3, 4, 5)
fmt.Println(cap(a)) // 6

在这次向append操作中会发生扩容,预估出需要的容量是5个int大小,如果是64位机器的话,一个int是64位的大小,5个int就是40字节大小,根据向上取整原则,为这个slice对象申请48字节的空间,所以此时的容量为48/8=6。

这种内存对齐的操作在每次扩容是都会发生,所以常常会得出与预期不符的答案,但好在cap莫名其妙的变大了不会造成什么危险的问题,在实际项目的开发中尽量还是提前预估好slice的容量,这样既能避免莫名其妙的结果发生,又能减少频繁的内存申请,内存复制,影响效率

总结扩容规则:

  • 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
  • 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
  • 如果扩容后的大小仍不能满足,那么新Slice容量等于所需的容量
  • 在以上计算完新Slice容量后,交由管理内存的组件申请内存,按照给出的表向上取整进行内存申请,申请出来的内存长度,作为Slice扩容后的容量