Golang slice and array source code

Foreword

There are many common data structures in golang, such as array, slice, map, etc. among them, the most commonly used are array, slice and map. Theoretically, array and slice are a kind of data structure, both of which are sequential structures. However, due to the fixed length characteristics of array, In some cases, the use of dynamic length is very unfriendly. At this time, slice needs to be used to replace the fixed length array.

What is Slice

The official explanation is as follows:

​   Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimension such as transformation matrices, most array programming in Go is done with slices rather than simple arrays.

The general meaning is as follows:

  Slice It's a packaged one array,It can provide a more general, powerful and convenient interface for data sequence. In addition to terms with explicit dimensions, such as transformation matrices, Go Most array programming in is done using slices rather than simple arrays.

 

I think slice is a bit like the vector in the c + + standard library, but the underlying implementation method may be a little different (I don't know much about c + +, if someone knows the underlying implementation of vector, I can solve my doubts). Slice is a structure that wraps the go array, but this structure can only be seen at other levels such as compilation, In the process of using it, we only need to define it as an array, which can be converted into slice structure during compilation. Next, let me analyze the relevant structure source code and operation source code of slice.

Code parsing

slice structure

 1 // slice Structure, which will be built during compilation
 2 // If you want to use it during operation, you can use its corresponding reflect structural morphology
 3 // Namely reflect.SliceHeader
 4 type slice struct {
 5     // A pointer to the underlying array
 6     array unsafe.Pointer
 7     // slice Current number of elements, i.e len()Number returned when
 8     len   int
 9     // slice Capacity i.e cap()Number returned when
10     cap   int
11 }

slice initialization

slice can be initialized in three ways:

  1. Subscript initialization method: a:=slice [:] / / this slice is a slice created by others. This creation method is closest to the underlying creation method. During compilation, the statement will be converted to the OpSliceMake operation of the compiler, which will call the SliceMake operation. The SliceMake operation will accept four parameters to create a new slice, including element type, array pointer, slice size and capacity, which is the same as the field composition of the slice structure shown in the previous chapter, In addition, the subscript initialization method does not copy the data in the original array, but directly refers to the pointer to the original array, which will affect the slice of the original slice when modifying slice a.
  2. Literal initialization method: ` ` ` a:=[]int{1,2,3} `, the process is as follows
  • When this method is created during compilation, it will first generate an array with a length of 3 (the length is automatically inferred according to the literal quantity)
  • Assign values to the elements of this array
  • Then a new length of 3 will be displayed (the length depends on the literal quantity)
  • Pointer to the array of (auto inferred)
  • Assign this pointer to slice according to the most basic subscript initialization method
1 var arr [3]int
2 arr[0] = 1
3 arr[1] = 2
4 arr[2] = 3
5 var arrp *[3]int = new([3]int)
6 *arrp = arr
7 slice := arrp[:]

  3.   Create slice with the keyword make([]int,3,3): initialize the slice with the keyword make. First, verify the len and cap passed in during the compilation phase to verify whether they are negative and whether len > cap. Determine whether the slice will be initialized on the heap by judging the size of the slice and whether it escapes. If the slice is small enough and there is no escape, An array of cap values will be created, and then initialized as literal initialization. If cap is 0, an array of corresponding length will be created according to the value of len. If escape occurs or the slice is too large, the runtime.makeslice() function will be called to allocate the slice memory on the heap. The runtime.makeslice code is as follows:

 1 // This function passes in the type, length and capacity of the slice to be initialized, and the returned pointer will be completed through the caller group slice structural morphology
 2 func makeslice(et *_type, len, cap int) unsafe.Pointer {
 3   // Judge whether the product of type and capacity will exceed the size of allocable memory, and whether the length is 0 and the capacity is less than the length
 4     mem, overflow := math.MulUintptr(et.size, uintptr(cap))
 5     if overflow || mem > maxAlloc || len < 0 || len > cap {
 6         mem, overflow := math.MulUintptr(et.size, uintptr(len))
 7         if overflow || mem > maxAlloc || len < 0 {
 8             panicmakeslicelen()
 9         }
10         panicmakeslicecap()
11     }
12     // If all are normal, call this function to request to return the element size in a continuous slice×Pointer to the memory space of the slice capacity length
13     return mallocgc(mem, et, true)
14 }

Access element

Slice access elements are accessed through the slice structure

Element address of corresponding index=Pointer to the underlying array+Number of bytes occupied by the corresponding element∗Indexes

The compiler will return the corresponding value through the element address of the corresponding index, that is, direct address access

Addition and expansion

A major advantage of slice over array is that it can be dynamically expanded according to the usage to adapt to the data added at any time. When adding, it calls the append function to append the tail of slice. If the cap value of slice is less than the current len plus the number of incoming values in append, the expansion operation will start, The append function has no explicit function body, but is converted during compilation. When append finds that it needs capacity expansion, it will call the runtime.growslice method. The source code of this method is as follows (to remove some useless code):

 1 func growslice(et *_type, old slice, cap int) slice {
 2     // If the required capacity is less than the required capacity, an error is reported
 3   // Theoretically, this problem should not arise
 4     if cap < old.cap {
 5         panic(errorString("growslice: cap out of range"))
 6     }
 7     // append Can't create one nil Pointer but len Slice not 0
 8     if et.size == 0 {
 9         return slice{unsafe.Pointer(&zerobase), old.len, cap}
10     }
11     
12     newcap := old.cap
13     doublecap := newcap + newcap
14   // If the demand capacity is greater than double the old capacity, use the demand capacity directly
15     if cap > doublecap {
16         newcap = cap
17     } else {
18     // If current len If it is less than 1024, the capacity will be doubled directly; otherwise, it will be 1.25 Multiply until the required capacity is met
19         if old.len < 1024 {
20             newcap = doublecap
21         } else {
22             for 0 < newcap && newcap < cap {
23                 newcap += newcap / 4
24             }
25             if newcap <= 0 {
26                 newcap = cap
27             }
28         }
29     }
30 
31     var overflow bool
32     var lenmem, newlenmem, capmem uintptr
33 // During capacity expansion, you cannot simply len To determine the memory length required for capacity expansion
34 // Memory alignment is also performed according to the element type of the slice
35 // Memory alignment occurs when the number of bytes occupied by an element is a multiple of 1, 8, or 2
36 // The memory alignment strategy is rounded up
37 // When rounding the target go 67 in memory allocation policy class Round the size in the page
38     switch {
39     case et.size == 1:
40         lenmem = uintptr(old.len)
41         newlenmem = uintptr(cap)
42         capmem = roundupsize(uintptr(newcap))
43         overflow = uintptr(newcap) > maxAlloc
44         newcap = int(capmem)
45     case et.size == sys.PtrSize:
46         lenmem = uintptr(old.len) * sys.PtrSize
47         newlenmem = uintptr(cap) * sys.PtrSize
48         capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
49         overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
50         newcap = int(capmem / sys.PtrSize)
51     case isPowerOfTwo(et.size):
52         var shift uintptr
53         if sys.PtrSize == 8 {
54             // Mask shift for better code generation.
55             shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
56         } else {
57             shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
58         }
59         lenmem = uintptr(old.len) << shift
60         newlenmem = uintptr(cap) << shift
61         capmem = roundupsize(uintptr(newcap) << shift)
62         overflow = uintptr(newcap) > (maxAlloc >> shift)
63         newcap = int(capmem >> shift)
64     default:
65         lenmem = uintptr(old.len) * et.size
66         newlenmem = uintptr(cap) * et.size
67         capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
68         capmem = roundupsize(capmem)
69         newcap = int(capmem / et.size)
70     }
71 
72 // If the required memory exceeds the maximum allocable memory, the panic
73     if overflow || capmem > maxAlloc {
74         panic(errorString("growslice: cap out of range"))
75     }
76 
77     var p unsafe.Pointer
78   // If the current element type is not a pointer, the position beyond the current length of the slice is cleared
79   // At last, the contents of the original array memory are copied to the newly applied memory.
80     if et.ptrdata == 0 {
81         p = mallocgc(capmem, nil, false)
82         memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
83     } else {
84     // If it is a pointer, it will be processed according to gc Aspect protects it from null pointers being deleted during allocation gc recovery
85         p = mallocgc(capmem, et, true)
86         if lenmem > 0 && writeBarrier.enabled {
87             bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
88         }
89     }
90     memmove(p, old.array, lenmem)
91     //This function eventually returns a new slice
92     return slice{p, old.len, newcap}
93 }

Copy of slice

Slice copying is also an interface provided for slices. You can copy the values in src slices to dst slices by calling the copy() function. After slice copying through this function, the operations on dst slices will not have any impact on src. The copy length is calculated according to the minimum len length in src and dst slices. The source code of runtime.slicecopy is as follows:

func slicecopy(toPtr unsafe.Pointer, toLen int, fmPtr unsafe.Pointer, fmLen int, width uintptr) int {
    if fmLen == 0 || toLen == 0 {
        return 0
    }

    n := fmLen
    if toLen < n {
        n = toLen
    }

    if width == 0 {
        return n
    }
    
    size := uintptr(n) * width
    if size == 1 {  
    // If only one element is directly assigned
        *(*byte)(toPtr) = *(*byte)(fmPtr)
    } else {
    // Directly copy the memory, if slice Too much data will affect performance
        memmove(toPtr, fmPtr, size)
    }
    return n
}

 

Posted by Gummie on Sun, 31 Oct 2021 13:59:22 -0700