Понять базовый дизайн срезов в Golang.

Go

Структура среза

Слайс — это массив в golang, который использует указатель для указания на непрерывный сегмент, поэтому, по сути, это ссылочный тип. ОдинsliceЗанимает 24 байта в golang

a = make([]int, 0)
unsafe.Sizeof(a)	// 24

var c int
unsafe.Sizeof(c)	// 8, 一个 int 在 golang 中占用 8 个bytes(本机是64位操作系统)

В slice.go среды выполнения определена структура slice.

type slice struct {
    array unsafe.Pointer	// 8 bytes
    len   int				// 8 bytes
    cap   int				// 8 bytes
    // 确认了,slice 的大小 24
}
  • массив - это указатель, указывающий на фактический массив
  • len — количество элементов в срезе
  • cap относится к выделенному в данный момент пространству

готов к отладке

Просто подготовьте программу, чтобы увидеть, как golang инициализирует слайс.

package main

import "fmt"

func main() {
    a := make([]int, 0)
    a = append(a, 2, 3, 4)
    fmt.Println(a)
}

Инициализация среза

использоватьdlvОтладка после разборки:

(dlv) disassemble
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
main.go:5       0x10b70f0       65488b0c2530000000              mov rcx, qword ptr gs:[0x30]
main.go:5       0x10b70f9       488d4424e8                      lea rax, ptr [rsp-0x18]
main.go:5       0x10b70fe       483b4110                        cmp rax, qword ptr [rcx+0x10]
main.go:5       0x10b7102       0f8637010000                    jbe 0x10b723f      main.go:5       0x10b7108*      4881ec98000000                  sub rsp, 0x98
main.go:5       0x10b710f       4889ac2490000000                mov qword ptr [rsp+0x90], rbp
main.go:5       0x10b7117       488dac2490000000                lea rbp, ptr [rsp+0x90]
main.go:6       0x10b711f       488d051a0e0100                  lea rax, ptr [rip+0x10e1a]
main.go:6       0x10b7126       48890424                        mov qword ptr [rsp], rax
main.go:6       0x10b712a       0f57c0                          xorps xmm0, xmm0
main.go:6       0x10b712d       0f11442408                      movups xmmword ptr [rsp+0x8], xmm0
main.go:6       0x10b7132       e8b99af8ff                      ** call $runtime.makeslice **
main.go:6       0x10b7137       488b442418                      mov rax, qword ptr [rsp+0x18]
main.go:6       0x10b713c       4889442460                      mov qword ptr [rsp+0x60], rax
main.go:6       0x10b7141       0f57c0                          xorps xmm0, xmm0
main.go:6       0x10b7144       0f11442468                      movups xmmword ptr [rsp+0x68], xmm0
...

В куче инструкций см.call $runtime.makesliceВызов должен быть для инициализации среза

func makeslice(et *_type, len, cap int) unsafe.Pointer {
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		// NOTE: Produce a 'len out of range' error instead of a
		// 'cap out of range' error when someone does make([]T, bignumber).
		// 'cap out of range' is true too, but since the cap is only being
		// supplied implicitly, saying len is clearer.
		// See golang.org/issue/4085.
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}

	return mallocgc(mem, et, true)
}

makelice, наконец, возвращает адрес памяти поля массива, где хранится реальное значение, в функцииuintptr()Что тогда?

println(uintptr(0), ^uintptr(0))
// 0	18446744073709551615	为什么按位异或后是这个数?

var c int = 1
println(^c, ^uint64(0))
// -2	18446744073709551615

Из этих строк проверки кода, со знаком 1, двоичным: 0001, XOR: 1110, старший бит 1 является отрицательным числом, указывающим -2;
Двоичный код uint64: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
После XOR: 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 Потому что беззнаковый, преобразованный в десятичный, равен 2^64 - 1 = 18446744073709551615 . Таким образом, фактически ^uintptr(0) относится к максимальному значению текущей машины (32-разрядная, uint32; 64-разрядная, uint64). мы можем распечатать текущийa

