【翻譯】Go 1.1 函數調用

Go 1.0 版本的 runtime 使用動態代碼生成來實現閉包。我認爲這樣一點也不方便:它避免了修改工具鏈寬來表達函數的值與函數調用慣例。然而,自從它限制了 Go 能夠運行的環境,這就很明顯地警示咱們不該該長期依賴動態的代碼生成。它同樣將一部分較小的工具鏈複雜化:棧追蹤代碼使用了很是簡陋的試探法去處理閉包,並且 GDB 也沒法在棧追蹤中支持閉包。權威的解決方案是用一對指針來表示閉包,其中一個指向靜態代碼,而另外一個指向能夠存取捕捉的變量的動態環境。 shell

做爲 Go 1.1 版本動態代碼生成的尋址的一部分,有必要普遍地研究一下 Go 是如何實現函數調用的。這篇文檔描述了實現的通常要求和當前的實現方法,而後主要集中在如何同時實現能夠避免使用動態代碼產生和使用接收者方法的表達式,並從反射包中移除會致使 panic 的代碼。我打算在 Go 1.1 版本中這樣作。
閉包

本篇是我在 2012 年 9 月發佈的一篇題爲 「動態產生 Go 代碼的替代方案」 的後續篇 框架

函數調用的種類

凡是在本篇中說起有關 「函數」 一詞,均表示能夠被執行的 Go 代碼塊,「調用」 則表示一個函數被調用的原理。 函數

在Go中,有 4 個不一樣種類的函數: 工具

  • 頂級函數
  • 帶有值傳遞接收者的方法
  • 帶有指針傳遞接收者的方法
  • 函數字面值
這裏還有 5 個不一樣種類的調用:
  • 直接調用頂級函數
  • 直接調用帶有值傳遞接收者的方法
  • 直接調用帶有指針傳遞接收者的方法
  • 間接調用函數的值
  • 在接口中間接調用方法
下面這個 Go 程序展現了全部可能的函數及調用的配對方案。

package main
func TopLevel(x int) {}
type Pointer struct{}
func (*Pointer) M(int) {}
type Value struct{}
func (Value) M(int) {}
type Interface interface { M(int) }
var literal = func(x int) {}
func main() {
        // direct call of top-level func
        TopLevel(1)
        
        // direct call of method with value receiver (two spellings, but same)
        var v Value
        v.M(1)
        Value.M(v, 1)
        // direct call of method with pointer receiver (two spellings, but same)
        var p Pointer
        (&p).M(1)
        (*Pointer).M(&p, 1)
        
        // indirect call of func value (×4)
        f1 := TopLevel
        f1(1)
        f2 := Value.M
        f2(v, 1)
        f3 := (*Pointer).M
        f3(&p, 1)
        f4 := literal
        f4(1)
        // indirect call of method on interface (×3)
        var i Interface
        i = v
        i.M(1)
        i = &v
        i.M(1)
        i = &p
        i.M(1)
        Interface.M(i, 1)
        Interface.M(v, 1)
        Interface.M(&p, 1)
}
正如你所看到的那樣,這裏有10種可能的函數與調用的組合:
  • 直接調用頂級函數/
  • 直接調用帶有值傳遞接收者的方法/
  • 直接調用帶有指針傳遞接收者的方法/
  • 間接調用函數值 / 經過設置頂級函數
  • 間接調用函數值 / 經過設置帶有值傳遞的方法
  • 間接調用函數值 / 經過帶有指針傳遞的方法
  • 間接調用函數值 / 經過調用函數字面值
  • 在接口中間接調用其它方法 / 包含帶有值傳遞的值方法
  • 在接口中間接調用方法 / 包含帶有指針傳遞的值方法
  • 在接口中間接調用方法 / 包含帶有指針傳遞的指針方法中

在這個列表中,斜槓的左側表示在編譯時即可預知的狀況,右側表示只有在運行時才能知道的狀況。在編譯時對於間接調用的代碼生成不能依賴於運行時的值;相反的,有些間接調用的例子卻可以被代碼生成適配函數所處理,從而使間接調用達到指望值。 佈局

當前的實現方法

這一部分描述了對可能的函數調用的實現方案。

直接調用頂級函數。
除了佔有連續棧位置的結果外,直接調用頂級函數將會經過棧傳遞全部參數。這與關聯的 C 編譯器的調用規範相匹配。 優化

直接調用方法。爲了使函數的間接調用和直接調用可以產生相同的代碼,帶有值傳遞和指針傳遞的接收者的方法都被選擇採用和調用帶有接收者的頂級函數相同的規範來傳遞參數。 ui

