github.com/haraldrudell/parl@v0.4.176/pslices/slice-away-append.go (about)

     1  /*
     2  © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package pslices
     7  
     8  import (
     9  	"unsafe"
    10  )
    11  
    12  // [SliceAwayAppend] [SliceAwayAppend1] do not zero-out obsolete slice elements
    13  //   - [SetLength] noZero
    14  const NoZeroOut = true
    15  
    16  // [SliceAwayAppend] [SliceAwayAppend1] do zero-out obsolete slice elements
    17  //   - [SetLength] noZero
    18  const DoZeroOut = false
    19  
    20  // SliceAwayAppend avoids allocations when a slice is
    21  // sliced away from the beginning while being appended to at the end
    22  //   - sliceAway: the slice of active values, sliced away and appended to
    23  //   - slice0: the original sliceAway
    24  //   - values: values that should be appended to sliceAway
    25  //   - noZeroOut NoZeroOut: do not set unused element to zero-value.
    26  //     Slices retaining values containing pointers in unused elements
    27  //     is a temporary memory leak. Zero-out prevents this memory leak
    28  //   - by storing the initial slice along with the slice-away slice,
    29  //     the initial slice can be retrieved which may avoid allocations
    30  //   - SliceAwayAppend takes pointer to slice so it can
    31  //     update slicedAway and slice0
    32  //   - There are three outcomes for a slice-away append:
    33  //   - — 1 realloc: the result is larger than the underlying array
    34  //   - — 2 append: appending fits slicedAway capacity
    35  //   - — 3 copy: appending to SlicedAway fits the underlying array but
    36  //     not slicedAway capacity
    37  func SliceAwayAppend[T any](slicedAway, slice0 *[]T, values []T, noZeroOut ...bool) {
    38  	// awaySlice is slicedAway prior to append
    39  	var awaySlice = *slicedAway
    40  	// sliceZero is slice0 pointer and capacity from make
    41  	var sliceZero = *slice0
    42  
    43  	// useRegularAppend indicates that a normal append
    44  	// should be used
    45  	//	- true for case 1 realloc and case 2 append
    46  	//	- false for case 3 copy
    47  	var valueLength = len(awaySlice) + len(values)
    48  
    49  	// 2 append: appending fits slicedAway capacity
    50  	if valueLength <= cap(awaySlice) {
    51  		*slicedAway = append(awaySlice, values...)
    52  		return
    53  	}
    54  
    55  	// 1 realloc: the result is larger than the underlying array
    56  	if valueLength > cap(sliceZero) {
    57  		awaySlice = append(awaySlice, values...)
    58  		// update sliceAway and slice0
    59  		*slicedAway = awaySlice
    60  		*slice0 = awaySlice
    61  		return
    62  	}
    63  	// 3 copy: appending to SlicedAway fits the underlying array but
    64  	// not slicedAway capacity
    65  	//	- slicedAway values need to be copied to
    66  	//		the beginning of sliceZero
    67  
    68  	// re-use slice0
    69  	//	- cannot arbitrary set length to do copy
    70  	//	- therefore, set length to zero and double-append
    71  	if len(sliceZero) > 0 {
    72  		sliceZero = sliceZero[:0]
    73  	}
    74  	// newSlice is at beginning of array and has the new aggregate length
    75  	//	- appends do not cause allocation
    76  	var newSlice = append(append(sliceZero, awaySlice...), values...)
    77  	*slicedAway = newSlice
    78  
    79  	// zero-out if not disabled
    80  	if len(noZeroOut) > 0 && noZeroOut[0] {
    81  		return // no zero-out
    82  	}
    83  	zeroOut(newSlice, awaySlice)
    84  }
    85  
    86  // SliceAwayAppend1 avoids allocations when a slice is
    87  // sliced away from the beginning and appended to at the end
    88  //   - sliceAway: the slice of active values, sliced away and appended to
    89  //   - slice0: the original sliceAway
    90  //   - value: the value that should be appended to sliceAway
    91  //   - by storing the initial slice along with the slice-away slice,
    92  //     the initial slice can be retrieved which may avoid allocations
    93  //   - SliceAwayAppend avoid such allocations based on two pointers to slice
    94  func SliceAwayAppend1[T any](slicedAway, slice0 *[]T, value T, noZeroOut ...bool) {
    95  	// awaySlice is slicedAway prior to append
    96  	var awaySlice = *slicedAway
    97  	// sliceZero is slice0 length and capacity prior to append
    98  	var sliceZero = *slice0
    99  
   100  	// useRegularAppend indicates that a normal append
   101  	// should be used
   102  	//	- true for case 1 realloc and case 2 append
   103  	//	- false for case 3 copy
   104  	var valueLength = len(awaySlice) + 1
   105  
   106  	// 2 append: appending fits slicedAway capacity
   107  	if valueLength <= cap(awaySlice) {
   108  		*slicedAway = append(awaySlice, value)
   109  		return
   110  	}
   111  
   112  	// 1 realloc: the result is larger than the underlying array
   113  	if valueLength > cap(sliceZero) {
   114  		awaySlice = append(awaySlice, value)
   115  		// update sliceAway and slice0
   116  		*slicedAway = awaySlice
   117  		*slice0 = awaySlice
   118  		return
   119  	}
   120  	// 3 copy: appending to SlicedAway fits the underlying array but
   121  	// not slicedAway capacity
   122  	//	- slicedAway values need to be copied to
   123  	//		the beginning of sliceZero
   124  
   125  	// re-use slice0
   126  	//	- cannot arbitrary set length to do copy
   127  	//	- therefore, set length to zero and double-append
   128  	if len(sliceZero) > 0 {
   129  		sliceZero = sliceZero[:0]
   130  	}
   131  	// newSlice is at beginning of array and has the new aggregate length
   132  	//	- appends do not cause allocation
   133  	var newSlice = append(append(sliceZero, awaySlice...), value)
   134  	*slicedAway = newSlice
   135  
   136  	// is zero-out disabled?
   137  	if len(noZeroOut) > 0 && noZeroOut[0] {
   138  		return // no zero-out
   139  	}
   140  	zeroOut(newSlice, awaySlice)
   141  }
   142  
   143  // zero-out emptied values
   144  //   - newSlice begins at the beginning of the underlying array and
   145  //     contains all the newly appended aggregate data
   146  //   - slicedAway is the previously slicedAway slice containing
   147  //     data from before the append
   148  //   - elements to zero out are those at end of slicedAway that
   149  //     are not part of newSlice
   150  func zeroOut[T any](newSlice, slicedAway []T) {
   151  
   152  	// offset his how many elements slicedAway is off from newSlice
   153  	var offset, isValid = Offset(newSlice, slicedAway)
   154  	if !isValid {
   155  		return // some issue with slices
   156  	}
   157  
   158  	// number of elements to zero out at end of slicedAway
   159  	var elementCount int
   160  	// the first index not used in newSlice now
   161  	var newEnd = len(newSlice)
   162  	// the first index not used before in newSlice
   163  	var oldEnd = offset + len(slicedAway)
   164  	// newEnd is greater or equal, there are no element to zero out
   165  	if newEnd >= oldEnd {
   166  		return // no elements to zero-out
   167  	}
   168  	// number of elements to zero out at the end of slicedAway
   169  	elementCount = oldEnd - newEnd
   170  	// limit elementCount to length of slicedAway
   171  	if elementCount > len(slicedAway) {
   172  		elementCount = len(slicedAway)
   173  	}
   174  
   175  	// zero out elementCount elements at the end of slicedAway
   176  	var zeroValue T
   177  	for index := len(slicedAway) - elementCount; index < len(slicedAway); index++ {
   178  		slicedAway[index] = zeroValue
   179  	}
   180  }
   181  
   182  // Offset calculates how many items a slice-away slice is off
   183  // from the initial slice
   184  //   - slice0 is a slice containing the beginning of the underlying array
   185  //   - slicedAway is a slice that has been sliced-off at the beginning
   186  func Offset[T any](slice0, slicedAway []T) (offset int, isValid bool) {
   187  
   188  	// verify that operation is possible
   189  	//	- slice0 slicedAway cannot be nil
   190  	if slice0 == nil || slicedAway == nil {
   191  		return // operation failed
   192  	}
   193  
   194  	// a pointer to a slice points to 3 values:
   195  	//	- pointer to underlying array: pointer to array of elements
   196  	//	- current slice length: int
   197  	//	- current slice capacity: int
   198  	//	- by casting uintptr to uint, pointer arithmetic becomes possible
   199  
   200  	// slice0p is the uint memory address of the underlying array of slice0
   201  	var slice0p = *((*uint)(unsafe.Pointer(&slice0)))
   202  	// slicedAwayp is the uint memory address of the underlying array of slicedAway
   203  	var slicedAwayp = *((*uint)(unsafe.Pointer(&slicedAway)))
   204  
   205  	// // slice0p 0x14000016160
   206  	// fmt.Fprintf(os.Stderr, "slice0p 0x%x\n", slice0p)
   207  	// // slicedAwayp 0x14000016170
   208  	// fmt.Fprintf(os.Stderr, "slicedAwayp 0x%x\n", slicedAwayp)
   209  
   210  	// slicedAway[0]: 3
   211  	// fmt.Fprintf(os.Stderr, "slicedAway[0]: %d\n",
   212  	// 	**(**int)(unsafe.Pointer(&slicedAway)),
   213  	// )
   214  
   215  	// ensure slicedAway is from the same underlying array as slice0
   216  	if slicedAwayp < slice0p {
   217  		return // not same array
   218  	}
   219  
   220  	// determine element size in bytes
   221  	var a [2]T
   222  	var elementSize = 0 + //
   223  		(uint)((uintptr)(unsafe.Pointer(&a[1]))) -
   224  		(uint)((uintptr)(unsafe.Pointer(&a[0])))
   225  
   226  		// int 64-bit elementSize 0x8
   227  	//fmt.Fprintf(os.Stderr, "int 64-bit elementSize 0x%x\n", elementSize)
   228  
   229  	// offset is how many elements have been sliced off from slice0
   230  	//	- not negative
   231  	offset = int((slicedAwayp - slice0p) / elementSize)
   232  	isValid =
   233  		// slicedAwayp - slice0p must be even divisible by element size
   234  		slice0p+uint(offset)*elementSize == slicedAwayp &&
   235  			// slicedAway and offset cannot beyond the end of slice0
   236  			offset+len(slicedAway) <= cap(slice0)
   237  
   238  	return
   239  }