(dlv) p a
[]int len: 1, cap: 0, [0]

Расширение среза

=>      main.go:7       0x10b7149       eb00                            jmp 0x10b714b
        main.go:7       0x10b714b       488d0dee0d0100                  lea rcx, ptr [rip+0x10dee]
        main.go:7       0x10b7152       48890c24                        mov qword ptr [rsp], rcx
        main.go:7       0x10b7156       4889442408                      mov qword ptr [rsp+0x8], rax
        main.go:7       0x10b715b       0f57c0                          xorps xmm0, xmm0
        main.go:7       0x10b715e       0f11442410                      movups xmmword ptr [rsp+0x10], xmm0
        main.go:7       0x10b7163       48c744242003000000              mov qword ptr [rsp+0x20], 0x3
        main.go:7       0x10b716c       e84f9bf8ff                      call $runtime.growslice
        main.go:7       0x10b7171       488b442428                      mov rax, qword ptr [rsp+0x28]
        main.go:7       0x10b7176       488b4c2430                      mov rcx, qword ptr [rsp+0x30]
        main.go:7       0x10b717b       488b542438                      mov rdx, qword ptr [rsp+0x38]
        main.go:7       0x10b7180       4883c103                        add rcx, 0x3
        main.go:7       0x10b7184       eb00                            jmp 0x10b7186
        main.go:7       0x10b7186       48c70002000000                  mov qword ptr [rax], 0x2
        main.go:7       0x10b718d       48c7400803000000                mov qword ptr [rax+0x8], 0x3
        main.go:7       0x10b7195       48c7401004000000                mov qword ptr [rax+0x10], 0x4
        main.go:7       0x10b719d       4889442460                      mov qword ptr [rsp+0x60], rax
        main.go:7       0x10b71a2       48894c2468                      mov qword ptr [rsp+0x68], rcx
        main.go:7       0x10b71a7       4889542470                      mov qword 
		...

При добавлении к срезу он фактически вызываетcall runtime.growslice, чтобы увидеть, что сделано:

func growslice(et *_type, old slice, cap int) slice {
	if cap < old.cap {
		panic(errorString("growslice: cap out of range"))
	}

	if et.size == 0 {
		// append should not create a slice with nil pointer but non-zero len.
		// We assume that append doesn't need to preserve old.array in this case.
		return slice{unsafe.Pointer(&zerobase), old.len, cap}
	}

	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// Specialize for common values of et.size.
	// For 1 we don't need any division/multiplication.
	// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
	// For powers of 2, use a variable shift.
	switch {
	case et.size == 1:
		lenmem = uintptr(old.len)
		newlenmem = uintptr(cap)
		capmem = roundupsize(uintptr(newcap))
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	case et.size == sys.PtrSize:
		lenmem = uintptr(old.len) * sys.PtrSize
		newlenmem = uintptr(cap) * sys.PtrSize
		capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
		overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
		newcap = int(capmem / sys.PtrSize)
	case isPowerOfTwo(et.size):
		var shift uintptr
		if sys.PtrSize == 8 {
			// Mask shift for better code generation.
			shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
		} else {
			shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
		}
		lenmem = uintptr(old.len) << shift
		newlenmem = uintptr(cap) << shift
		capmem = roundupsize(uintptr(newcap) << shift)
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		newcap = int(capmem >> shift)
	default:
		lenmem = uintptr(old.len) * et.size
		newlenmem = uintptr(cap) * et.size
		capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
		capmem = roundupsize(capmem)
		newcap = int(capmem / et.size)
	}

	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: cap out of range"))
	}

	var p unsafe.Pointer
	if et.ptrdata == 0 {
		// 申请内存
		p = mallocgc(capmem, nil, false)
		
		// 清除未使用的地址
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
		p = mallocgc(capmem, et, true)
		if lenmem > 0 && writeBarrier.enabled {
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
		}
	}
	// 拷贝大小为 lenmem 个btyes,从old.array到p
	memmove(p, old.array, lenmem)

	return slice{p, old.len, newcap}