函數值的間接調用。除了真正的 CALL 或者 BL 指令,間接調用會被像調用頂級函數同樣處理:在這個例子當中,函數值會採用包含代碼地址的函數去執行。這說明了對頂級函數的間接調用會變成直接調用。正如我前面提到的,對於間接方法函數的調用會變成直接方法的調用。這一樣致使對函數字面值的間接調用會變成直接調用。 google

在通常狀況下,函數字面值的值有 2 個部分:能夠在編譯時被處理的函數和一些只有在函數字面值表達式得到實際值的時候才知道與哪些隱藏參數關聯的函數。除了對函數值的間接調用,爲了可以匹配調用的轉換,如今的實現方法是經過運行時產生代碼來提供對編譯時產生含有隱藏參數的函數體的支持。 spa

在接口中間接調用方法。接口的值是在容許的狀況下,可使字賦予類型的一個結對(類型,字),或者一個指向某個類型的字的指針。接口取回一個能夠被執行的方法的地址並像間接調用含有接收者的函數同樣進行直接調用。若是類型的值就是它自身的指針或者方法是一個指針方法,那麼直接調用和間接調用的調用轉換就和我前面說的相吻合了,所以能夠說是調用相同的函數。若是類型的值是一個擁有具體大小的字的空指針和一個值傳遞的方法,一樣也會應用相同的最優化的方法。不然,對於剩下的例子,換句話來說,在類型的指針或空指針上調用一個值傳遞的方法,代碼在被其它調用環境使用時將不會有相同的調用轉換。一個轉換適配函數必須在編譯時進行轉換,並且轉換適配函數的地址要在接口被調用時保存在方法查詢表中。舉個例子,上面的值類型在像這樣被用在一個方法表的適配函數中:

func Value.M·i(word uintptr, x int) {
        v := *(*Value)(unsafe.Pointer(&word))
        v.M(x)
}
若是值超過字的大小(在本例中正好相反,值小於字的大小),轉換函數將會同等對待並在 &word 中省略 & 符號。

當前實現方法存在的問題

如今的實現方法是高雅的,徹底地完全地基於如下 3 個考慮:1. 間接調用應當與 C 的慣例匹配;2. 間接調用應當分配與直接調用相同的代碼;3. 當擁有指針和指針方法的接口被調用時應當和直接調用分配相同的代碼。

運行時代碼生成。爲了使函數字面值的值與間接調用相匹配,如今的實現方法是經過運用運行時的代碼生成來實現對於局部變量的捕捉。在嵌入式或沙盒系統的環境下,運行時代碼生成會變得很是耗費資源甚至沒法完成。Go 不能依賴於此。比較經常使用的解決方案是製造兩個字的函數值,一個代碼指針指向編譯時產生的代碼,另外一個數據指針則指向被捕捉到的局部變量。

方法值。在通過將近一年的討論,咱們一致研究決定將賦予在 Go 語言中方法值的意義,大體以下所示:

var r io.Reader
f := r.Read  // f has type func([]byte) (int, error)
n, err := f(p)

甚至有一次咱們達成了一致,我都沒必要麻煩地去寫一份改變說明,由於 「f:=r.Read」 的實現將會隱藏在閉包中,若是強制人們像下面這樣寫將會更好:

f := func(p []byte) (int, error) { return r.Read(p) }

明確地指出閉包。一樣我也很是地懶到不想去實現它。但我堅信總有一天我會想要作這個的。事實上,"f:=r.Read" 對於新手來講是很是莫名其妙的。雙字的函數值的表達可能會瑣碎地實現 「f := r.Read」。

反射。在反射中,v.Method(i) 會返回一個帶有預期限制的接收者的第i個方法的相似的玩意。好比說,假設第 i 個方法的名稱是 F,v.Method(i).Call(ValueOf(x), ValueOf(y)) 至關於 v.F(x, y) 的反射。實際上,方法和調用的兩個不一樣的步驟說明了 v.Method(i) 自身一個表明着什麼。今天它表示一個 reflect.Value 而且能夠被用於調用或類型檢查。然而,像 v.Method(i).Interface() 這樣的接口方法會致使 panic,由於這裏沒有 Go 的值能夠在 interface{} 中返回。

雙字函數值的表示使用更瑣碎的方法來建立一個帶有預期限制的真正的方法,就像在例子 「f := r.Read」 中同樣瑣碎。所以接口的調用再也不須要 panic(這對於同時修復他們很重要;若是咱們修復了反射但卻不容許使用 「f := r.Read」 的話,人們一樣會用其它愚蠢的方法來使用反射而致使一個語言變得 2B,咱們必須避免)。

