深度探索 Go 對象模型

來源:cyningsun.github.io/01-12-2020/…
源代碼:github.com/cyningsun/g…html

目錄

瞭解一門語言的高級特性,僅僅從浮於表面,是沒法把握住語言的精髓的。學習過 C++ 的高階開發者,必定讀過神書《Inside The C++ Object Model》,本文的目標是同樣的:經過對象模型,掌握 Go 語言的底層機制,從更深層次解釋語言特性。git

編譯與執行

衆所周知,Go 源碼並不能直接運行,全部代碼必須一行行,經過「編譯」——「彙編」——「連接」 階段 轉化爲低級的機器語言指令,便可執行程序。程序員

compile.png

「彙編」和「連接」階段各類語言並沒有區別,因此通常經過「編譯」和「執行」階段來支持各類語言特性。對於 Go 語言,執行過程並沒有法直接修改執行指令,所以全部語言特性都是「編譯」相關的。理解這一點很重要,由於下面依賴「編譯」的產物 彙編代碼 來解讀對象模型。github

什麼是對象模型?

何爲 Go 對象模型? Go 對象模型能夠歸納爲如下兩部分:golang

  1. 支持面向對象程序設計的部分編程

    • 封裝
    • 繼承
    • 多態
  2. 各類特性的底層實現機制bash

    • 反射

下面分別從 struct 和 interface 來解釋模型如何支持以上兩部分。ide

Struct 語意學

struct.png

面向對象編程,把對象做爲程序的基本單元,一個對象包含了數據和操做數據的函數,前者爲成員變量,後者爲成員函數。因此研究對象須要分別從成員變量和成員函數入手。函數

成員變量

如下有三段程序:佈局

// First: global varible
var (
   X,Y,Z float32
)

// Second: simple type
type point3d struct {
	X, Y, Z float32
}

// Third: inherit type
type point struct {
	X float32
}

type point2d struct {
	point
	Y float32
}

type point3d struct {
	point2d
	Z float32
}
複製代碼

從風格來看,三段程序大相徑庭。有許多使人信服的討論告訴咱們,爲何「數據封裝」(Second & Third)要比使用「全局變量」好。但,從程序員的角度看,會有幾個疑問:

  1. 「數據封裝」 以後,內存成本增長了多少?
  2. 「數據封裝」 以後,在執行過程當中,變量的存儲效率是否下降了?
內存佈局

先看內存變化。瞭解內存變化最好的辦法就是經過代碼打印對象的內存大小,先看全局變量大小

var (
	X, Y, Z float32
)

func main() {
	fmt.Printf("X size:%v, Y size:%v, Z size:%v\n", unsafe.Sizeof(X), unsafe.Sizeof(Y), unsafe.Sizeof(Z))
	fmt.Printf("X addr:%v, Y addr:%v, Z addr:%v\n", &X, &Y, &Z)
}
複製代碼

執行程序輸出爲:

$ go run variable.go
X size:4, Y size:4, Z size:4
X addr:0x118ee88, Y addr:0x118ee8c, Z addr:0x118ee90
複製代碼

能夠看到,X、Y、Z三個字段大小均爲4字節,且三個字段內存地址順序排列。

再看第二段代碼的輸出

func TestLayout(t *testing.T) {
	p := point3d{X: 1, Y: 2, Z: 3}
	fmt.Printf("point3d size:%v, align:%v\n", unsafe.Sizeof(p), unsafe.Alignof(p))
	typ := reflect.TypeOf(p)
	fmt.Printf("Struct:%v is %d bytes long\n", typ.Name(), typ.Size())
	fmt.Printf("X at offset %v, size=%d\n", unsafe.Offsetof(p.X), unsafe.Sizeof(p.X))
	fmt.Printf("Y at offset %v, size=%d\n", unsafe.Offsetof(p.Y), unsafe.Sizeof(p.Y))
	fmt.Printf("Z at offset %v, size=%d\n", unsafe.Offsetof(p.Z), unsafe.Sizeof(p.Z))
}
複製代碼

執行程序輸出爲:

$ go test -v -run TestLayout
=== RUN   TestLayout
point3d size:12, align:4
Struct:point3d is 12 bytes long
X at offset 0, size=4
Y at offset 4, size=4
Z at offset 8, size=4
複製代碼

能夠看到,X、Y、Z三個字段大小同樣爲4字節,內存排列也與上一個版本同樣。

繼續,第三段代碼

