Go 語言是如何計算 len() 的?
- 原文地址:https://tpaschalis.github.io/golang-len/
- 原文作者:Paschalis Tsilias
- 本文永久連結:https://github.com/gocn/translator/blob/master/2021/w31_How_Does_Go_Calculate_Len.md
- 譯者:twx
- 校對:Cluas , cvley
撰寫此文的動力源於不久前在 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)
在 buildssa 和 genssa 之後,再深入幾層,我們終於可以將 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.OpSliceLen 和 ssa.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)
}
hmap 和 hchan 結構的定義表明,它們的第一個欄位確實包含 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日撰寫
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Go 語言切片是如何擴容的?Go
- 語言是 Go 還是 Golang?Golang
- Go是Google的語言,而不是我們的語言Go
- 什麼是Go語言?Go語言有什麼特點?Go
- GO語言————6.11 計算函式執行時間Go函式
- C語言如何計算陣列的長度C語言陣列
- Go 是物件導向的語言嗎?Go物件
- Go語言GOPATH是什麼Go
- Go是一門什麼樣的語言?Go
- Go語言————1、初識GO語言Go
- 詳解 Go 語言的計時器Go
- 詳解Go語言的計時器Go
- 如何客觀的評價 Go 語言Go
- 【Go語言入門系列】(七)如何使用Go的方法?Go
- 如何學習一門計算機程式語言計算機
- Go語言的”坑“Go
- go語言的介面Go
- Java (計算機程式語言)Java計算機
- golang 快速入門 [5.1]-go 語言是如何執行的-連結器Golang
- go語言與c語言的相互呼叫GoC語言
- Go語言設計模式彙總Go設計模式
- Go 語言程式設計規範Go程式設計
- Go語言併發程式設計Go程式設計
- 如何開始學習Go語言Go
- GO語言————2、GO語言環境安裝Go
- 計算今天是該年的第幾天(c語言實現)C語言
- 計算機程式語言的分類,解釋型語言、編譯型語言、指令碼語言的關係計算機編譯指令碼
- 【Go 語言入門專欄】Go 語言的起源與發展Go
- golang 快速入門 [5.2]-go 語言是如何執行的-記憶體概述Golang記憶體
- 《快學 Go 語言》第 8 課 —— 程式大廈是如何構建起來的Go
- Go語言版本的forgeryGo
- Go語言的前景分析Go
- Go語言的那些坑Go
- 【Go語言入門系列】(八)Go語言是不是面嚮物件語言?Go物件
- Go_go語言初探Go
- C語言:迴文數計算C語言
- 課程-計算機語言學計算機
- Go語言程式設計快速入門Go程式設計