github.com/haraldrudell/parl@v0.4.176/pslices/shifter.go (about)

     1  /*
     2  © 2023–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  	"github.com/haraldrudell/parl/perrors"
    12  	"github.com/haraldrudell/parl/plog"
    13  )
    14  
    15  const (
    16  	//   - [ZeroFillingShifter] provided as argument to [NewShifter] causes
    17  	//     freed elements to be set to the T zero-value.
    18  	//     This prevents temporary memory leaks when:
    19  	//   - T contains pointers and
    20  	//   - previously used T elements remain in the slice’s underlying array
    21  	ZeroFillingShifter = true
    22  )
    23  
    24  // Shifter implements append filtered by a capacity check
    25  //   - this avoids unnecessary slice re-allocations for when
    26  //     a slice is used as a buffer:
    27  //   - — items are sliced off at the beginning and
    28  //   - — new items appended at the end
    29  type Shifter[T any] struct {
    30  	// Slice contains current elements
    31  	Slice []T
    32  	// initialSlice is the slice provided or from last re-allocating append
    33  	initialSlice []T
    34  	// if zeroFill [ZeroFillingShifter], slice element size in bytes
    35  	elementSize int
    36  }
    37  
    38  // NewShifter returns a slice container providing an Append that filters
    39  // unnecessary allocations
    40  //   - if zeroFill is [ZeroFillingShifter] freed elements are set to zero-value
    41  //   - — this prevents temporary memory leaks when:
    42  //   - — elements contains pointers and
    43  //   - — previously used elements remain in the slice’s underlying array
    44  func NewShifter[T any](slice []T, zeroFill ...bool) (shifter *Shifter[T]) {
    45  	var elementSize int
    46  	if len(zeroFill) > 0 && zeroFill[0] {
    47  		var s []T
    48  		if cap(slice) >= 2 {
    49  			s = slice[0:2]
    50  		} else {
    51  			s = make([]T, 2)
    52  		}
    53  		elementSize = //
    54  			int(uintptr(unsafe.Pointer(&s[1]))) -
    55  				int(uintptr(unsafe.Pointer(&s[0])))
    56  	}
    57  	return &Shifter[T]{Slice: slice, initialSlice: slice, elementSize: elementSize}
    58  }
    59  
    60  // Append avoids unnecessary allocations if capacity is sufficient
    61  func (s *Shifter[T]) Append(items ...T) (slice []T) {
    62  	if len(items) == 0 {
    63  		return s.Slice // noop return
    64  	}
    65  
    66  	var requiredCapacity = len(s.Slice) + len(items)
    67  	if requiredCapacity > cap(s.initialSlice) {
    68  
    69  		// if insufficient capacity, use regular append
    70  		s.Slice = append(s.Slice, items...)
    71  	} else {
    72  
    73  		// re-use initialSlice
    74  		var xSlice = s.Slice
    75  		s.Slice = append(append(s.initialSlice[:0], s.Slice...), items...)
    76  
    77  		// do zero fill
    78  		//	- when xSlice was more off from initialSlice than len(items)
    79  		if s.elementSize > 0 && len(xSlice) > 0 {
    80  			s.zeroFill(s.Slice, xSlice)
    81  		}
    82  	}
    83  	s.initialSlice = s.Slice
    84  	slice = s.Slice
    85  
    86  	return
    87  }
    88  
    89  // zeroFill fills unused elements of now with zero-value
    90  //   - now and before are non-empty slices
    91  //   - before is expected to be a slice-result of now
    92  //   - the task is to possibly write zero-values to last elements of before
    93  func (s *Shifter[T]) zeroFill(now, before []T) {
    94  	plog.D("now len %d before len %d", len(now), len(before))
    95  	// first element of before as index of now
    96  	var beforeIndex int
    97  	var beforep = int(uintptr(unsafe.Pointer(&before[0])))
    98  	var nowp = int(uintptr(unsafe.Pointer(&now[0])))
    99  	beforeIndex = (beforep - nowp) / s.elementSize
   100  
   101  	// check beforeIndex
   102  	var capacity = cap(now)
   103  	plog.D("beforeIndex: %d cap %d", beforeIndex, capacity)
   104  	if beforeIndex < 0 || beforeIndex >= capacity {
   105  		panic(s.zeroFillError(now, before, capacity, beforeIndex))
   106  	}
   107  
   108  	// zero-fill end of before
   109  	var firstUnused = len(now) - beforeIndex
   110  	plog.D("firstUnused: %d", firstUnused)
   111  	var t T
   112  	for firstUnused < len(before) {
   113  		before[firstUnused] = t
   114  		firstUnused++
   115  	}
   116  }
   117  
   118  // zeroFillError returns an actionable error for a software deficiency
   119  func (s *Shifter[T]) zeroFillError(now, before []T, capacity, beforeIndex int) (err error) {
   120  	var bSign string
   121  	var b = beforeIndex
   122  	if b < 0 {
   123  		bSign = "-"
   124  		b = -beforeIndex
   125  	}
   126  	err = perrors.ErrorfPF("bad zeroFill: now: len %d cap %d before: len %d cap %d index: %s0x%x",
   127  		len(now), capacity,
   128  		len(before), cap(before),
   129  		bSign, b,
   130  	)
   131  	return
   132  }