<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>

          淺談Golang內(nèi)存對齊

          共 8817字,需瀏覽 18分鐘

           ·

          2021-10-08 21:25

          如果你在 golang spec[1] 里以「alignment」為關(guān)鍵字搜索的話,那么會發(fā)現(xiàn)與此相關(guān)的內(nèi)容并不多,只是在結(jié)尾介紹 unsafe 包的時候提了一下,不過別忘了字兒越少事兒越大:

          Computer architectures may require memory addresses to be aligned; that is, for addresses of a variable to be a multiple of a factor, the variable’s type’s alignment. The function Alignof takes an expression denoting a variable of any type and returns the alignment of the (type of the) variable in bytes. For a variable x:

          uintptr(unsafe.Pointer(&x))?%?unsafe.Alignof(x)?==?0

          The following minimal alignment properties are guaranteed:

          • For a variable x of any type: unsafe.Alignof(x) is at least 1.
          • For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
          • For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array’s element type.

          當然,如果你以前沒有接觸過內(nèi)存對齊的話,那么對你來說上面的內(nèi)容可能過于言簡意賅,在繼續(xù)學(xué)習(xí)之前我建議你閱讀以下資料,有助于消化理解:

          • 圖解 Go 之內(nèi)存對齊[2]
          • 在 Go 中恰到好處的內(nèi)存對齊[3]
          • Go 結(jié)構(gòu)體的內(nèi)存布局[4]
          • Golang 是否有必要內(nèi)存對齊[5]

          測試

          我構(gòu)造了一個 struct,它有一個特征:字段按照一小一大的順序排列,如果不看注釋中的 Sizeof、Alignof、Offsetof 信息(通過 unsafe 獲取),你能否說出它占用多少個字節(jié)?

          package?main

          import?(
          ?"fmt"
          ?"unsafe"
          )

          type?memAlign?struct?{
          ?a?byte?????//?Sizeof:?1??Alignof:?1?Offsetof:?0
          ?b?int??????//?Sizeof:?8??Alignof:?8?Offsetof:?8
          ?c?byte?????//?Sizeof:?1??Alignof:?1?Offsetof:?16
          ?d?string???//?Sizeof:?16?Alignof:?8?Offsetof:?24
          ?e?byte?????//?Sizeof:?1??Alignof:?1?Offsetof:?40
          ?f?[]string?//?Sizeof:?24?Alignof:?8?Offsetof:?48
          }

          func?main()?{
          ?var?m?memAlign
          ?fmt.Println(unsafe.Sizeof(m))
          }

          初學(xué)者往往會認為 struct 的大小應(yīng)該等于內(nèi)部各個字段大小的和,于是得出本例的答案是 51(1+8+1+16+1+24=51),不過實際上答案卻是 72!究其原因是因為內(nèi)存對齊的緣故導(dǎo)致各個字段之間可能存在 padding。那么有沒有簡單的方法來減少 padding 呢?我們不妨把字段按照從大到小的順序排列,再試一試:

          package?main

          import?(
          ?"fmt"
          ?"unsafe"
          )

          type?memAlign?struct?{
          ?f?[]string?//?Sizeof:?24?Alignof:?8?Offsetof:?0
          ?d?string???//?Sizeof:?16?Alignof:?8?Offsetof:?24
          ?b?int??????//?Sizeof:?8??Alignof:?8?Offsetof:?40
          ?a?byte?????//?Sizeof:?1??Alignof:?1?Offsetof:?48
          ?c?byte?????//?Sizeof:?1??Alignof:?1?Offsetof:?49
          ?e?byte?????//?Sizeof:?1??Alignof:?1?Offsetof:?50
          }

          func?main()?{
          ?var?m?memAlign
          ?fmt.Println(unsafe.Sizeof(m))
          }

          結(jié)果答案變成了 56,比 72 小了很多,不過還是比 51 大,說明還是存在 padding,這是因為不僅字段要內(nèi)存對齊,struct 本身也要內(nèi)存對齊。

          另:我剛學(xué) golang 的時候一直有一個疑問:為什么切片的大小是 24,字符串的大小是 16 呢?我估計別的初學(xué)者也會有類似的問題,一并解釋一下,這是因為切片和字符串也是 struct,其定義分別對應(yīng) SliceHeader[6]StringHeader[7],它們的大小分別是 24 和 16:

          type?SliceHeader?struct?{
          ?Data?uintptr
          ?Len??int
          ?Cap??int
          }

          type?StringHeader?struct?{
          ?Data?uintptr
          ?Len??int
          }

          因為 uintptr 的大小等于 int,所以切片的大小等于 3*8=24,字符串的大小等于 2*8=16。

          工具

          只要我們寫點代碼,調(diào)用 unsafe 包的 Sizeof、Alignof、Offsetof 等方法,那么就可以搞清楚 struct 內(nèi)存對齊的各種細節(jié),不過這畢竟是個沒有技術(shù)含量的體力活,有沒有相關(guān)工具可以提升我們的工作效率呢?答案是 go-tools[8]

          shell>?go?install?honnef.co/go/tools/cmd/structlayout@latest
          shell>?go?install?honnef.co/go/tools/cmd/structlayout-pretty@latest
          shell>?go?install?honnef.co/go/tools/cmd/structlayout-optimize@latest

          其中,structlayout 是用來分析數(shù)據(jù)的,pretty 是用來圖形化顯示的,optimize 是用來優(yōu)化建議的,這里就用文章開頭優(yōu)化前的代碼給出一個 structlayout-pretty 的例子:

          shell>?structlayout?-json?./main.go?memAlign?|?structlayout-pretty
          structlayout-pretty

          雖然 structlayout-pretty 我們可以很直觀的看到在哪里存在 padding,不過它是 ascii 風(fēng)格的,有時候不太方便,此時另外一個圖形化工具 structlayout-svg[9] 更爽:

          shell>?go?install?github.com/ajstarks/svgo/structlayout-svg@latest

          把文章開頭優(yōu)化前后的代碼分別用 structlayout-svg 生成結(jié)果:

          shell>?structlayout?-json?./main.go?memAlign?|?structlayout-svg

          優(yōu)化前:

          優(yōu)化前

          優(yōu)化后:

          優(yōu)化后

          效果超贊是不是!不過如果我們要把工具集成到 CI 里,那么此類圖形化工具就不合適了,好在我們的工具箱里還有寶貝,它就是 fieldalignment[10]

          shell>?go?install?golang.org/x/tools/...@latest

          把文章開頭優(yōu)化前后的代碼分別用 fieldalignment 生成結(jié)果:

          shell>?awk?'$1?==?"module"?{print?$2}'?./go.mod?|?xargs?fieldalignment

          優(yōu)化前:struct of size 72 could be 56;優(yōu)化后:struct with 32 pointer bytes could be 24。

          如上可見,fieldalignment 準確判斷出優(yōu)化前代碼的 struct size 存在優(yōu)化空間;但是優(yōu)化后代碼的 pointer bytes 是什么鬼?按照文檔中的說明,pointer bytes 的含義如下:

          Pointer bytes is how many bytes of the object that the garbage collector has to potentially scan for pointers, for example:

          struct?{?uint32;?string?}

          have 16 pointer bytes because the garbage collector has to scan up through the string’s inner pointer.

          struct?{?string;?*uint32?}

          has 24 pointer bytes because it has to scan further through the *uint32.

          struct?{?string;?uint32?}

          has 8 because it can stop immediately after the string pointer.

          看到這里,不禁讓人產(chǎn)生疑惑:GC 不會這么傻吧,難道它還要一個字節(jié)一個字節(jié)的掃描內(nèi)存么?讓我們做個實驗測試一下 pointer bytes 有沒有影響,正所謂有病沒病走兩步:

          package?main

          import?(
          ?"runtime"
          ?"time"
          )

          //?pointer?bytes:?8
          type?foo?struct?{
          ?S?string
          ?U?uint32
          }

          //?pointer?bytes:?16
          type?bar?struct?{
          ?U?uint32
          ?S?string
          }

          //?GODEBUG=gctrace=1?go?run?main.go
          func?main()?{
          ?v?:=?make([]foo,?1e8)
          ?//?v?:=?make([]bar,?1e8)
          ?for?range?time.Tick(time.Second)?{
          ??runtime.GC()
          ?}
          ?runtime.KeepAlive(v)
          }

          代碼里構(gòu)造了一個巨大的切片變量,棧必然保存不了,于是變量會逃逸到堆,接著周期性的調(diào)用 runtime.GC 來手動觸發(fā) GC,然后執(zhí)行的時候通過 GODEBUG=gctrace=1 獲取實時的 GC 相關(guān)信息。結(jié)果顯示,不管是小 pointer bytes 的 foo,還是大 pointer bytes 的 bar,最終 GC 消耗的時間差不多。換句話說,pointer bytes 的大小對 GC 的影響很小很小,在 golang 的相關(guān) issue[11] 的討論中,也能印證此結(jié)論,篇幅所限,這里就不多說了。

          另:命令輸出的 gctrace 信息比較多,相關(guān)格式說明可以參考 runtime[12] 中的注釋信息。

          例子

          了解了內(nèi)存對齊的相關(guān)知識后,讓我們看看現(xiàn)實世界中的例子,首先是 groupcache[13]

          type?Group?struct?{
          ?name?string
          ?getter?Getter
          ?peersOnce?sync.Once
          ?peers?PeerPicker
          ?cacheBytes?int64
          ?mainCache?cache
          ?hotCache?cache
          ?loadGroup?flightGroup
          ?_?int32?//?force?Stats?to?be?8-byte?aligned?on?32-bit?platforms
          ?Stats?Stats
          }

          通過注釋我們可以看到,為了強制讓 Stats 在 32 位平臺上按 8 字節(jié)對齊,在 Stats 字段的前面加了一個「_ int32」,換句話說,就是加了 4 個字節(jié),那么為什么要這么做?

          原因是 Stats 字段要參與 atomic 原子運算,關(guān)于 atomic[14],文檔最后記錄了如下內(nèi)容:

          On ARM, 386, and 32-bit MIPS, it is the caller’s responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

          也就是說,在 32 位平臺,調(diào)用者有責(zé)任自己保證原子操作是 64 位對齊的,此外,struct 中第一個字段可以被認為是 64 位對齊的。在本例中,因為 Stats 字段要參與 atomic 運算,而且不是第一個字段,所以我們必須手動保證它是 64 位對齊的,不過加了 _ int32 就能保證是 64 位對齊的么?讓我們寫代碼驗證一下:

          package?main

          import?(
          ?"fmt"
          ?"unsafe"

          ?"github.com/golang/groupcache"
          )

          //?GOARCH=386?go?run?main.go
          func?main()?{
          ?var?g?groupcache.Group
          ?fmt.Println(unsafe.Offsetof(g.Stats))
          }

          結(jié)果顯示在 32 位下運行,Stats 的 offset 是 176,是 8 的倍數(shù),滿足 64 位對齊。如果沒有「_ int32」做 padding,那么 Stats 的 offset 將是 172,就不再是 8 的倍數(shù)了。

          再看看 sync.WaitGroup 中內(nèi)存對齊的例子:

          type?WaitGroup?struct?{
          ?noCopy?noCopy

          ?//?64-bit?value:?high?32?bits?are?counter,?low?32?bits?are?waiter?count.
          ?//?64-bit?atomic?operations?require?64-bit?alignment,?but?32-bit
          ?//?compilers?do?not?ensure?it.?So?we?allocate?12?bytes?and?then?use
          ?//?the?aligned?8?bytes?in?them?as?state,?and?the?other?4?as?storage
          ?//?for?the?sema.
          ?state1?[3]uint32
          }

          func?(wg?*WaitGroup)?state()?(statep?*uint64,?semap?*uint32)?{
          ?if?uintptr(unsafe.Pointer(&wg.state1))%8?==?0?{
          ??return?(*uint64)(unsafe.Pointer(&wg.state1)),?&wg.state1[2]
          ?}?else?{
          ??return?(*uint64)(unsafe.Pointer(&wg.state1[1])),?&wg.state1[0]
          ?}
          }

          首先,noCopy 是什么鬼,其實它的作用就像名字一樣,它是如何實現(xiàn)的呢,看注釋:

          //?noCopy?may?be?embedded?into?structs?which?must?not?be?copied
          //?after?the?first?use.
          //
          //?See?https://golang.org/issues/8005#issuecomment-190753527
          //?for?details.
          type?noCopy?struct{}

          //?Lock?is?a?no-op?used?by?-copylocks?checker?from?`go?vet`.
          func?(*noCopy)?Lock()???{}
          func?(*noCopy)?Unlock()?{}

          實際上它只是起到標識的作用,以便 go vet 能夠借此發(fā)現(xiàn)問題,詳細說明在 issue[15] 中有描述,如果你在自己的項目里有類似 noCopy 的需求,那么也可以照貓畫虎,

          接下來是內(nèi)存對齊相關(guān)的重頭戲了,state1 字段是一個有 3 個元素的 uint32 數(shù)組,它會保存兩種數(shù)據(jù),分別是 statep 和 semap,其中,statep 要參與 atomic 運算,所以我們要保證它是 64 位對齊的。如果「uintptr(unsafe.Pointer(&wg.state1))%8 == 0」成立,那么取前兩個 int32 做 statep,否則取后兩個 int32 做 statep。

          為什么可以這樣做?因為「uintptr(unsafe.Pointer(&wg.state1))%8 == 0」成立的時候,前兩個 int32 自然滿足 64 位對齊;當「uintptr(unsafe.Pointer(&wg.state1))%8 == 0」不成立的時候, 其運算結(jié)果必然等于 4,此時我們正好可以把第一個 int32 當作是一個 4 字節(jié)的 padding,于是后兩個字節(jié)的 int32 就又滿足 64 位對齊了。

          如果你認為自己理解了,那么思考一下,在定義 state1 的時候,如果不用 [3]int32,而是換成一個 int64 加上一個 int32,或者是一個 [12]byte,它們都是 12 個字節(jié),是否可以?

          參考資料

          [1]

          golang spec: https://golang.org/ref/spec

          [2]

          圖解 Go 之內(nèi)存對齊: http://blog.newbmiao.com/slides/%E5%9B%BE%E8%A7%A3Go%E4%B9%8B%E5%86%85%E5%AD%98%E5%AF%B9%E9%BD%90.pdf

          [3]

          在 Go 中恰到好處的內(nèi)存對齊: https://eddycjy.gitbook.io/golang/di-1-ke-za-tan/go-memory-align

          [4]

          Go 結(jié)構(gòu)體的內(nèi)存布局: https://www.liwenzhou.com/posts/Go/struct_memory_layout/

          [5]

          Golang 是否有必要內(nèi)存對齊: https://ms2008.github.io/2019/08/01/golang-memory-alignment/

          [6]

          SliceHeader: https://pkg.go.dev/reflect#SliceHeader

          [7]

          StringHeader: https://pkg.go.dev/reflect#StringHeader

          [8]

          go-tools: https://github.com/dominikh/go-tools

          [9]

          structlayout-svg: https://github.com/ajstarks/svgo/tree/master/structlayout-svg

          [10]

          fieldalignment: https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md#fieldalignment

          [11]

          issue: https://github.com/golang/go/issues/44877#issuecomment-794565908

          [12]

          runtime: https://pkg.go.dev/runtime

          [13]

          groupcache: https://github.com/golang/groupcache/blob/master/groupcache.go

          [14]

          atomic: https://pkg.go.dev/sync/atomic

          [15]

          issue: https://github.com/golang/go/issues/8005#issuecomment-190753527



          推薦閱讀


          福利

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

          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  久久免费黄片视频 | 日韩三级片网站在线观看 | 午夜国产操逼视频 | 手机在线免费片片AAA | 做爱吃逼逼视频 |