common/multiSizeSlicePool.go (95 lines of code) (raw):

// Copyright © 2017 Microsoft <wastore@microsoft.com> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package common import ( "math/bits" ) // A pool of byte slices // Like sync.Pool, but strongly-typed to byte slices type ByteSlicePooler interface { RentSlice(desiredLength int64) []byte ReturnSlice(slice []byte) Prune() } // Pools byte slices of a single size. // We are not using sync.Pool because it reserves the right // to ignore is contents and pretend to be empty. That's OK if // there are enough GCs to cause it to be flushed/emptied. // But it didn't seem to be getting emptied for us (presumably because // we didn't have many GCs... because we were pooling resources!) // And so we would get 150 or so objects just sitting there in the pool, // and if each of those is for a 100 MB "max size" storage block, that gets bad. // Discussion at the following URL confirms the problematic nature and that "roll your own" // can be better for low-contention cases - which is what we believe ours to be: // https://github.com/golang/go/issues/22950 type simpleSlicePool struct { c chan []byte } func newSimpleSlicePool(maxCapacity int) *simpleSlicePool { return &simpleSlicePool{ c: make(chan []byte, maxCapacity), } } func (p *simpleSlicePool) Get() []byte { select { case existingItem := <-p.c: return existingItem default: return nil } } func (p *simpleSlicePool) Put(b []byte) { select { case p.c <- b: return default: // just throw b away and let it get GC'd if p.c is full } } // A pool of byte slices, optimized so that it actually has a sub-pool for each // different size (in powers of 2) up to some pre-specified limit. The use of sub-pools // minimized wastage, in cases where the desired slice sizes vary greatly. // (E.g. if only had one pool, holding really big slices, it would be wasteful when // we only need to put put small amounts of data into them). type multiSizeSlicePool struct { // It is safe for multiple readers to read this, once we have populated it // See https://groups.google.com/forum/#!topic/golang-nuts/nL8z96SXcDs poolsBySize []*simpleSlicePool } // Create new slice pool capable of pooling slices up to maxSliceLength in size func NewMultiSizeSlicePool(maxSliceLength int64) ByteSlicePooler { maxSlotIndex, _ := getSlotInfo(maxSliceLength) poolsBySize := make([]*simpleSlicePool, maxSlotIndex+1) for i := 0; i <= maxSlotIndex; i++ { maxCount := getMaxSliceCountInPool(i) poolsBySize[i] = newSimpleSlicePool(maxCount) } return &multiSizeSlicePool{poolsBySize: poolsBySize} } var indexOf32KSlot, _ = getSlotInfo(32 * 1024) // For a given requested len(slice), this returns the slot index to use, and the max // cap(slice) of the slices that will be found at that index func getSlotInfo(exactSliceLength int64) (slotIndex int, maxCapInSlot int) { if exactSliceLength <= 0 { panic("exact slice length must be greater than zero") } // raw slot index is fast computation of the base-2 logarithm, rounded down... rawSlotIndex := 63 - bits.LeadingZeros64(uint64(exactSliceLength)) // ...but in most cases we actually want to round up. // E.g. we want 255 to go into the same bucket as 256. Why? because we want exact // powers of 2 to be the largest thing in each bucket, since usually // we will be using powers of 2, and that means we will usually be using // all the allocated capacity (i.e. len == cap). That gives the most efficient use of RAM. // The only time we don't want to round up, is if we already had an exact power of // 2 to start with. isExactPowerOfTwo := bits.OnesCount64(uint64(exactSliceLength)) == 1 if isExactPowerOfTwo { slotIndex = rawSlotIndex } else { slotIndex = rawSlotIndex + 1 } // Max cap in slot is the biggest number that maps to that slot index // (e.g. slot index of exactSliceLength=1 (which=2 to the power of 0) // is 0 (because log-base2 of 1 == 0), so (2 to the power of slotIndex) // is the highest number that still fits the slot) maxCapInSlot = 1 << uint(slotIndex) return } func holdsSmallSlices(slotIndex int) bool { return slotIndex <= indexOf32KSlot } // For a given slot index, this returns the max number of pooled slices which the pool at // that index should be allowed to hold. func getMaxSliceCountInPool(slotIndex int) int { if holdsSmallSlices(slotIndex) { // Choose something fairly high for these because there's no significant RAM // cost in doing so, and small files are a tricky case for perf so let's give // them all the pooling help we can return 500 } else { // Limit the medium and large ones a bit more strictly. return 100 } } // RentSlice borrows a slice from the pool (or creates a new one if none of suitable capacity is available) // Note that the returned slice may contain non-zero data - i.e. old data from the previous time it was used. // That's safe IFF you are going to do the likes of io.ReadFull to read into it, since you know that all of the // old bytes will be overwritten in that case. func (mp *multiSizeSlicePool) RentSlice(desiredSize int64) []byte { slotIndex, maxCapInSlot := getSlotInfo(desiredSize) // get the pool that most closely corresponds to the desired size pool := mp.poolsBySize[slotIndex] // try to get a pooled slice if typedSlice := pool.Get(); typedSlice != nil { // clear out the entire slice up to the capacity // a zero-ing-out loop written in the right form in Go, will be automatically turned into a call to memclr, // which is an optimized Go runtime routine written in assembler // more info here: https://github.com/golang/go/commit/f03c9202c43e0abb130669852082117ca50aa9b1 typedSlice = typedSlice[0:maxCapInSlot] for i := range typedSlice { typedSlice[i] = 0 } // here we set len to the exact desired size that was requested typedSlice = typedSlice[0:desiredSize] return typedSlice } // make a new slice if nothing pooled return make([]byte, desiredSize, maxCapInSlot) } // returns the slice to its pool func (mp *multiSizeSlicePool) ReturnSlice(slice []byte) { slotIndex, _ := getSlotInfo(int64(cap(slice))) // be sure to use capacity, not length, here // get the pool that most closely corresponds to the desired size pool := mp.poolsBySize[slotIndex] // put the slice back into the pool pool.Put(slice) } // Prune inactive stuff in all the big slots if due (don't worry about the little ones, they don't eat much RAM) // Why do this? Because for the large slot sizes its hard to deal with slots that are full and IDLE. // I.e. we were using them, but now we're working with other files in the same job that have different chunk sizes, // so we have a pool slot that's full of slices that are no longer getting used. // Would it work to just give the slot a very small max capacity? Maybe. E.g. max number in slot = 4 for the 128 MB slot. // But can we be confident that 4 is enough for the pooling to have the desired benefits? Not sure. // Hence the pruning, so that we don't need to set the fixed limits that low. With pruning, the count will (gradually) // come down only when the slot is IDLE. func (mp *multiSizeSlicePool) Prune() { for index := 0; index < len(mp.poolsBySize); index++ { shouldPrune := !holdsSmallSlices(index) if shouldPrune { // Get one item from the pool and throw it away. // With repeated calls of Prune, this will gradually drain idle pools. // But, since Prune is not called very often, // it won't have much adverse impact on active pools. _ = mp.poolsBySize[index].Get() } } }