func TestLayout(t *testing.T) {
	p := point3d{point2d: point2d{point: point{X: 1}, Y: 2}, Z: 3}
	fmt.Printf("point3d size:%v, align:%v\n", unsafe.Sizeof(p), unsafe.Alignof(p))
	typ := reflect.TypeOf(p)
	fmt.Printf("Struct:%v is %d bytes long\n", typ.Name(), typ.Size())
	fmt.Printf("X at offset %v, size=%d\n", unsafe.Offsetof(p.X), unsafe.Sizeof(p.X))
	fmt.Printf("Y at offset %v, size=%d\n", unsafe.Offsetof(p.Y), unsafe.Sizeof(p.Y))
	fmt.Printf("Z at offset %v, size=%d\n", unsafe.Offsetof(p.Z), unsafe.Sizeof(p.Z))
}
複製代碼

執行程序輸出爲:

$ go test -v -run TestLayout
=== RUN   TestLayout
point3d size:12, align:4
Struct:point3d is 12 bytes long
X at offset 0, size=4
Y at offset 4, size=4
Z at offset 8, size=4
複製代碼

能夠看到,X、Y、Z三個字段大小同樣爲4字節,內存排列也與以前兩個版本同樣。

綜上所述,咱們能夠看到,不管是否封裝,仍是多深的繼承層次,對成員變量的內存佈局都並沒有影響,均按照字段定義的順序排列(不考慮內存對齊的狀況)。即內存佈局相似以下:

memory-offset.png

變量存取

成員變量有兩種讀取方式,既能夠經過對象讀取,也能夠經過對象的指針讀取。兩種讀取方式與直接變量讀取會有什麼不一樣麼?使用一段代碼再看下:

type point struct {
	X float32
}

type point2d struct {
	point
	Y float32
}

type point3d struct {
	point2d
	Z float32
}

func main() {
	var (
		w float32
	)
	point := point3d{point2d: point2d{point: point{X: 1}, Y: 2}, Z: 3} // L25
	p := &point  // L26
	w = point.Y  // L27
	fmt.Printf("w:%f\n", w)
	w = p.Y     // L29
	fmt.Printf("w:%f\n", w)
}
複製代碼

還記得以前提過的「編譯」階段麼?咱們使用 go tool 能夠查看源代碼彙編以後的代碼

data_access.go:25	0x10948d8	f30f11442444		MOVSS X0, 0x44(SP)
data_access.go:25	0x10948de	f30f11442448		MOVSS X0, 0x48(SP)
data_access.go:25	0x10948e4	f30f1144244c		MOVSS X0, 0x4c(SP)
data_access.go:25	0x10948ea	f30f10055ab50400	MOVSS $f32.3f800000(SB), X0	
data_access.go:25	0x10948f2	f30f11442444		MOVSS X0, 0x44(SP)
data_access.go:25	0x10948f8	f30f100550b50400	MOVSS $f32.40000000(SB), X0	
data_access.go:25	0x1094900	f30f11442448		MOVSS X0, 0x48(SP)
data_access.go:25	0x1094906	f30f100546b50400	MOVSS $f32.40400000(SB), X0	
data_access.go:25	0x109490e	f30f1144244c		MOVSS X0, 0x4c(SP)
data_access.go:26	0x1094914	488d442444		LEAQ 0x44(SP), AX
data_access.go:26	0x1094919	4889442450		MOVQ AX, 0x50(SP)
data_access.go:27	0x109491e	f30f10442448		MOVSS 0x48(SP), X0	// 讀取 Y 到寄存器 X0
data_access.go:27	0x1094924	f30f11442440		MOVSS X0, 0x40(SP)	// 賦值 寄存器 X0 給 w
...
data_access.go:29	0x10949c7	488b442450		MOVQ 0x50(SP), AX	// 讀取 對象地址 到寄存器 AX 		
data_access.go:29	0x10949cc	8400			TESTB AL, 0(AX)	
data_access.go:29	0x10949ce	f30f104004		MOVSS 0x4(AX), X0	// 從對象起始地址偏移4字節讀取數據到寄存器 X0 	
data_access.go:29	0x10949d3	f30f11442440		MOVSS X0, 0x40(SP)	// 賦值 寄存器 X0 給 w
複製代碼

能夠看到,每一個成員變量的偏移量在編譯時便可獲知,無論其有多麼複雜的繼承,都是同樣的。經過對象存取一個data member,其效率和存取一個非成員變量是同樣的。

函數調用

