Source code interpretation of golang slice

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

  1. How to generate assembly
go tool compile -N -S slice.go > slice.S
  1. Need to understand the use of unsafe.Pointer

  2. Slice.go is at runtime/slice.go

  3. The above code uses version go1.12.5

  4. 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))
}

Keywords: Go less

Added by christine75 on Sun, 26 Apr 2020 12:40:56 +0300