Конкретные стратегии расширения:

  • Если запрашиваемая емкость (cap) более чем в 2 раза превышает исходную емкость (old.cap) или исходная емкость
  • В противном случае рассчитайтеnewcap += newcap / 4, знайте, что newcap не меньше емкости для применения, если она переполняется, то newcap = cap (емкость для применения)

После завершения расширения адрес пересчитывается в соответствии с размером t.size, среди которых новый слайсlenдля исходного фрагментаcap(Только если длина среза превышает колпачок, его нужно расширить). Затем применитеcapmemразмер памяти, скопированный из old.arraylenmemбайт (т. е. всю копию исходного слайса, lenmem — вычисленный размер исходного слайса) вp.

a := make([]int, 0)
a = append(a, 1)
println("1 times:", len(a), cap(a))	// 1 times: 1 1

a = append(a, 2, 3)
println("2 times:", len(a), cap(a))	// 2 times: 3 4

a = append(a, 4)
println("3 times:", len(a), cap(a))	// 3 times: 4 4

Как можно видеть:

  1. еслиappendПослеlenбольше, чемcapв 2 раза, то есть расширен до более чемlenпервое кратное 2
  2. еслиappendПослеlenбольше, чемcapи меньше чемcapвдвое большеcapувеличить в 2 раза
  3. еслиappendПослеlenменьше, чемcap, непосредственно добавить

Срез загрязнения

использоватьslice, может вызвать некоторые проблемы неосознанно.

a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
shadow = append(shadow, 100)
fmt.Println(shadow, a)
// [2 3 100] [1 2 3 100 5]

Результат оказался неожиданным, но и закономерным. в структуреarrayэто указатель на массив[1,2,3,4,5]адрес памятиshadowуказывает на[2,3]адрес памяти. в направленииshadowПосле добавления он будет напрямую изменять реальный массив, косвенно затрагивая все срезы, указывающие на массив. Таким образом, вы можете изменить приведенный выше код следующим образом:

a := []int{1, 2, 3, 4, 5}
shadow := append([]int{}, a[1:3]...)
shadow = append(shadow, 100)
fmt.Println(shadow, a)
// [2 3 100] [1 2 3 4 5]

Если возвращаемое значение функции является вышеуказанным случаемreturn a[1:3], также вызовет[1,2,3,4,5]Память, занятая блокировкой, не может быть освобождена.

темная магия

понялsliceсам является указателем на реальный массив, вGolangпредоставлено вunsafeвыполнять манипуляции с указателем.

a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
shadowPtr := uintptr(unsafe.Pointer(&shadow[0]))
offset := unsafe.Sizeof(int(0))
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr - offset)))	// 1
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr + 2*offset)))	// 4

shadowPtrявляется положением первого нижнего индекса a, aint8 байт на 64-битной машине, смещение 1 впередoffset, является 0-м индексом 1 a; смещение 2 назадoffset, является третьим индексом 4 a .

Параллельная безопасность

sliceявляется безопасным типом данных, не связанным с сопрограммой, если вы создаете несколькоgoroutineправильноsliceОдновременное чтение и запись приведет к потере. увидеть кусок кода

package main

import (
	"fmt"
	"sync"
)

func main () {
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            a = append(a, i)
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println(len(a))
}
// 9403 9876 9985 9491 ...

Выполняется несколько раз, результаты получаются каждый раз разные, короче говоря, это не должно быть желаемых 10 000. Чтобы решить эту проблему, рассмотрим задачу в соответствии с программной идеей безопасности сопрограмм, может рассмотреть возможность использованияchannelЕго собственная функция (блокировка) для обеспечения безопасного одновременного чтения и записи.

func main() {
    a := make([]int, 0)
    buffer := make(chan int)
    go func() {
        for v := range buffer {
            a = append(a, v)
        }
    }()

    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            buffer <- i
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println(len(a))
}
// 10000