前面的例子提過,對象的總大小恰好等於全部的成員變量之和,也就意味着成員函數並不佔用對象的內存大小。那成員函數的調用是怎麼實現的呢?咱們經過一段代碼看下

type point3d struct {
	X, Y, Z float32
}

func (p *point3d) Println() {
	fmt.Printf("%v,%v,%v\n", p.X, p.Y, p.Z)
}

func main() {
	p := point3d{X: 1, Y: 2, Z: 3} // L14
	p.Println()                   // L15
}
複製代碼

一樣使用 go tool獲取對應的彙編代碼

call.go:14	0x1094a7d	0f57c0			XORPS X0, X0	
  call.go:14	0x1094a80	f30f1144240c		MOVSS X0, 0xc(SP)
  call.go:14	0x1094a86	f30f11442410		MOVSS X0, 0x10(SP)
  call.go:14	0x1094a8c	f30f11442414		MOVSS X0, 0x14(SP)
  call.go:14	0x1094a92	f30f100592b30400	MOVSS $f32.3f800000(SB), X0	
  call.go:14	0x1094a9a	f30f1144240c		MOVSS X0, 0xc(SP)
  call.go:14	0x1094aa0	f30f100588b30400	MOVSS $f32.40000000(SB), X0	
  call.go:14	0x1094aa8	f30f11442410		MOVSS X0, 0x10(SP)
  call.go:14	0x1094aae	f30f10057eb30400	MOVSS $f32.40400000(SB), X0	
  call.go:14	0x1094ab6	f30f11442414		MOVSS X0, 0x14(SP)
  call.go:15	0x1094abc	488d44240c		LEAQ 0xc(SP), AX //將對象 q 的起始地址保存到寄存器AX
  call.go:15	0x1094ac1	48890424		MOVQ AX, 0(SP)	  //將對象 q 的起始地址 壓棧
  call.go:15	0x1094ac5	e8d6fdffff		CALL main.(*point3d).Println(SB)	  // 調用 struct point 的 Println() 函數
複製代碼

能夠看到成員函數的調用都是先把參數壓棧,而後調用對應的的函數。可見,成員函數與普通的函數調用並沒有不一樣。那麼函數的內存在哪裏呢?

還記得進程的內存分佈麼?

process-memory.png

沒錯,全部的函數都在進程的代碼段(Text Segment)

Interface 語意學

第一部分講了,封裝和繼承的影響,剩下這部分會講清楚 Go 如何使用 interface 實現多態反射。其中interface又有兩種形式,一種是有函數的非空interface,一種是空的interface(interface{})。話很少說,直接上代碼,看下這兩種類型的interface的變量在內存大小上有何區別:

type Point interface {
	Println()
}

type point struct {
	X float32
}

type point2d struct {
	point
	Y float32
}

type point3d struct {
	point2d
	Z float32
}

func TestPolymorphism(t *testing.T) {
	var (
		p Point
	)
	p = &point{X: 1}
	fmt.Printf("point size:%v\n\n", unsafe.Sizeof(p))

	p = &point2d{point: point{X: 1}, Y: 2}
	fmt.Printf("point2d size:%v\n\n", unsafe.Sizeof(p))

	p = &point3d{point2d: point2d{point: point{X: 1}, Y: 2}, Z: 3}
	fmt.Printf("point3d size:%v\n\n", unsafe.Sizeof(p))
}
複製代碼

執行程序輸出爲:

$ go test -v -run TestPolymorphism
=== RUN   TestPolymorphism
p size:16, nilP size:16
p size:16, nilP size:16
p size:16, nilP size:16
複製代碼

能夠看到兩種類型的interface 變量大小並沒有不一樣,均爲16字節。能夠明確一點:interface 變量中存儲的並不是對象的指針,而是特殊的定義類型的變量。那麼 interface 是怎麼支持多態反射的呢?

經過 reflect 包,咱們找到了答案。原來,針對以上兩種類型的interface, Go 語言底層定義了兩個結構分別爲 iface 和 eface。二者實現是相似的,如下咱們僅針對非空interface進行分析

interface 底層

type iface struct {
    tab  *itab          // 類型信息
    data unsafe.Pointer  // 接口指向對象的指針
}

// 類型信息
type itab struct {
    inter  *interfacetype    // 接口的類型信息
    _type  *_type           // 接口指向對象的類型信息
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
    fun    [1]uintptr       // 接口方法實現列表,即函數地址列表,按字典序排序
}

