1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 16324字,需瀏覽 33分鐘

         ·

        2022-07-04 17:46

        有一個(gè)這樣的問(wèn)題:



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

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

        要真正理解切片,離不開(kāi)運(yùn)行時(shí)的切片表示。在我的專(zhuān)欄[2]《Go語(yǔ)言精進(jìn)之路》[3]一書(shū)中都有對(duì)切片在運(yùn)行時(shí)表示的細(xì)致講解。

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

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

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

        // 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)
        }

        在這段代碼中,我們通過(guò)unsafe包以及reflect.SliceHeader輸出了空切片與nil切片在內(nèi)存中的表示,即SliceHeader各個(gè)字段的值。我們?cè)?a style="color: rgb(30, 107, 184);font-weight: bold;border-bottom: 1px solid rgb(30, 107, 184);" data-linktype="2">Go 1.18beta2下運(yùn)行一下上述代碼(使用-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}

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

        好了,此時(shí)我們?cè)倩氐奖疚拈_(kāi)始處那個(gè)童鞋提出的那個(gè)問(wèn)題:空切片到底分沒(méi)分配底層數(shù)組?

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


        要想回答這個(gè)問(wèn)題,我們需要下沉到匯編層面去看。

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

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

        // layout6.go

        1 package main

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

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

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

        $go tool compile -S -N -l layout6.go > layout6.s // -N -l兩個(gè)命令行選項(xiàng)用于關(guān)閉Go編譯器的優(yōu)化,優(yōu)化后的代碼會(huì)掩蓋實(shí)現(xiàn)細(xì)節(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)于匯編語(yǔ)法的問(wèn)題,大家可以參考前面提供的參考資料,這里不贅述。我們這里最關(guān)注的是對(duì)應(yīng)Go源碼第4行Go代碼的匯編源碼,這里我把這段匯編源碼單獨(dú)提出來(lái)放在下面:

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

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

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

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

        我們?cè)賮?lái)看一個(gè)例子,以進(jìn)一步證實(shí)我們上面的結(jié)論。這個(gè)例子的源碼如下:

        // layout7.go
        1 package main

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

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

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

        layout7.s中main函數(shù)的匯編代碼如下(過(guò)濾掉了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
            ... ...

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

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

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

            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寄存器的值做了賦值,這兩個(gè)寄存器分配存儲(chǔ)了切片的len和cap。

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

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

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

        // layout10.go
        package main

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

        它對(duì)應(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語(yǔ)言第一課”: http://gk.link/a/10AVZ

        [2] 

        專(zhuān)欄: http://gk.link/a/10AVZ

        [3] 

        《Go語(yǔ)言精進(jìn)之路》: 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語(yǔ)言高級(jí)編程》: 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



        推薦閱讀


        福利

        我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲?。贿€可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

        瀏覽 17
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            五月丁香欧美综合 | 免费啪啪视频 | 红桃无码 | 五月天色度导航 | 亚洲老女人性爱视频 | 亚洲乱码一区二区三区 | 暴插国产 | 新婚之夜初尝高潮电影 | 综合网操笔 | 一级全黄色片 |