美文网首页go学习
Golang slice 的底层实现

Golang slice 的底层实现

作者: Love语鬼 | 来源:发表于2017-12-15 16:15 被阅读0次

首先我们来看段代码的输出

    s := make([][]int, 4)
    for i := 0; i < 4; i++ {
        s[i] = make([]int, 4)
        s[i][0] = 1
    }

    s0 := s[0]
    s0 = append(s0, 5)
    fmt.Println(s)

输出的结果是

[[1 0 0 0] [1 0 0 0] [1 0 0 0] [1 0 0 0]]

append的值5并没有输出,那么究竟是s0并不等价于s[0],还是有其他原因呢?
首先,肯定的是在Go中,所有的拷贝都是值拷贝,不存在引用拷贝;
其次,slice由一个指针,两个整型(分别是size和cap)来实现,既然s0包含的指针和s[0]包含的指针相同,为什么append操作并没有达到预期效果呢?

带着这些疑问,我们来看看slice的底层实现。

slice的结构

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

array指向一个数组元素的地址,这个数组可能在makeslice时创建,也可能之前就存在,而slice被"attach"上去,例如 s := a[0:5];

//代码比较简单
func makeslice(et *_type, len, cap int) slice {
   maxElements := maxSliceCap(et.size)
   
   if len < 0 || uintptr(len) > maxElements {
       panic(errorString("makeslice: len out of range"))
   }

   if cap < len || uintptr(cap) > maxElements {
       panic(errorString("makeslice: cap out of range"))
   }
   //分配数组空间
   p := mallocgc(et.size*uintptr(cap), et, true)
   return slice{p, len, cap}
}

e.g.

s := make([]int, 1, 3)
fmt.Printf("%p, %v, len:%d, cap:%d", unsafe.Pointer(&s[0]), s, len(s), cap(s))

输出:

0xc42007c0a0, [0], len:1, cap:3

对于直接"attach"到数组的情形,类似下面这样

a := [10]int{0,1,2,3,4,5,6,7,8,9}

fmt.Printf("%p, %v \n", &a, a)

s1 := a[1:5:7]

fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s1[0]), s1, len(s1), cap(s1), unsafe.Pointer(&s1) )

输出:

0xc42006e050, [0 1 2 3 4 5 6 7 8 9] 
0xc42006e058, [1 2 3 4], len:4, cap:6, self:0xc42006c140

根据两个指针的关系,可以看出,slice直接指向了数组中的元素


slice attach1

同理,slice2还可以通过另一个slice1构造,但其属性依赖slice1,并不是slice1底层的数组

s2 := s1[2:]
fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s2[0]), s2, len(s2), cap(s2), unsafe.Pointer(&s2) )

输出

0xc42006e068, [3 4], len:2, cap:4, self:0xc42006a140 
slice attach2

修改slice中元素的值,实际上修改的是底层数组元素的值

s2[0] = 100
fmt.Println(a, s1, s2)

输出

[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4]

扩张

当我们对slice进行append操作时,若len + 追加元素个数 <= cap时,不会发生内存扩张;否则,新的内存被申请,同时旧的数据被拷贝至新内存的前部;

先上代码

func growslice(et *_type, old slice, cap int) slice {
    ......

    if et.size == 0 {
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }
        
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }
    // 计算新的cap,针对不同情况分别处理
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            //2倍
            newcap = doublecap
        } else {
            //每次尝试扩1/4
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }
    ......
    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        //这里有个优化细节,先不zero,因为前部会发生覆盖
        p = mallocgc(capmem, nil, false)
        memmove(p, old.array, lenmem)
        //只对后部zero
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            memmove(p, old.array, lenmem)
        } else {
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    return slice{p, old.len, newcap}
}

当不需要扩容时,append会修改底层数组的值

s2 = append(s2, 1000)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

输出

[0 1 2 100 4 1000 6 7 8 9] [1 2 100 4] [100 4 1000]
len(s1)=4, cap(s1)=6, len(s2)=3, cap(s2)=4

注意,对s2的append,仅改变了s2的len,并没有改变s1的len
当append过多时,slice底层数组会发生变化

s2 = append(s2, 1000, 1001, 1002)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

输出

0xc420014280, [100 4 1000 1001 1002] 
[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4 1000 1001 1002]
len(s1)=4, cap(s1)=6, len(s2)=5, cap(s2)=8

原数组a,切片s1的属性未受影响;但s2底层的数组已发生变化,cap也是之前的2倍。

总结

1、多个slice指向相同的底层数组时,修改其中一个slice,可能会影响其他slice的值;
2、slice作为参数传递时,比数组更为高效,因为slice的结构比较小;
3、slice在扩张时,可能会发生底层数组的变更及内存拷贝;
4、因为slice引用了数组,这可能导致数组空间不会被gc,当数组空间很大,而slice引用内容很少时尤为严重;

相关文章

  • Golang slice 的底层实现

    首先我们来看段代码的输出 输出的结果是 append的值5并没有输出,那么究竟是s0并不等价于s[0],还是有其他...

  • slice切片做函数参数

    首先要明确一点:slice作为函数参数是值传递. 再来分析一下golang中的切片slice底层的实现细节...

  • golang slice

    关于golang slice有很多大神写了很多文章,阐述了slice的底层实现和使用中注意点.这篇文章是我参考ht...

  • golang中slice底层实现

    什么是切片 切片就是我们经常所说的动态数组,可以灵活的增加和减少切片内的元素。常见的切片操作有:reslice、a...

  • Golang之数组和切片

    引用 数组、字符串和切片 Go数组中的索引问题 深入解析 Go 中 Slice 底层实现 Golang 入门 : ...

  • golang

    golang携程调度,runtime包 golang内存模型 csp原理 context的原理 slice底层结构...

  • 正确理解golang slice的复制

    slice 三个属性 golang 的slice是一个指向底层的数组的指针结构体。 这个结构体有三个属性,1.指...

  • 深入理解Golang Slice

    深入理解Golang Slice 数据结构 slice的底层数据结构中的array是一个指针,指向的是一个Arra...

  • golang 切片小结

    golang slice

  • 剖析golang slice的实现

    本文基于golang 1.10版本分析。 slice 结构 slice实际就是一个struct,在runtime/...

网友评论

    本文标题:Golang slice 的底层实现

    本文链接:https://www.haomeiwen.com/subject/dqorwxtx.html