// 接口類型信息
type interfacetype struct {
   typ     _type
   pkgpath name
   mhdr    []imethod      // 接口方法聲明列表,按字典序排序
}
複製代碼

經過代碼,能夠看到,iface 類型包含兩個指針,恰好爲16字節(64位機器)。iface 不但包含了指向對象指向對象的類型,還包含了接口類型。如此

  1. iface 就能夠在其中扮演粘結劑的角色,經過 reflect 包在對象、接口、類型之間進行轉換了。
  2. iface 的變量能夠在編譯階段,在變量賦值處,增長拷貝指向對象(父類或者子類)的類型信息的指令,就能夠在運行期完成多態的支持了

interface.png

理論驗證

下面咱們仍是經過測試代碼來驗證咱們的理論,咱們本身定義底層的相關類型,而後經過強制類型轉換,來嘗試解析interface變量中的數據:

type Iface struct {
	Tab *Itab
	Data unsafe.Pointer
}

type Itab struct {
	Inter uintptr
	Type uintptr
	Hash uint32
	_ [4]byte
	Fun [1]uintptr
}

type Eface struct {
	Type uintptr
	Data unsafe.Pointer
}

func TestInterface(t *testing.T) {
	var (
		p    Point
		nilP interface{}
	)
	point := &point3d{X: 1, Y: 2, Z: 3}
	nilP = point
	fmt.Printf("eface size:%v\n", unsafe.Sizeof(nilP))
	eface := (*face.Eface)(unsafe.Pointer(&nilP))
	spew.Dump(eface.Type)
	spew.Dump(eface.Data)
	fmt.Printf("eface offset: eface._type = %v, eface.data = %v\n\n",
		unsafe.Offsetof(eface.Type), unsafe.Offsetof(eface.Data))

	p = point
	fmt.Printf("point size:%v\n", unsafe.Sizeof(p))
	iface := (*face.Iface)(unsafe.Pointer(&p))
	spew.Dump(iface.Tab)
	spew.Dump(iface.Data)
	fmt.Printf("Iface offset: iface.tab = %v, iface.data = %v\n\n",
		unsafe.Offsetof(iface.Tab), unsafe.Offsetof(iface.Data))
}
複製代碼

執行程序輸出爲:

$ go test -v -run TestInterface
=== RUN   TestInterface
eface size:16
(uintptr) 0x111f2c0
(unsafe.Pointer) 0xc00008e250
eface offset: eface._type = 0, eface.data = 8

point size:16
(*face.Itab)(0x116ec40)({
 Inter: (uintptr) 0x1122680,
 Type: (uintptr) 0x111f2c0,
 Hash: (uint32) 960374823,
 _: ([4]uint8) (len=4 cap=4) {
  00000000  00 00 00 00                                       |....|
 },
 Fun: ([1]uintptr) (len=1 cap=1) {
  (uintptr) 0x10fce20
 }
})
(unsafe.Pointer) 0xc00008e250
Iface offset: iface.tab = 0, iface.data = 8
複製代碼

下面咱們再經過彙編代碼看下,賦值操做作了什麼?

type Point interface {
	Println()
}

type point3d struct {
	X, Y, Z float32
}

func (p *point3d) Println() {
	fmt.Printf("%v,%v,%v\n", p.X, p.Y, p.Z)
}

func main() {
	point := point3d{X: 1, Y: 2, Z: 3} // L18
	var (
		nilP interface{}   // L20
		p    Point        // L21
	)
	nilP = &point         // L23
	p = &point            // L24
	fmt.Println(nilP, p) 
}
複製代碼

經過 go tool 查看彙編代碼以下:

