<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go究竟是否為空切片分配了底層數(shù)組

          共 16324字,需瀏覽 33分鐘

           ·

          2022-07-04 17:46

          有一個這樣的問題:



          切片是Go語言中的一個重要的語法元素,也是日常Go開發(fā)中使用最為頻繁的語法元素。有過Go語言開發(fā)經(jīng)驗的童鞋估計大多都知道空切片(empty slice)與nil切片(nil slice)比較的梗,這也是Go面試中的一道高頻題。

          var sl1 = []int{} // sl1是空切片
          var sl2 []int     // sl2是nil切片

          要真正理解切片,離不開運行時的切片表示。在我的專欄[2]《Go語言精進之路》[3]一書中都有對切片在運行時表示的細致講解。

          切片在運行時由三個字段構(gòu)成,reflect包中有切片在類型系統(tǒng)中表示的對應(yīng)的定義:

          // $GOROOT/src/reflect/value.go
          type SliceHeader struct {
              Data uintptr
              Len  int
              Cap  int
          }

          基于這個定義我們來理解空切片和nil切片就容易多了。我們用一段代碼來看看這兩種切片的差別:

          // dumpslice.go
          package main

          import (
              "fmt"
              "reflect"
              "unsafe"
          )

          func main() {
              var sl1 = []int{}
              ph1 := (*reflect.SliceHeader)(unsafe.Pointer(&sl1))
              fmt.Printf("empty slice's header is %#v\n", *ph1)
              var sl2 []int
              ph2 := (*reflect.SliceHeader)(unsafe.Pointer(&sl2))
              fmt.Printf("nil slice's header is %#v\n", *ph2)
          }

          在這段代碼中,我們通過unsafe包以及reflect.SliceHeader輸出了空切片與nil切片在內(nèi)存中的表示,即SliceHeader各個字段的值。我們在Go 1.18beta2下運行一下上述代碼(使用-gcflags '-l -N'可關(guān)閉Go編譯器的優(yōu)化):

          $go run -gcflags '-l -N' dumpslice.go
          empty slice's header is reflect.SliceHeader{Data:0xc000092eb0, Len:0, Cap:0}
          nil slice'
          s header is reflect.SliceHeader{Data:0x0, Len:0, Cap:0}

          通過輸出結(jié)果,我們看到nil切片在運行時表示的三個字段值都是0;而空切片的len、cap值為0,但data值不為零

          好了,此時我們再回到本文開始處那個童鞋提出的那個問題:空切片到底分沒分配底層數(shù)組

          答案是肯定的:沒有分配!那么上述代碼中空切片在運行時表示中第一個字段data的值0xc000092eb0從何而來,難道不是底層數(shù)組的地址么?


          要想回答這個問題,我們需要下沉到匯編層面去看。

          Go使用plan9的匯編語法,目前市面上關(guān)于這種匯編的資料比較少,比較權(quán)威是Go官方的asm資料[4]和Rob Pike編寫的A Manual for the Plan 9 assembler[5]。此外IBM工程師的 Dropping down Go functions in assembly language[6]這份資料也十分不錯。國內(nèi)《Go語言高級編程》[7]一書以及曹春輝的plan9 assembly 完全解析[8]講解的十分全面,值得大家參考。

          我們以下面這段最簡單的有關(guān)空切片的代碼為例:

          // layout6.go

          1 package main

          3 func main() {
          4     var sl = []int{}
          5     _ = sl
          6 }

          生成go源碼對應(yīng)匯編代碼的主要方法有:go tool compile -S xxx.go和針對編譯后的二進制文件使用go tool objdump -S exe_file。

          我們看看這段代碼對應(yīng)的匯編代碼,我們使用下面命令將上述go源碼轉(zhuǎn)換為匯編代碼(Go 1.18beta2 on darwin amd64):

          $go tool compile -S -N -l layout6.go > layout6.s // -N -l兩個命令行選項用于關(guān)閉Go編譯器的優(yōu)化,優(yōu)化后的代碼會掩蓋實現(xiàn)細節(jié)

          (在MacOS上)生成的layout6.s匯編代碼如下(匯編代碼中的FUNCDATA和PCDATA是Go編譯器插入的、給GC使用的指示符,這里將其濾掉了):

          "".main STEXT nosplit size=48 args=0x0 locals=0x30 funcid=0x0 align=0x0
              0x0000 00000 (layout6.go:3) TEXT    "".main(SB), NOSPLIT|ABIInternal, $48-0 // 48是main函數(shù)的棧幀大小,0表示參數(shù)大小
              0x0000 00000 (layout6.go:3) SUBQ    $48, SP
              0x0004 00004 (layout6.go:3) MOVQ    BP, 40(SP)
              0x0009 00009 (layout6.go:3) LEAQ    40(SP), BP
              0x000e 00014 (layout6.go:4) LEAQ    ""..autotmp_2(SP), AX
              0x0012 00018 (layout6.go:4) MOVQ    AX, ""..autotmp_1+8(SP)
              0x0017 00023 (layout6.go:4) TESTB   AL, (AX)
              0x0019 00025 (layout6.go:4) JMP 27
              0x001b 00027 (layout6.go:4) MOVQ    AX, "".sl+16(SP)
              0x0020 00032 (layout6.go:4) MOVUPS  X15, "".sl+24(SP)
              0x0026 00038 (layout6.go:6) MOVQ    40(SP), BP
              0x002b 00043 (layout6.go:6) ADDQ    $48, SP
              0x002f 00047 (layout6.go:6) RET
              0x0000 48 83 ec 30 48 89 6c 24 28 48 8d 6c 24 28 48 8d  H..0H.l$(H.l$(H.
              0x0010 04 24 48 89 44 24 08 84 00 eb 00 48 89 44 24 10  .$H.D$.....H.D$.
              0x0020 44 0f 11 7c 24 18 48 8b 6c 24 28 48 83 c4 30 c3  D..|$.H.l$(H..0.
          go.cuinfo.packagename. SDWARFCUINFO dupok size=0
              0x0000 6d 61 69 6e                                      main
          ""..inittask SNOPTRDATA size=24
              0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
              0x0010 00 00 00 00 00 00 00 00                          ........
          gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
              0x0000 01 00 00 00 00 00 00 00                          ........
          gclocals·ff19ed39bdde8a01a800918ac3ef0ec7 SRODATA dupok size=9
              0x0000 01 00 00 00 04 00 00 00 00                       .........

          關(guān)于匯編語法的問題,大家可以參考前面提供的參考資料,這里不贅述。我們這里最關(guān)注的是對應(yīng)Go源碼第4行Go代碼的匯編源碼,這里我把這段匯編源碼單獨提出來放在下面:

              0x000e 00014 (layout6.go:4) LEAQ    ""..autotmp_2(SP), AX
              0x0012 00018 (layout6.go:4) MOVQ    AX, ""..autotmp_1+8(SP)
              0x0017 00023 (layout6.go:4) TESTB   AL, (AX)
              0x0019 00025 (layout6.go:4) JMP 27
              0x001b 00027 (layout6.go:4) MOVQ    AX, "".sl+16(SP)
              0x0020 00032 (layout6.go:4) MOVUPS  X15, "".sl+24(SP)

          我們逐行看一下:

          • 00014行:將SP寄存器指向的內(nèi)存單元(該內(nèi)存單元被命名為autotmp_2)的地址存入AX寄存器中;
          • 00019行:將AX寄存器中存儲的值寫入地址為SP+8的內(nèi)存單元中,這個內(nèi)存單元被命名為autotmp_1;
          • 00023行:將AL寄存器中的值與AX寄存器指向的內(nèi)存單元的值做邏輯與操作,設(shè)置相關(guān)標志位;
          • 00025行:無條件跳轉(zhuǎn)至00027行執(zhí)行;
          • 00027行:將AX寄存器中存儲的值寫入sl切片變量運行時表示的第一個字段data中,該字段的地址為SP+16;
          • 00032行:使用intel平臺上的SIMD指令集SSE的MOVUPS指令通過X15代表的固定的零寄存器對起始地址為SP+24的連續(xù)128bit(16個字節(jié))進行清零。即sl切片變量運行時的len和cap字段被清零。

          關(guān)于X15寄存器的含義,在Go internal ABI specification[9]中有說明。

          我這里用一幅圖展示一下上面操作后的main函數(shù)棧情況:

          我們看到切片sl的指向底層數(shù)組的指針data的值實際上是一個棧上的內(nèi)存單元的地址,Go編譯器并沒有在堆上額外分配新的內(nèi)存空間作為切片sl的底層數(shù)組。只是上面匯編代碼的第00019行、00023行的操作讓人很迷,不知道這兩部指令操作的意圖為何。

          我們再來看一個例子,以進一步證實我們上面的結(jié)論。這個例子的源碼如下:

          // layout7.go
          1 package main

          3 func main() {
          4     var sl = []int{}
          5     sl = append(sl, 1)
          6 }

          在這個例子中,我們先是聲明了一個空切片sl,之后又通過append為sl追加了一個元素。append時,由于sl為空切片,Go勢必會為sl新分配底層存儲數(shù)組,我們通過對比一下第4行和第5行兩個操作的異同來確認“空切片并未分配底層數(shù)組”的結(jié)論。我們同樣通過go tool compile -S命令得到該源碼對應(yīng)的匯編代碼:

          $go tool compile -S -N -l layout7.go > layout7.s

          layout7.s中main函數(shù)的匯編代碼如下(過濾掉了PCDATA和FUNCDATA指示符行):

          "".main STEXT size=114 args=0x0 locals=0x70 funcid=0x0 align=0x0
              0x0000 00000 (layout7.go:3) TEXT    "".main(SB), ABIInternal, $112-0
              0x0000 00000 (layout7.go:3) CMPQ    SP, 16(R14)
              0x0004 00004 (layout7.go:3) JLS 107
              0x0006 00006 (layout7.go:3) SUBQ    $112, SP
              0x000a 00010 (layout7.go:3) MOVQ    BP, 104(SP)
              0x000f 00015 (layout7.go:3) LEAQ    104(SP), BP
              0x0014 00020 (layout7.go:4) LEAQ    ""..autotmp_2+64(SP), BX
              0x0019 00025 (layout7.go:4) MOVQ    BX, ""..autotmp_1+72(SP)
              0x001e 00030 (layout7.go:4) TESTB   AL, (BX)
              0x0020 00032 (layout7.go:4) JMP 34
              0x0022 00034 (layout7.go:4) MOVQ    BX, "".sl+80(SP)
              0x0027 00039 (layout7.go:4) MOVUPS  X15, "".sl+88(SP)
              0x002d 00045 (layout7.go:5) JMP 47
              0x002f 00047 (layout7.go:5) LEAQ    type.int(SB), AX
              0x0036 00054 (layout7.go:5) XORL    CX, CX
              0x0038 00056 (layout7.go:5) MOVQ    CX, DI
              0x003b 00059 (layout7.go:5) MOVL    $1, SI
              0x0040 00064 (layout7.go:5) CALL    runtime.growslice(SB)
              0x0045 00069 (layout7.go:5) LEAQ    1(BX), DX
              0x0049 00073 (layout7.go:5) JMP 75
              0x004b 00075 (layout7.go:5) MOVQ    $1, (AX)
              0x0052 00082 (layout7.go:5) MOVQ    AX, "".sl+80(SP)
              0x0057 00087 (layout7.go:5) MOVQ    DX, "".sl+88(SP)
              0x005c 00092 (layout7.go:5) MOVQ    CX, "".sl+96(SP)
              0x0061 00097 (layout7.go:6) MOVQ    104(SP), BP
              0x0066 00102 (layout7.go:6) ADDQ    $112, SP
              0x006a 00106 (layout7.go:6) RET
              0x006b 00107 (layout7.go:6) NOP
              0x006b 00107 (layout7.go:3) CALL    runtime.morestack_noctxt(SB)
              0x0070 00112 (layout7.go:3) JMP 0
              ... ...

          有了對layout6.s的匯編的分析的基礎(chǔ),再來看這段匯編似乎就好很多了。首先layout7.s中對應(yīng)var sl = []int{}代碼的第00020到00039的原理與layout6.s一致。sl的data字段被賦值為一個棧上內(nèi)存單元(SP+64)的地址。

          從第00047到00073實際上是為調(diào)用runtime.growslice函數(shù)做準備以及調(diào)用runtime.growslice函數(shù)。runtime.growslice函數(shù)負責在堆上分配新的底層數(shù)組用于存儲切片sl的元素。runtime.growslice返回后,我們看到,第00075行,Go將一個立即數(shù)1寫入AX寄存器指向的內(nèi)存單元,即growslice新分配的底層數(shù)組的第一個元素的內(nèi)存單元。

          之后,sl的三個字段被重新做了賦值:

              0x0052 00082 (layout7.go:5) MOVQ    AX, "".sl+80(SP)
              0x0057 00087 (layout7.go:5) MOVQ    DX, "".sl+88(SP)
              0x005c 00092 (layout7.go:5) MOVQ    CX, "".sl+96(SP)

          我們看到:00082行,sl的data字段(SP+80)被賦值為AX寄存器中的值,即堆上分配新的底層數(shù)組的地址。而后的len和cap字段也分配用DX和CX寄存器的值做了賦值,這兩個寄存器分配存儲了切片的len和cap。

          我這里同樣用一幅示意圖展示append后main函數(shù)棧的情況:

          通過這個例子,我們可以看到,如果Go在堆上為切片分配底層數(shù)組,我們會在匯編代碼中看到growslice或newobject這樣的調(diào)用。

          如果一個非空切片沒有逃逸到堆上,那么Go也可能在棧上為該切片分配底層數(shù)組空間,比如下面這段代碼:

          // layout10.go
          package main

          func main() {
              var sl = []int{11, 12, 13}
              _ = sl
          }

          它對應(yīng)的匯編如下:

          "".main STEXT nosplit size=103 args=0x0 locals=0x40 funcid=0x0 align=0x0
              0x0000 00000 (layout10.go:3)    TEXT    "".main(SB), NOSPLIT|ABIInternal, $64-0
              0x0000 00000 (layout10.go:3)    SUBQ    $64, SP
              0x0004 00004 (layout10.go:3)    MOVQ    BP, 56(SP)
              0x0009 00009 (layout10.go:3)    LEAQ    56(SP), BP
              0x000e 00014 (layout10.go:4)    MOVUPS  X15, ""..autotmp_2(SP)
              0x0013 00019 (layout10.go:4)    MOVUPS  X15, ""..autotmp_2+8(SP)
              0x0019 00025 (layout10.go:4)    LEAQ    ""..autotmp_2(SP), AX
              0x001d 00029 (layout10.go:4)    MOVQ    AX, ""..autotmp_1+24(SP)
              0x0022 00034 (layout10.go:4)    TESTB   AL, (AX)
              0x0024 00036 (layout10.go:4)    MOVQ    $11""..autotmp_2(SP)
              0x002c 00044 (layout10.go:4)    TESTB   AL, (AX)
              0x002e 00046 (layout10.go:4)    MOVQ    $12""..autotmp_2+8(SP)
              0x0037 00055 (layout10.go:4)    TESTB   AL, (AX)
              0x0039 00057 (layout10.go:4)    MOVQ    $13""..autotmp_2+16(SP)
              0x0042 00066 (layout10.go:4)    TESTB   AL, (AX)
              0x0044 00068 (layout10.go:4)    JMP 70
              0x0046 00070 (layout10.go:4)    MOVQ    AX, "".sl+32(SP)
              0x004b 00075 (layout10.go:4)    MOVQ    $3"".sl+40(SP)
              0x0054 00084 (layout10.go:4)    MOVQ    $3"".sl+48(SP)
              0x005d 00093 (layout10.go:6)    MOVQ    56(SP), BP
              0x0062 00098 (layout10.go:6)    ADDQ    $64, SP
              0x0066 00102 (layout10.go:6)    RET

          這段匯編代碼就留給大家自己閱讀分析吧。

          參考資料

          [1] 

          “Go語言第一課”: http://gk.link/a/10AVZ

          [2] 

          專欄: http://gk.link/a/10AVZ

          [3] 

          《Go語言精進之路》: https://book.douban.com/subject/35720728/

          [4] 

          Go官方的asm資料: https://go.dev/doc/asm

          [5] 

          A Manual for the Plan 9 assembler: https://9p.io/sys/doc/asm.html

          [6] 

          Dropping down Go functions in assembly language: https://github.com/golang/go/files/447163/GoFunctionsInAssembly.pdf

          [7] 

          《Go語言高級編程》: https://book.douban.com/subject/34442131/

          [8] 

          plan9 assembly 完全解析: https://go.xargin.com/docs/assembly/assembly/

          [9] 

          Go internal ABI specification: https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-abi.md



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復 ebook 獲取;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。

          瀏覽 17
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  精品国产一区二区有限公司 | 国内精品国产三级国产在线专 | 精品人妻无码一区二区三级精东 | 日本一级黄色作爱视频网站 | 日韩午夜精品一区二区三区 |