Go plan9 彙編:記憶體對齊和遞迴

hxia043發表於2024-09-02

原創文章,歡迎轉載,轉載請註明出處,謝謝。


Go plan9 彙編系列文章:

  • Go plan9 彙編: 打通應用到底層的任督二脈
  • Go plan9 彙編:手寫彙編
  • Go plan9 彙編:說透函式棧
  • Go plan9 彙編:記憶體對齊和遞迴

0. 前言

在 Go plan9 彙編系列文章中,介紹了函式和函式棧的呼叫。這裡繼續看記憶體對齊和遞迴呼叫方面的內容。

1. 記憶體對齊

直接上示例:

type temp struct {
	a bool
	b int16
	c []string
}

func main() {
	var t = temp{a: true, b: 1, c: []string{}}
	fmt.Println(unsafe.Sizeof(t))
}

輸出:

32

改寫 temp 結構體成員變數位置:

type temp struct {
	a bool
    c []string
	b int16
}

func main() {
	var t = temp{a: true, b: 1, c: []string{}}
	fmt.Println(unsafe.Sizeof(t))
}

輸出:

40

為什麼移動下結構體成員的位置會對結構體在記憶體中的大小有影響呢?

列印示例中結構體成員變數地址如下:

# 示例 1
func main() {
	var t = temp{a: true, b: 1, c: []string{}}

	fmt.Println(unsafe.Sizeof(t.a), unsafe.Sizeof(t.b), unsafe.Sizeof(t.c))
	fmt.Printf("%p %p %p %p\n", &t, &t.a, &t.b, &t.c)

	fmt.Println(unsafe.Sizeof(t))
}

# go run ex10.go 
1 2 24
0xc0000a4000 0xc0000a4000 0xc0000a4002 0xc0000a4008
32

# 示例 2
func main() {
	var t = temp{a: true, b: 1, c: []string{}}

	fmt.Println(unsafe.Sizeof(t.a), unsafe.Sizeof(t.c), unsafe.Sizeof(t.b))
	fmt.Printf("%p %p %p %p\n", &t, &t.a, &t.c, &t.b)

	fmt.Println(unsafe.Sizeof(t))
}

# go run ex10.go 
1 24 2
0xc00006e090 0xc00006e090 0xc00006e098 0xc00006e0b0
40

可以看到,在為結構體分配記憶體時是要遵循記憶體對齊的,記憶體對齊是為了簡化定址,CPU 可一次找到變數的位置。因為記憶體對齊的存在,這裡示例 2 中雖然變數 a 只佔 1 個位元組,但卻獨佔了 8 個位元組,這對於寫程式碼來說是一種記憶體消耗,應當避免的。

2. 遞迴

我們看一個遞迴的示例:

func main() {
	println(sum(1000))
}

//go:nosplit
func sum(n int) int {
	if n > 0 {
		return n + sum(n-1)
	} else {
		return 0
	}
}

輸出:

# go run ex7.go 
# command-line-arguments
main.sum: nosplit stack over 792 byte limit
main.sum<1>
    grows 24 bytes, calls main.sum<1>
    infinite cycle

這裡我們在 sum 函式前加 //go:nosplit 是要宣告這個函式是不可棧分裂的函式。意味著當函式棧滿的時候,(記憶體分配器)不會為它開闢新的空間。

Go 為 goroutine 分配的初始棧空間大小為 2K,如果 main 棧加上 nosplit 的 sum 棧超過 2K,將導致爆棧。

//go:nosplit 拿掉,重新執行:

func main() {
	println(sum(100000))
}

func sum(n int) int {
	if n > 0 {
		return n + sum(n-1)
	} else {
		return 0
	}
}

輸出:

5000050000

那麼 sum 是否可以無限遞迴呢?我們給 sum 一個大數 10000000000000,接著重新執行:

runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc0200f8398 stack=[0xc0200f8000, 0xc0400f8000]
fatal error: stack overflow

輸出 stack overflow,main 協程的棧是從 0xc0200f8000 到 0xc0400f8000,這裡遞迴所用的棧超過了 goroutine 棧的最大限制 1000000000-byte(超過的意思是 main 棧加上 sum 遞迴呼叫的棧超過了最大限制),也就是 1G。


相關文章