Golang slice 從原始碼來理解

8090Lambert發表於2019-09-12

Slice 結構體

slice 是 golang 中利用指標指向某個連續片段的陣列,所以本質上它算是引用型別。
一個 slice 在 golang 中佔用24個 bytes

a = make([]int, 0)
unsafe.Sizeof(a)    // 24

var c int
unsafe.Sizeof(c)    // 8, 一個 int 在 golang 中佔用 8 個bytes(本機是64位作業系統)

在 runtime 的 slice.go 中,定義了 slice 的 struct

type slice struct {
    array unsafe.Pointer    // 8 bytes
    len   int               // 8 bytes
    cap   int               // 8 bytes
    // 確認了,slice 的大小 24
}
  • array 是指向真實的陣列的 ptr
  • len 是指切片已有元素個數
  • cap 是指當前分配的空間

準備除錯

簡單準備一段程式,看看 golang 是如何初始化一個切片的

package main

import "fmt"

func main() {
    a := make([]int, 0)
    a = append(a, 2, 3, 4)
    fmt.Println(a)
}

Slice 初始化

使用 dlv 除錯,反彙編後:

(dlv) disassemble
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
main.go:5       0x10b70f0       65488b0c2530000000              mov rcx, qword ptr gs:[0x30]
main.go:5       0x10b70f9       488d4424e8                      lea rax, ptr [rsp-0x18]
main.go:5       0x10b70fe       483b4110                        cmp rax, qword ptr [rcx+0x10]
main.go:5       0x10b7102       0f8637010000                    jbe 0x10b723f      main.go:5       0x10b7108*      4881ec98000000                  sub rsp, 0x98
main.go:5       0x10b710f       4889ac2490000000                mov qword ptr [rsp+0x90], rbp
main.go:5       0x10b7117       488dac2490000000                lea rbp, ptr [rsp+0x90]
main.go:6       0x10b711f       488d051a0e0100                  lea rax, ptr [rip+0x10e1a]
main.go:6       0x10b7126       48890424                        mov qword ptr [rsp], rax
main.go:6       0x10b712a       0f57c0                          xorps xmm0, xmm0
main.go:6       0x10b712d       0f11442408                      movups xmmword ptr [rsp+0x8], xmm0
main.go:6       0x10b7132       e8b99af8ff                      ** call $runtime.makeslice **
main.go:6       0x10b7137       488b442418                      mov rax, qword ptr [rsp+0x18]
main.go:6       0x10b713c       4889442460                      mov qword ptr [rsp+0x60], rax
main.go:6       0x10b7141       0f57c0                          xorps xmm0, xmm0
main.go:6       0x10b7144       0f11442468                      movups xmmword ptr [rsp+0x68], xmm0
...

在一堆指令中,看到 call $runtime.makeslice 的呼叫應該是初始化 slice

func makeslice(et *_type, len, cap int) unsafe.Pointer {
    mem, overflow := math.MulUintptr(et.size, uintptr(cap))
    if overflow || mem > maxAlloc || len < 0 || len > cap {
        // NOTE: Produce a 'len out of range' error instead of a
        // 'cap out of range' error when someone does make([]T, bignumber).
        // 'cap out of range' is true too, but since the cap is only being
        // supplied implicitly, saying len is clearer.
        // See golang.org/issue/4085.
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }

    return mallocgc(mem, et, true)
}

makeslice 最後返回真正值儲存的陣列域的記憶體地址,函式中 uintptr() 是什麼呢?

println(uintptr(0), ^uintptr(0))
// 0    18446744073709551615    為什麼按位異或後是這個數?

var c int = 1
println(^c, ^uint64(0))
// -2   18446744073709551615

從這幾行程式碼驗證,有符號的1,二進位制為:0001,異或後:1110,最高位1是負數,表示-2;
uint64二進位制:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
異或後:1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
因為無符號的,轉換成10進位制,就是 2 ^ 64 - 1 = 18446744073709551615
。所以,其實^uintptr(0) 就是指當前機器(32位,uint32;64位,uint64)的最大值。
我們可以列印下現在的 a

(dlv) p a
[]int len: 1, cap: 0, [0]

Slice 擴容

=>      main.go:7       0x10b7149       eb00                            jmp 0x10b714b
        main.go:7       0x10b714b       488d0dee0d0100                  lea rcx, ptr [rip+0x10dee]
        main.go:7       0x10b7152       48890c24                        mov qword ptr [rsp], rcx
        main.go:7       0x10b7156       4889442408                      mov qword ptr [rsp+0x8], rax
        main.go:7       0x10b715b       0f57c0                          xorps xmm0, xmm0
        main.go:7       0x10b715e       0f11442410                      movups xmmword ptr [rsp+0x10], xmm0
        main.go:7       0x10b7163       48c744242003000000              mov qword ptr [rsp+0x20], 0x3
        main.go:7       0x10b716c       e84f9bf8ff                      call $runtime.growslice
        main.go:7       0x10b7171       488b442428                      mov rax, qword ptr [rsp+0x28]
        main.go:7       0x10b7176       488b4c2430                      mov rcx, qword ptr [rsp+0x30]
        main.go:7       0x10b717b       488b542438                      mov rdx, qword ptr [rsp+0x38]
        main.go:7       0x10b7180       4883c103                        add rcx, 0x3
        main.go:7       0x10b7184       eb00                            jmp 0x10b7186
        main.go:7       0x10b7186       48c70002000000                  mov qword ptr [rax], 0x2
        main.go:7       0x10b718d       48c7400803000000                mov qword ptr [rax+0x8], 0x3
        main.go:7       0x10b7195       48c7401004000000                mov qword ptr [rax+0x10], 0x4
        main.go:7       0x10b719d       4889442460                      mov qword ptr [rsp+0x60], rax
        main.go:7       0x10b71a2       48894c2468                      mov qword ptr [rsp+0x68], rcx
        main.go:7       0x10b71a7       4889542470                      mov qword 
        ...

