This paper studies the creation, expansion and implementation of deep copy of golang slice from the perspective of source code.
Internal data structure
slice has only three fields, among which array is the part to save data, len field is the length and cap is the capacity.
type slice struct { array unsafe.Pointer // Data section len int // length cap int // capacity }
The size of the empty slice can be output through the following code:
package main import "fmt" import "unsafe" func main() { data := make([]int, 0, 3) // 24 len:8, cap:8, array:8 fmt.Println(unsafe.Sizeof(data)) // We can get the field value of the internal structure of the array by pointer ptr := unsafe.Pointer(&data) opt := (*[3]int)(ptr) // addr, 0, 3 fmt.Println(opt[0], opt[1], opt[2]) data = append(data, 123) fmt.Println(unsafe.Sizeof(data)) shallowCopy := data[:1] ptr1 := unsafe.Pointer(&shallowCopy) opt1 := (*[3]int)(ptr1) fmt.Println(opt1[0]) }
Establish
To create a slice is to allocate memory. Cap and Len are set in assembly.
The following code is mainly used to judge the capacity and allocate the memory.
func makeslice(et *_type, len, cap int) unsafe.Pointer { // Get the memory size to apply mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() } // Allocate memory // Small objects are allocated from the current P's cache idle data // Large objects (size > 32KB) are allocated directly from the heap // runtime/malloc.go return mallocgc(mem, et, true) }
append
For slice without memory expansion, data can be copied directly.
The above DX stores the array pointer, AX is the offset of the data, and 123 is stored in the array.
In case of insufficient capacity, it is necessary to expand slice. This is what slice is concerned about. (because for large s lice, grow slice will affect the efficiency of memory allocation and execution)
func growslice(et *_type, old slice, cap int) slice { // Static analysis, memory scanning // ... if cap < old.cap { panic(errorString("growslice: cap out of range")) } // If the storage type space is 0, such as [] struct {}, the data is empty and the length is not empty if et.size == 0 { return slice{unsafe.Pointer(&zerobase), old.len, cap} } newcap := old.cap doublecap := newcap + newcap if cap > doublecap { // If the new capacity is more than twice the original capacity, the new capacity will be applied directly newcap = cap } else { if old.len < 1024 { // If the original length is less than 1024, the new capacity is twice the old capacity newcap = doublecap } else { // Increase by 1 / 4 of the original capacity until the new capacity is met for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Check whether the capacity overflows by verifying that the newcap is greater than 0. if newcap <= 0 { newcap = cap } } } var overflow bool var lenmem, newlenmem, capmem uintptr // In order to speed up the calculation (less division, multiplication) // Select different calculation methods for different slice element sizes // Gets the memory size that needs to be requested. 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): // Multiple of two, calculated by displacement 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: // Other Division 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) } // Determine whether it will overflow if overflow || capmem > maxAlloc { panic(errorString("growslice: cap out of range")) } // memory allocation var p unsafe.Pointer if et.kind&kindNoPointers != 0 { p = mallocgc(capmem, nil, false) // Clear part of memory without data copy memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) } else { // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory. p = mallocgc(capmem, et, true) if writeBarrier.enabled { // gc correlation // Only shade the pointers in old.array since we know the destination slice p // only contains nil pointers because it has been cleared during alloc. bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem) } } // Data copy memmove(p, old.array, lenmem) return slice{p, old.len, newcap} }
Slice copy
Light copy of slice
shallowCopy := data[:1] ptr1 := unsafe.Pointer(&shallowCopy) opt1 := (*[3]int)(ptr1) fmt.Println(opt1[0])
Here is the assembly code of the above code:
Above, first copy the member data of data to the register, and then copy from the register to the object of shallowCopy. (note that it's just a copy of the pointer, so it's a shallow copy)
Deep copy of slice
Deep copy is also relatively simple, just a deep copy of memory.
func slicecopy(to, fm slice, width uintptr) int { if fm.len == 0 || to.len == 0 { return 0 } n := fm.len if to.len < n { n = to.len } // If the element size is 0, it will return directly if width == 0 { return n } // State analysis and memory scanning // ... size := uintptr(n) * width // Direct memory copy if size == 1 { // common case worth about 2x to do here *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer } else { memmove(to.array, fm.array, size) } return n } // Copy of string slice func slicestringcopy(to []byte, fm string) int { if len(fm) == 0 || len(to) == 0 { return 0 } n := len(fm) if len(to) < n { n = len(to) } // State analysis and memory scanning // ... memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n)) return n }
Other
- How to generate assembly
go tool compile -N -S slice.go > slice.S
-
Need to understand the use of unsafe.Pointer
-
Slice.go is at runtime/slice.go
-
The above code uses version go1.12.5
-
There is another point to remind. Objects of type length 0. For example, struct {} type. (therefore, many use channel struct {} for channel transmission, saving memory)
package main import "fmt" import "unsafe" func main() { var data [100000]struct{} var data1 [100000]int // 0 fmt.Println(unsafe.Sizeof(data)) // 800000 fmt.Println(unsafe.Sizeof(data1)) }