Go 語言是如何計算 len() 的?

tt發表於2021-08-11

撰寫此文的動力源於不久前在 Gophers Slack 上的一個問題 :一位開發人員想知道在哪裡可以找到更多關於 len 函式的資訊。

I want to know how the len func gets called. (我想知道 len 函式是怎麼被呼叫的。)

很快,有人回答了正確答案

It doesn’t. Len is compiler magic, not an actual function call. (Len 是編譯器魔術,而不是一個實際的函式呼叫。)

… all the types len works on have the same header format, the compiler just treats the object like a header and returns the integer representing the length of elements (所有 len 處理的型別都有相同的頭部格式,編譯器只是將物件當作頭部對待,並返回表示元素長度的整數)

雖然這些答案在技術上是正確的,但我認為用簡潔的語言展開構成這個"魔法"的層次結構會很棒!這也是一個很好的小練習,可以讓你更深入地瞭解 Go 編譯器的內部工作原理。

僅供參考,本帖中的所有連結都指向即將釋出的 Go 1.17 分支 (https://github.com/golang/go/tree/release-branch.go1.17).

小插曲

一些背景資訊可能有助於理解這篇文章。

Go 編譯器由四個主要階段組成。你可以從 這裡 (https://golang.org/src/cmd/compile/README) 開始閱讀。前兩個一般稱為編譯器"前端",而後兩個也稱為編譯器"後端"。

  • 解析; 對原始檔進行詞法分析和語法分析,併為每個原始檔構建一個語法樹
  • AST 抽象語法樹轉換和型別檢查; 將語法樹轉換為編譯器的 AST 表示形式,並對 AST 樹進行型別檢查
  • 生成 SSA 靜態單賦值; AST 樹被轉換為 Static Single Assignment (SSA 靜態單賦值) 形式,這是一種可以實現優化的較低階別的中間表示形式
  • 生成機器碼; SSA 經過另一個特定於機器的優化過程,然後傳遞給彙編程式,轉換為機器程式碼並寫入最終的二進位制檔案

讓我們開始深入吧!

入口

Go 編譯器的入口點 (毫不奇怪) 是 compile/internal/gc 包中的 main() 函式 (https://github.com/golang/go/blob/release-branch.go1.17/src/cmd/compile/internal/gc/main.go)

如註釋所示,這個函式負責解析 Go 原始檔,對解析後的 Go 包進行型別檢查,將所有內容編譯為機器程式碼併為編譯過的包編寫定義。

最初發生的事情之一就是型別檢查。typecheck.InitUniverse() ,它定義了基本型別、內建函式和運算元。

在這裡,我們可以看到所有內建函式是如何被匹配到一個 “操作” 的,我們可以使用 ir.OLEN 來跟蹤 len() 呼叫的步驟。

var builtinFuncs = [...]struct {
    name string
    op   ir.Op
}{
    {"append", ir.OAPPEND},
    {"cap", ir.OCAP},
    {"close", ir.OCLOSE},
    {"complex", ir.OCOMPLEX},
    {"copy", ir.OCOPY},
    {"delete", ir.ODELETE},
    {"imag", ir.OIMAG},
    {"len", ir.OLEN},
    {"make", ir.OMAKE},
    {"new", ir.ONEW},
    {"panic", ir.OPANIC},
    {"print", ir.OPRINT},
    {"println", ir.OPRINTN},
    {"real", ir.OREAL},
    {"recover", ir.ORECOVER},
}

稍後在 InitUniverse 中,可以看到 okfor 陣列的初始化,它定義了各種運算元的有效型別; 例如,哪些型別應該允許 + 操作符使用。

if types.IsInt[et] || et == types.TIDEAL {
    ...
    okforadd[et] = true
    ...
}
if types.IsFloat[et] {
    ...
    okforadd[et] = true
    ...
    }
if types.IsComplex[et] {
    ...
    okforadd[et] = true
    ...
}

同樣,我們可以看到所有的型別將成為 len() 的有效輸入

okforlen[types.TARRAY] = true
okforlen[types.TCHAN] = true
okforlen[types.TMAP] = true
okforlen[types.TSLICE] = true
okforlen[types.TSTRING] = true

編譯器前端

接下來是編譯的下一個主要步驟,這時候我們對輸入進行解析並從 noder.LoadPackage(flag.Args()) 開始進行型別檢查。(https://github.com/golang/go/blob/release-branch.go1.17/src/cmd/compile/internal/gc/main.go#L191-L192)

再深入一些,我們可以看到每個檔案被單獨解析,然後在五個不同的階段進行型別檢查。(https://github.com/golang/go/blob/release-branch.go1.17/src/cmd/compile/internal/noder/noder.go#L40-L64)

Phase 1: const, type, and names and types of funcs. (常量,型別,識別符號以及函式的型別)
Phase 2: Variable assignments, interface assignments, alias declarations.(有效的賦值,介面賦值,別名宣告)
Phase 3: Type check function bodies.(函式體型別檢查)
Phase 4: Check external declarations. (檢查外部宣告)
Phase 5: Verify map keys, unused dot imports.(檢驗Map的鍵和未使用的點引入)

一旦在最後的型別檢查階段遇到 len 語句,它就會被轉換為 UnaryExpr,因為它實際上不會最終成為一個函式呼叫。

編譯器隱式獲取引數的地址,並使用 okforlen 陣列來驗證引數的合法性或發出相關的錯誤訊息。

// typecheck1 should ONLY be called from typecheck.
func typecheck1(n ir.Node, top int) ir.Node {
    if n, ok := n.(*ir.Name); ok {
        typecheckdef(n)
    }

    switch n.Op() {
    ...
    case ir.OCAP, ir.OLEN:
        n := n.(*ir.UnaryExpr)
        return tcLenCap(n)
    }
}

// tcLenCap typechecks an OLEN or OCAP node.
func tcLenCap(n *ir.UnaryExpr) ir.Node {
    n.X = Expr(n.X)
    n.X = DefaultLit(n.X, nil)
    n.X = implicitstar(n.X)
    ...
    var ok bool
    if n.Op() == ir.OLEN {
        ok = okforlen[t.Kind()]
    } else {
        ok = okforcap[t.Kind()]
    }
    if !ok {
        base.Errorf("invalid argument %L for %v", l, n.Op())
        n.SetType(nil)
        return n
    }

    n.SetType(types.Types[types.TINT])
    return n
}

返回到主編譯器流程,在所有內容都進行了型別檢查之後,所有的函式都將被排隊。(https://github.com/golang/go/blob/release-branch.go1.17/src/cmd/compile/internal/gc/main.go#L277-L287)

compileFunctions() 中,佇列中的每個元素都通過 ssagen.Compile 傳遞

compile = func(fns []*ir.Func) {
    wg.Add(len(fns))
    for _, fn := range fns {
        fn := fn
        queue(func(worker int) {
            ssagen.Compile(fn, worker)
            compile(fn.Closures)
            wg.Done()
        })
    }
}
...
compile(compilequeue)

buildssagenssa 之後,再深入幾層,我們終於可以將 AST 樹中的 len 表示式轉換為 SSA。

現在很容易看到每個可用型別是如何處理的!

// expr converts the expression n to ssa, adds it to s and returns the ssa result.
func (s *state) expr(n ir.Node) *ssa.Value {
    ...
    switch n.Op() {
    case ir.OLEN, ir.OCAP:
        n := n.(*ir.UnaryExpr)
        switch {
        case n.X.Type().IsSlice():
            op := ssa.OpSliceLen
            if n.Op() == ir.OCAP {
                op = ssa.OpSliceCap
            }
            return s.newValue1(op, types.Types[types.TINT], s.expr(n.X))
        case n.X.Type().IsString(): // string; not reachable for OCAP
            return s.newValue1(ssa.OpStringLen, types.Types[types.TINT], s.expr(n.X))
        case n.X.Type().IsMap(), n.X.Type().IsChan():
            return s.referenceTypeBuiltin(n, s.expr(n.X))
        default: // array
            return s.constInt(types.Types[types.TINT], n.X.Type().NumElem())
        }
        ...
    }
    ...
}

Arrays

對於陣列,我們只需基於輸入陣列的 NumElem () 方法返回一個常量整數,該方法只訪問輸入陣列的 Bound 欄位。

// Array contains Type fields specific to array types.
type Array struct {
    Elem  *Type // element type
    Bound int64 // number of elements; <0 if unknown yet
}

func (t *Type) NumElem() int64 {
    t.wantEtype(TARRAY)
    return t.Extra.(*Array).Bound
}

Slices, Strings

對於切片和字串,我們必須瞭解 ssa.OpSliceLenssa.OpStringLen 是如何處理的。

當這兩個呼叫中的任何一個在展開階段遇到 rewriteSelect 方法時,編譯器使用類似 offset+x.ptrSize 的指標演算法遞迴地遍歷切片和字串以找出它們的大小

func (x *expandState) rewriteSelect(leaf *Value, selector *Value, offset int64, regOffset Abi1RO) []*LocalSlot {
    switch selector.Op {
    ...
    case OpStringLen, OpSliceLen:
        ls := x.rewriteSelect(leaf, selector.Args[0], offset+x.ptrSize, regOffset+RO_slice_len)
        locs = x.splitSlots(ls, ".len", x.ptrSize, leafType)
    ...
    }
    return locs

Maps, Channels

最後,對於 Map 和 Channel,我們使用 referenceTypeBuiltin 輔助函式。它的內部工作方式有點神奇,但是它最終做的是獲取 map/chan 引數的地址並使用零偏移量引用它的結構佈局,很像 unsafe.Pointer(uintptr(unsafe.Pointer(s))) 那樣最終返回第一個結構欄位的值。

// referenceTypeBuiltin generates code for the len/cap builtins for maps and channels.
func (s *state) referenceTypeBuiltin(n *ir.UnaryExpr, x *ssa.Value) *ssa.Value {
    if !n.X.Type().IsMap() && !n.X.Type().IsChan() {
        s.Fatalf("node must be a map or a channel")
    }
    // if n == nil {
    //   return 0
    // } else {
    //   // len
    //   return *((*int)n)
    //   // cap
    //   return *(((*int)n)+1)
    // }
    lenType := n.Type()
    nilValue := s.constNil(types.Types[types.TUINTPTR])
    cmp := s.newValue2(ssa.OpEqPtr, types.Types[types.TBOOL], x, nilValue)
    b := s.endBlock()
    b.Kind = ssa.BlockIf
    b.SetControl(cmp)
    b.Likely = ssa.BranchUnlikely

    bThen := s.f.NewBlock(ssa.BlockPlain)
    bElse := s.f.NewBlock(ssa.BlockPlain)
    bAfter := s.f.NewBlock(ssa.BlockPlain)

    ...
    switch n.Op() {
    case ir.OLEN:
        // length is stored in the first word for map/chan
        s.vars[n] = s.load(lenType, x)
    ...
    return s.variable(n, lenType)
}

hmaphchan 結構的定義表明,它們的第一個欄位確實包含 Len() 所需要的東西,分別是 count int // # live cells == size of map (Map 的大小) 和 qcount uint // total data in the queue (佇列裡所有的資料)。

type hmap struct {
    count     int // # live cells == size of map.  Must be first (used by len() builtin)
    flags     uint8
    B         uint8
    noverflow uint16
    hash0     uint32

    buckets    unsafe.Pointer
    oldbuckets unsafe.Pointer
    nevacuate  uintptr

    extra *mapextra
}

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint
    buf      unsafe.Pointer
    elemsize uint16
    closed   uint32
    elemtype *_type
    sendx    uint
    recvx    uint
    recvq    waitq
    sendq    waitq

    lock mutex
}

臨別贈言

就是這樣!這篇文章並沒有我想象的那麼長,我希望你也能對它感興趣。

我對於 Go 編譯器的內部工作幾乎沒有經驗,所以有些地方可能會有錯。除此之外,隨著泛型和新型別系統在接下來的幾個 Go 版本中的出現,很多事情也都會發生改變。但我希望我至少提供了一種方法,可以讓你接下來自己深入探索。

請不要猶豫,向我提出意見、建議、新文章的想法,或者僅僅是談一談 Go

下次見!

於2021年7月31日撰寫

更多原創文章乾貨分享,請關注公眾號
  • Go 語言是如何計算 len() 的?
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章