TEXT main.main(SB) /Users/cyningsun/Documents/go/src/github.com/cyningsun/go-test/20200102-inside-golang-object-model/main/build.go
  build.go:17	0x1094de0	65488b0c2530000000      MOVQ GS:0x30, CX
  build.go:17	0x1094de9	488d4424b0              LEAQ -0x50(SP), AX
  build.go:17	0x1094dee	483b4110                CMPQ 0x10(CX), AX
  build.go:17	0x1094df2	0f86b9010000            JBE 0x1094fb1
  build.go:17	0x1094df8	4881ecd0000000          SUBQ $0xd0, SP
  build.go:17	0x1094dff	4889ac24c8000000        MOVQ BP, 0xc8(SP)
  build.go:17	0x1094e07	488dac24c8000000        LEAQ 0xc8(SP), BP
  build.go:18	0x1094e0f	488d05ea1e0200          LEAQ type.*+137216(SB), AX   // point := point3d{X: 1, Y: 2, Z: 3}
  build.go:18	0x1094e16	48890424                MOVQ AX, 0(SP)
  build.go:18	0x1094e1a	e81160f7ff              CALL runtime.newobject(SB)
  build.go:18	0x1094e1f	488b442408              MOVQ 0x8(SP), AX
  build.go:18	0x1094e24	4889442458              MOVQ AX, 0x58(SP)
  build.go:18	0x1094e29	0f57c0                  XORPS X0, X0
  build.go:18	0x1094e2c	f30f11442434            MOVSS X0, 0x34(SP)
  build.go:18	0x1094e32	f30f11442438            MOVSS X0, 0x38(SP)
  build.go:18	0x1094e38	f30f1144243c            MOVSS X0, 0x3c(SP)
  build.go:18	0x1094e3e	f30f1005a6b80400        MOVSS $f32.3f800000(SB), X0
  build.go:18	0x1094e46	f30f11442434            MOVSS X0, 0x34(SP)
  build.go:18	0x1094e4c	f30f100d9cb80400        MOVSS $f32.40000000(SB), X1
  build.go:18	0x1094e54	f30f114c2438            MOVSS X1, 0x38(SP)	
  build.go:18	0x1094e5a	f30f101592b80400        MOVSS $f32.40400000(SB), X2
  build.go:18	0x1094e62	f30f1154243c            MOVSS X2, 0x3c(SP)	
  build.go:18	0x1094e68	488b442458              MOVQ 0x58(SP), AX	
  build.go:18	0x1094e6d	f30f1100                MOVSS X0, 0(AX)	
  build.go:18	0x1094e71	f30f114804              MOVSS X1, 0x4(AX)	
  build.go:18	0x1094e76	f30f115008              MOVSS X2, 0x8(AX)	
  build.go:20	0x1094e7b	0f57c0                  XORPS X0, X0 // nilP interface{}	
  build.go:20	0x1094e7e	0f11442470              MOVUPS X0, 0x70(SP)// nilP 開始地址爲0x70	
  build.go:21	0x1094e83	0f57c0                  XORPS X0, X0 // p Point	
  build.go:21	0x1094e86	0f11442460              MOVUPS X0, 0x60(SP)	
  build.go:23	0x1094e8b	488b442458              MOVQ 0x58(SP), AX	// nilP = &point  ;0x58(SP) 爲 point 的地址	
  build.go:23	0x1094e90	4889442448              MOVQ AX, 0x48(SP) // SP 指向 point 地址	
  build.go:23	0x1094e95	488d0da4860100          LEAQ type.*+98368(SB), CX // ;從內存加載 Point類型地址 到 CX 寄存器
  build.go:23	0x1094e9c	48894c2470              MOVQ CX, 0x70(SP) // ;將 Point類型地址(8字節) 保存到 0x70(即eface._type)	
  build.go:23	0x1094ea1	4889442478              MOVQ AX, 0x78(SP) // ;將 point 對象地址(8字節) 保存到 0x78(即eface.data)
  build.go:24	0x1094ea6	488b442458              MOVQ 0x58(SP), AX	// p = &point	
  build.go:24	0x1094eab	4889442448              MOVQ AX, 0x48(SP)	// ;SP 指向 point 地址
  build.go:24	0x1094eb0	488d0d09d50400          LEAQ go.itab.*main.point3d,main.Point(SB), CX	// ;從內存加載 Point類型 itab 地址 到 CX 寄存器
  build.go:24	0x1094eb7	48894c2460              MOVQ CX, 0x60(SP)	// ;將 Point類型地址(8字節) 保存到 0x70(即iface.tab)	
  build.go:24	0x1094ebc	4889442468              MOVQ AX, 0x68(SP)	// ;將 point 對象地址(8字節) 保存到 0x78(即iface.data)	
  build.go:25	0x1094ec1	488b442468              MOVQ 0x68(SP), AX	// fmt.Println(nilP, p)	

  ...
複製代碼

事實正如理論通常,在編譯階段,賦值命令被轉化爲類型信息和對象指針的拷貝,保存下來執行期轉換所須要的一切信息。

綜述

從底層代碼和彙編出發,分析 struct 和 interface 的 對象模型,理清了Go 語言高級特性的底層機制。再去學習反射等表層細節,事半功倍。

參考連接:

相關文章
相關標籤/搜索