<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 內(nèi)存對(duì)齊的那些事兒

          共 5056字,需瀏覽 11分鐘

           ·

          2021-09-30 20:40

          在討論內(nèi)存對(duì)齊前我們先看一個(gè)思考題,我們都知道Go的結(jié)構(gòu)體在內(nèi)存中是由一塊連續(xù)的內(nèi)存表示的,那么下面的結(jié)構(gòu)體占用的內(nèi)存大小是多少呢?

          type ST1 struct {
           A byte
           B int64
           C byte
          }

          在64位系統(tǒng)下 byte 類型就只占1字節(jié),int64 占用的是8個(gè)字節(jié),按照數(shù)據(jù)類型占的字節(jié)數(shù)推理,很快就能得出結(jié)論:這個(gè)結(jié)構(gòu)體的內(nèi)存大小是10個(gè)字節(jié) (1 + 8 +1 )。這個(gè)推論到底對(duì)不對(duì)呢?我們讓 Golang 自己揭曉一下答案。

          package main

          import (
           "fmt"
           "unsafe"
          )

          type ST1 struct {
           A byte
           B int64
           C byte
          }


          func main() {
           fmt.Println("ST1.A 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{}.A)))
           fmt.Println("ST1.A 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{}.A)))
           fmt.Println("ST1.B 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{}.B)))
           fmt.Println("ST1.B 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{}.B)))
           fmt.Println("ST1.C 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{}.C)))
           fmt.Println("ST1.C 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{}.C)))
           fmt.Println("ST1結(jié)構(gòu)體 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{})))
           fmt.Println("ST1結(jié)構(gòu)體 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{})))
          }

          ## 輸出
          ST1.A 占用的字節(jié)數(shù)是:1
          ST1.A 對(duì)齊的字節(jié)數(shù)是:1
          ST1.B 占用的字節(jié)數(shù)是:8
          ST1.B 對(duì)齊的字節(jié)數(shù)是:8
          ST1.C 占用的字節(jié)數(shù)是:1
          ST1.C 對(duì)齊的字節(jié)數(shù)是:1
          ST1結(jié)構(gòu)體 占用的字節(jié)數(shù)是:24
          ST1結(jié)構(gòu)體 對(duì)齊的字節(jié)數(shù)是:8

          Golang 告訴我們 ST1 結(jié)構(gòu)體占用的字節(jié)數(shù)是24。但是每個(gè)字段占用的字節(jié)數(shù)總共加起來確實(shí)是只有10個(gè)字節(jié),這是怎么回事呢?

          因?yàn)樽侄蜝占用的字節(jié)數(shù)是8,內(nèi)存對(duì)齊的字節(jié)數(shù)也是8,A字段所在的8個(gè)字節(jié)里不足以存放字段B,所以只好留下7個(gè)字節(jié)的空洞,在下一個(gè) 8 字節(jié)存放字段B。又因?yàn)榻Y(jié)構(gòu)體ST1是8字節(jié)對(duì)齊的(可以理解為占的內(nèi)存空間必須是8字節(jié)的倍數(shù),且起始地址能夠整除8),所以 C 字段占據(jù)了下一個(gè)8字節(jié),但是又留下了7個(gè)字節(jié)的空洞。

          這樣ST1結(jié)構(gòu)體總共占用的字節(jié)數(shù)正好是 24 字節(jié)。

          既然知道了 Go 編譯器在對(duì)結(jié)構(gòu)體進(jìn)行內(nèi)存對(duì)齊的時(shí)候會(huì)在字段之間留下內(nèi)存空洞,那么我們把只需要 1 個(gè)字節(jié)對(duì)齊的字段 C 放在需要 8 個(gè)字節(jié)內(nèi)存對(duì)齊的字段 B 前面就能讓結(jié)構(gòu)體 ST1 少占 8 個(gè)字節(jié)。下面我們把 ST1 的 C 字段放在 B 的前面再觀察一下 ST1 結(jié)構(gòu)體的大小。

          package main

          import (
           "fmt"
           "unsafe"
          )

          type ST1 struct {
           A byte
           C byte
           B int64
          }


          func main() {
           fmt.Println("ST1.A 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{}.A)))
           fmt.Println("ST1.A 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{}.A)))
           fmt.Println("ST1.B 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{}.B)))
           fmt.Println("ST1.B 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{}.B)))
           fmt.Println("ST1.C 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{}.C)))
           fmt.Println("ST1.C 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{}.C)))
           fmt.Println("ST1結(jié)構(gòu)體 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST1{})))
           fmt.Println("ST1結(jié)構(gòu)體 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST1{})))
          }

          ## 輸出

          ST1.A 占用的字節(jié)數(shù)是:1
          ST1.A 對(duì)齊的字節(jié)數(shù)是:1
          ST1.B 占用的字節(jié)數(shù)是:8
          ST1.B 對(duì)齊的字節(jié)數(shù)是:8
          ST1.C 占用的字節(jié)數(shù)是:1
          ST1.C 對(duì)齊的字節(jié)數(shù)是:1
          ST1結(jié)構(gòu)體 占用的字節(jié)數(shù)是:16
          ST1結(jié)構(gòu)體 對(duì)齊的字節(jié)數(shù)是:8

          重排字段后,ST1 結(jié)構(gòu)體的內(nèi)存布局變成了下圖這樣

          僅僅只是調(diào)換了一下順序,結(jié)構(gòu)體 ST1 就減少了三分之一的內(nèi)存占用空間。在實(shí)際編程應(yīng)用時(shí)大部分時(shí)候我們不用太過于注意內(nèi)存對(duì)齊對(duì)數(shù)據(jù)結(jié)構(gòu)空間的影響,不過作為工程師了解內(nèi)存對(duì)齊這個(gè)知識(shí)還是很重要的,它實(shí)際上是一種典型的以空間換時(shí)間的策略。

          內(nèi)存對(duì)齊

          操作系統(tǒng)在讀取數(shù)據(jù)的時(shí)候并非按照我們想象的那樣一個(gè)字節(jié)一個(gè)字節(jié)的去讀取,而是一個(gè)字一個(gè)字的去讀取。

          字是用于表示其自然的數(shù)據(jù)單位,也叫machine word。字是系統(tǒng)用來一次性處理事務(wù)的一個(gè)固定長度。

          字長 / 步長 就是一個(gè)字可容納的字節(jié)數(shù),一般 N 位系統(tǒng)的字長是 (N / 8) 個(gè)字節(jié)。

          因此,當(dāng) CPU 從存儲(chǔ)器讀數(shù)據(jù)到寄存器,或者從寄存器寫數(shù)據(jù)到存儲(chǔ)器,每次 IO 的數(shù)據(jù)長度是字長。如 32 位系統(tǒng)訪問粒度是 4 字節(jié)(bytes),64 位系統(tǒng)的就是 8 字節(jié)。當(dāng)被訪問的數(shù)據(jù)長度為 n 字節(jié)且該數(shù)據(jù)的內(nèi)存地址為 n 字節(jié)對(duì)齊,那么操作系統(tǒng)就可以高效地一次定位到數(shù)據(jù),無需多次讀取、處理對(duì)齊運(yùn)算等額外操作。

          內(nèi)存對(duì)齊的原則是:將數(shù)據(jù)盡量的存儲(chǔ)在一個(gè)字長內(nèi),避免跨字長的存儲(chǔ)

          Go 官方文檔中對(duì)數(shù)據(jù)類型的內(nèi)存對(duì)齊也有如下保證:

          1. 對(duì)于任何類型的變量 x,unsafe.Alignof(x) 的結(jié)果最小為1 (類型最小是一字節(jié)對(duì)齊的)。
          2. 對(duì)于一個(gè)結(jié)構(gòu)體類型的變量 x,unsafe.Alignof(x) 的結(jié)果為 x 的所有字段的對(duì)齊字節(jié)數(shù)中的最大值。
          3. 對(duì)于一個(gè)數(shù)組類型的變量 x , unsafe.Alignof(x) 的結(jié)果和此數(shù)組的元素類型的一個(gè)變量的對(duì)齊字節(jié)數(shù)相等,也就是 unsafe.Alignof(x) == unsafe.Alignof(x[i])

          下面這個(gè)表格列出了每種數(shù)據(jù)類型對(duì)齊的字節(jié)數(shù)

          數(shù)據(jù)類型對(duì)齊字節(jié)數(shù)
          bool, byte, unit8 int81
          uint16, int162
          uint32, int32, float32, complex644
          uint64, int64, float64, complex648
          array由其元素類型決定
          struct由其字段類型決定, 最小為1
          其他類型8

          零字節(jié)類型的對(duì)齊

          我們都知道 struct{} 類型占用的字節(jié)數(shù)是 0,但其實(shí)它的內(nèi)存對(duì)齊數(shù)是 1,這么設(shè)定的原因?yàn)榱吮WC當(dāng)它作為結(jié)構(gòu)體的末尾字段時(shí),不會(huì)訪問到其他數(shù)據(jù)結(jié)構(gòu)的地址。比如像下面這個(gè)結(jié)構(gòu)體 ST2

          type ST2 struct {
           A uint32
           B uint64
           C struct{}
          }

          雖然字段 C 占用的字節(jié)數(shù)為0,但是編譯器會(huì)為它補(bǔ) 8 個(gè)字節(jié),這樣就能保證訪問字段 C 的時(shí)候不會(huì)訪問到其他數(shù)據(jù)結(jié)構(gòu)的內(nèi)存地址。

          type ST2 struct {
           A uint32
           B uint64
           C struct{}
          }

          func main() {
           fmt.Println("ST2.C 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST2{}.C)))
           fmt.Println("ST2.C 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST2{}.C)))
           fmt.Println("ST2 結(jié)構(gòu)體占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST2{})))
          }

          ## 輸出

          ST2.C 占用的字節(jié)數(shù)是:0
          ST2.C 對(duì)齊的字節(jié)數(shù)是:1
          ST2 結(jié)構(gòu)體占用的字節(jié)數(shù)是:24

          當(dāng)然因?yàn)?C 前一個(gè)字段 B 占據(jù)了整個(gè)字長,如果把 A 和 B 的順序調(diào)換一下,因?yàn)?A 只占 4 個(gè)字節(jié),C 的對(duì)齊字節(jié)數(shù)是 1, 足夠排在這個(gè)字剩余的字節(jié)里。這樣一來 ST2 結(jié)構(gòu)體的占用空間就能減少到 16 個(gè)字節(jié)。

          type ST2 struct {
           B uint64
           A uint32
           C struct{}
          }

          func main() {
           fmt.Println("ST2.C 占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST2{}.C)))
           fmt.Println("ST2.C 對(duì)齊的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Alignof(ST2{}.C)))
           fmt.Println("ST2 結(jié)構(gòu)體占用的字節(jié)數(shù)是:" + fmt.Sprint(unsafe.Sizeof(ST2{})))
          }

          ## 輸出
          ST2.C 占用的字節(jié)數(shù)是:0
          ST2.C 對(duì)齊的字節(jié)數(shù)是:1
          ST2 結(jié)構(gòu)體占用的字節(jié)數(shù)是:16

          總結(jié)

          內(nèi)存對(duì)齊在我理解就是為了計(jì)算機(jī)訪問數(shù)據(jù)的效率,對(duì)于像結(jié)構(gòu)體、數(shù)組等這樣的占用連續(xù)內(nèi)存空間的復(fù)合數(shù)據(jù)結(jié)構(gòu)來說:

          • 數(shù)據(jù)結(jié)構(gòu)占用的字節(jié)數(shù)是對(duì)齊字節(jié)數(shù)的整數(shù)倍。
          • 數(shù)據(jù)結(jié)構(gòu)的邊界地址能夠整除整個(gè)數(shù)據(jù)結(jié)構(gòu)的對(duì)齊字節(jié)數(shù)。

          這樣 CPU 既減少了對(duì)內(nèi)存的讀取次數(shù),也不需要再對(duì)讀取到的數(shù)據(jù)進(jìn)行篩選和拼接,是一種典型的以空間換時(shí)間的方法。

          希望通過這篇文章能讓你更了解 Go 語言也更了解內(nèi)存對(duì)齊這個(gè)計(jì)算機(jī)操作系統(tǒng)減少內(nèi)存訪問頻率的機(jī)制。

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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  黄色片黄色片一级片不卡片 | 日韩一级免费看 | 成人黄网站在线观看 | 操碰欧美 | 大香蕉伊人免费 |