底層知識-slice

由於文章由markdown的方式編寫, 部分鏈接與圖片顯示可能存在問題, 大家可以移步到github源查看, 文章摘錄: https://halfrost.com/go_slice/

ps. 測試環境:

Linux * 4.15.0-66-generic #75-Ubuntu SMP Tue Oct 1 05:24:09 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

ubuntu18.04

go version: 1.12.4

cpu: Intel(R) Core(TM) i5-8500 CPU @ 3.00GHz

memory: 16GB
  1. 數組爲值類型, 切片爲引用類型

  2. 切片本身並不是動態數組或者數組指針。它內部實現的數據結構通過指針引用底層數組,設定相關屬性將數據讀寫操作限定在指定的區域內。切片本身是一個只讀對象,其工作機制類似數組指針的一種封裝

  3. 切片賦值會在堆上開闢空間, 證據:

package main
import "testing"
func array() [1024]int {
	var x [1024]int
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}
func slice() []int {
	x := make([]int, 1024)
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}
func BenchmarkArray(b *testing.B) {
	for i := 0; i < b.N; i++ {
		array()
	}
}
func BenchmarkSlice(b *testing.B) {
	for i := 0; i < b.N; i++ {
		slice()
	}
}

測試命令, 禁用內聯和優化

go test -bench . -benchmem -gcflags "-N -l"

測試結果

BenchmarkArray-6        1000000              2277 ns/op              0 B/op          0 allocs/op

BenchmarkSlice-6          500000              3191 ns/op            8192 B/op          1 allocs/op

結論

這樣對比看來,並非所有時候都適合用切片代替數組,因爲切片底層數組可能會在堆上分配內存,而且小數組在棧上拷貝的消耗也未必比 make 消耗大

 

切片結構的類型

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

手動創建一個slice結構, 從內存中得到切片的方法:

s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])

fmt.Println(&s[0])
fmt.Println(ptr)

var s1 = struct {
   addr unsafe.Pointer
   len int
   cap int
}{ptr, 200, 200}
s2 := *(*[]byte)(unsafe.Pointer(&s1))
fmt.Println(s2)

也可以更直接的:

sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&s)))

sliceHeader.Cap = 200

sliceHeader.Len = 200

sliceHeader.Data = uintptr(ptr)

nil切片

nil 切片被用在很多標準庫和內置函數中,描述一個不存在的切片的時候,就需要用到 nil 切片。比如函數在發生異常的時候,返回的切片就是 nil 切片。nil 切片的指針指向 nil。

空切片

空切片和 nil 切片的區別在於,空切片指向的地址不是nil,指向的是一個內存地址,但是它沒有分配任何內存空間,即底層元素包含0個元素。

最後需要說明的一點是。不管是使用 nil 切片還是空切片,對其調用內置函數 append,len 和 cap 的效果都是一樣的。

slice擴容

Go 中切片擴容的策略是這樣的:

  • 首先判斷,如果新申請容量(cap)大於2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)

  • 否則判斷,如果舊切片的長度小於1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap)

  • 否則判斷,如果舊切片長度大於等於1024,則最終容量(newcap)從舊容量(old.cap)開始循環增加原來的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大於等於新申請的容量(cap),即(newcap >= cap)

  • 如果最終容量(cap)計算值溢出,則最終容量(cap)就是新申請容量(cap)

注意:擴容擴大的容量都是針對原來的容量而言的,而不是針對原來數組的長度而言的

Range的用法

代碼測試:

func main() {
	slice := []int{10, 20, 30, 40}
	for index, value := range slice {
		fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
	}
}

測試結果:

value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章