在對 slice 做 append 的時候,其實是呼叫了 call runtime.growslice,看看做了什麼:

func growslice(et *_type, old slice, cap int) slice {
    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }

    if et.size == 0 {
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve old.array in this case.
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    var overflow bool
    var lenmem, newlenmem, capmem uintptr
    // Specialize for common values of et.size.
    // For 1 we don't need any division/multiplication.
    // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
    // For powers of 2, use a variable shift.
    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):
        var shift uintptr
        if sys.PtrSize == 8 {
            // Mask shift for better code generation.
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }

    if overflow || capmem > maxAlloc {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.ptrdata == 0 {
        // 申請記憶體
        p = mallocgc(capmem, nil, false)

        // 清除未使用的地址
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if lenmem > 0 && writeBarrier.enabled {
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
        }
    }
    // 複製大小為 lenmem 個btyes,從old.array到p
    memmove(p, old.array, lenmem)

    return slice{p, old.len, newcap}

具體擴容的策略:

  • 如果要申請的容量(cap)大於 2 倍的原容量(old.cap)或者 原容量 < 1024 ,那麼newcap = old.cap + old.cap
  • 否則,計算 newcap += newcap / 4,知道 newcap 不小於要申請的容量,如果溢位,newcap = cap(要申請的容量)

擴容完成後就開始根據 t.size 的大小,重新計算地址,其中新 slice 的 len 為原 slice 的 cap (只有 slice 的 len 超過 cap,才需要擴容)。
接著申請 capmem 大小的記憶體,從 old.array 複製 lenmem 個 bytes (就是原 slice 整個複製,lenmem 就是計算的原切片的大小)到 p

a := make([]int, 0)
a = append(a, 1)
println("1 times:", len(a), cap(a)) // 1 times: 1 1

a = append(a, 2, 3)
println("2 times:", len(a), cap(a)) // 2 times: 3 4

a = append(a, 4)
println("3 times:", len(a), cap(a)) // 3 times: 4 4

可以看出:

  1. 如果 append 後的 len 大於 cap 的2倍,即擴大至大於 len 的第一個2的倍數
  2. 如果 append 後的 len 大於 cap 且小於 cap 的兩倍,cap擴大至2倍
  3. 如果 append 後的 len 小於 cap,直接追加

Slice汙染

使用 slice,也許不知不覺中就會造成一些問題。

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

結果很意外,但也是符合邏輯。a 的結構體中 array 是指向陣列 [1,2,3,4,5]的記憶體地址,shadow 是指向其中 [2,3] 的記憶體地址。在向 shadow 增加後,會直接修改真實的陣列,間接影響到指向陣列的所有切片。所以可以修改上述程式碼為:

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

如果某個函式的返回值,是上述的這種情況 return a[1:3],還會造成 [1,2,3,4,5] 鎖佔用的記憶體無法釋放。

黑魔法

知道了 slice 本身是指向真實的陣列的指標,在 Golang 中提供了 unsafe 來做指標操作。

a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
shadowPtr := uintptr(unsafe.Pointer(&shadow[0]))
offset := unsafe.Sizeof(int(0))
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr - offset)))    // 1
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr + 2*offset)))  // 4

shadowPtr 是 a 的第1個下標的位置,一個 int 在64位機器上是8 bytes,向前偏移1個 offset,是 a 的第0個下標 1;向後偏移2個 offset,是 a 的第3個下標 4。

併發安全

slice 是非協程安全的資料型別,如果建立多個 goroutineslice 進行併發讀寫,會造成丟失。看一段程式碼

package main

import (
    "fmt"
    "sync"
)

func main () {
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            a = append(a, i)
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println(len(a))
}
// 9403 9876 9985 9491 ...

多次執行,每次得到的結果都不一樣,總之一定不會是想要的 10000 個。想要解決這個問題,按照協程安全的程式設計思想來考慮問題,
可以考慮使用 channel 本身的特性(阻塞)來實現安全的併發讀寫。

func main() {
    a := make([]int, 0)
    buffer := make(chan int)
    go func() {
        for v := range buffer {
            a = append(a, v)
        }
    }()

    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            buffer <- i
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println(len(a))
}
// 10000
本作品採用《CC 協議》,轉載必須註明作者和本文連結
8090lambert

相關文章