新的實現方法

咱們建議只改變 「間接調用函數值」 的實現機制。其它調用細節都保持不變。

新的實現方法避免了須要爲運行時代碼生成而建立一個指向數據內存的可變大小塊的函數值指針,其中第一個字保存了代碼的指針和調用代碼所須要的其它數據。下圖灰色部分展現了當前實現中函數變量的內存佈局,黑色部分是新實現的改變:


在當前實現中,函數值保存了指向被調用的實際代碼的指針。新的方案介紹了經過數據塊的間接實現。

當前實現的調用順序以下所示:

MOV …, R1
CALL R1
在新的實現中,調用順序增長了一個間接環節,這也使得間接塊(中間沙箱的地址)的地址被放在一個已知的寄存器中(R0):

MOV …, R0
MOV 0(R0), R1
CALL R1  # called code can access 「data」 using R0
根據上面的程序,請思考初始化一個 Go 函數的各類可能的方式:

f1 := TopLevel
f1(1)
f2 := Value.M
f2(v, 1)
f3 := (*Pointer).M
f3(&p, 1)
f4 := literal
f4(1)
除了調用能夠捕捉到外部變量的函數字面值,這個過程不須要相關的數據,所以內存佈局被簡化成:


在這個例子中,中間沙箱就是單純的 C 函數指針,所以 Go 函數值只是一個指向 C 函數指針的指針。額外的 C 函數指針字必須被分配好,可是分配的實際狀況是由編譯器來提早實現,從而使得指針字只能被分配在只讀數據中,並且每次存儲函數值都只會實現一次,不管程序中有多少地方須要這樣作。

就是說 「f := MyFunc」 這個賦值會生成如下代碼:

MOV $MyFunc·f(SB), f1
DATA MyFunc·f(SB)/8, $MyFunc(SB)
GLOBL MyFunc·f(SB), 10, $8

實際的存儲指令記錄了指向只讀數據 MyFunc·f 的指針,指針自己也保存了一個指向 MyFunc 實際代碼的指針。後綴 ·f 爲間接層創建了一個單獨的命名空間。在 GLOBL 這個聲明中,10 位的標識字使用 8 位(只讀內存)和 2 位(可在最終二進制文件時被合併的定義副本)。

這能夠被應用於全部不須要關聯數據的函數。在上面的代碼片斷中,不須要捕捉變量的 f一、f二、f3 和 f4 都使用這種模式。

若是函數字面值(f4)須要捕捉變量,那麼一個更大的間接塊須要在運行時分配。第一個字指向編譯時生成的函數,塊的其他部分保存指向捕捉到的變量的指針。函數 「runtime.closure」 的調用是用於建立閉包,就目前而言,這會爲其分配內存而後將用於生成代碼序列的實際機器指令填滿剩餘部分。新的實現只會分配內存並拷貝必要的代碼和數據指針到其中,只有少許的複合數據。

像 「f := r.Read」 這樣將一個函數表達式賦值給一個函數值會分配一個包含指向適配函數的指針的間接塊,並拷貝 r。語句 「f := r.Read」 也會被進行大體以下的處理:

type funcValue struct {
        f func([]byte) (int, error)
        r io.Reader
}
func readAdapter(b []byte) (int, error) {
        r := (*io.Reader)(R0+8)
        return r.Read(b)
}
f := &funcValue{readAdapter, r}

反射。事實上,一個指向 C 函數的指針是一個合法的 Go 函數值,這意味着反射能夠經過指針自身的函數 table 生成 Go 函數值。舉例來講,像 *bytes.Buffer 這樣的混合類型擁有一個關聯的方法表。假設 table[0] 保存着 (*Buffer).Read 的代碼地址(C 函數指針),table[1] 保存着 (*Buffer).Write 的地址,以此類推,當發生以下反射調用時: 

reflect.TypeOf(new(bytes.Buffer)).Method(0)

必須建立一個與 (*Buffer).Read 相關的 Go 函數值,它能夠返回 &table[0] 而不須要分配一個顯式的間接塊。

容許使用 「f := r.Read" 的其中一個目標是使得

f := reflect.ValueOf(new(bytes.Buffer)).Method(0).Interface()
與前個反射調用具備相同效果;當前的實現不容許調用接口並致使 panic。最明顯的實現是爲每一個方法生成像 readAdapter 這樣的函數,而後在反射表中記錄指向這些函數的指針。這樣作的缺點是會使得反射表和文本塊不斷增加,其中包括了須要在極大多數狀況下都用不到的適配器。然而,咱們能夠包含一個使用反射的單獨適配器來處理全部函數。它須要使用匯編語言編寫,並使得前文中的調用會生成相似下面的東西:

typedef struct funcValue funcValue;
struct funcValue {
        void (*adapter)(void);
        void (*fn)(void);
        uintptr rcvrArgBytes;
        uintptr inArgBytes;
        uintptr outArgBytes;
        byte rcvr[0];
}
f := malloc(sizeof(funcValue) + sizeof(*bytes.Buffer))
f.adapter = adapter
f.fn = (*bytes.Buffer).Read;
f.rcvrArgBytes = sizeof(*bytes.Buffer);
f.inArgBytes = sizeof([]byte)
f.outArgBytes = sizeof(int, error);
memmove(&f.rcvr, r, sizeof(*bytes.Buffer));

適配器須要使用上下文中已記錄的 funcValue 來完成函數的通常調用,相似(但更簡潔)如今使用的 reflect.Call。這會比預生成自定義適配器稍慢些,但卻避免了空間的浪費。

新實現的屬性

相比較當前的實現,新實現犧牲了 Go 與 C 函數之間的直接調用。更確切地說,Go func(int) 和 C void(*) 在運行時具備相同類型。它們之間的區別必須被說明,運行時一樣也要區別從 C char* 轉換而來的 Go string。新的實現帶來了另外 2 個更好更重要的屬性:間接調用會變成對相同代碼的直接調用,以及對接口的調用會被變成相同代碼的直接函數調用,以防帶有指針方法的指針調用。

新的實現要求在函數值的間接調用前的即時內存加載。在某些狀況下間接的 CALL 指令會被延遲,可是隻會影響到使用函數值的調用,對接口的調用沒有影響。

新的實如今內存中保存函數值的大小,所以對函數值的代碼賦值不須要修改任何東西,並且使用 C 或彙編語言編寫的帶有函數值參數的代碼不須要從新計算參數框架或局部變量位移。固然,函數值意味着已經被改變,即便它的大小是同樣的,因此當 C 或彙編語言嘗試調用 Go 函數值時將會須要一些調整。

新的實現不會增加運行時反射的內存表需求。間接函數字只會在使用像語句 「f := MyFunc」 時被包括;反射使用不一樣的策略來避免從新分配,但同時又複用了已經存在的表。

向後不兼容

幾乎沒有現存的代碼須要改變。只有使用 C 或彙編語言調用 Go 函數值的部分存在不兼容。這些代碼須要更新爲使用間接塊。

直接編譯 Go 代碼來建立函數值和經過反射來建立函數值的均指向相同的底層代碼,甚至不存在閉包的使用,而後會使用不一樣的指針來獲取代碼。由於不允函數之間的比較,新實現涉及到的改變只會破壞使用了 unsafe 代碼的程序,以及那些使用 gccgo 編譯的使用共享庫的程序。

如今想要經過反射的 Value.Pointer 來返回惟一的函數標識符是不可能的了。惟有經過返回間接塊的地址來對擁有不一樣 「Pointer()」 的函數進行區別。不幸地是,一些人如今可能依賴的 runtime.FuncForPc 的結果會變得沒什麼做用。相反的,咱們將會使可能會返回相同指針的 Value.Pointer 的不一樣函數的指針包含其關聯的代碼指針和文檔(例如:函數字面值的多個實例,或全部函數使用通用的反射適配器)。惟一能夠保證的是,當且僅當函數爲 nil 時 Value.Pointer 會返回 0。

實現計劃

實現計劃能夠被分爲如下這些步驟。每一步都是一個工做樹。

1. 讓函數值擁有一個間接字,保持原有運行時的代碼生成。全部的函數值將會看齊來像第二影像(沒有關聯數據)。這要求編譯器生成的 MyFunc·f 的字,運行時的任務是區分從 Go 函數值變化而來的 C 函數指針,並使反射可以識別新設計。

2. 改變函數字面值實現保存在間接塊中捕捉到的指針而不是運行時的代碼生成。這主要是編譯器的改變,以及刪除運行時代碼生成的函數 runtime.closure。

3. 改變 reflect.MakeFunc 實現避免運行代碼生成(修復問題 373六、373八、4081)。

4. 從回溯例程以及其它可能的地方刪除閉包。

5. 增長對 「f := r.Read」 的支持(修復問題 2280)。

6. 使得反射的 v.Method(i).Interface() 能夠工做(修復問題 1517)。

原文地址:https://docs.google.com/document/d/1bMwCey-gmqZVTpRax-ESeVuZGmjwbocYs1iHplK-cjo/pub

相關文章
相關標籤/搜索