// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package bitutils import ( "math" "math/bits" "unsafe" "github.com/apache/arrow/go/v10/arrow/bitutil" "github.com/apache/arrow/go/v10/internal/utils" ) func loadWord(byt []byte) uint64 { return utils.ToLEUint64(*(*uint64)(unsafe.Pointer(&byt[0]))) } func shiftWord(current, next uint64, shift int64) uint64 { if shift == 0 { return current } return (current >> shift) | (next << (64 - shift)) } // BitBlockCount is returned by the various bit block counter utilities // in order to return a length of bits and the population count of that // slice of bits. type BitBlockCount struct { Len int16 Popcnt int16 } // NoneSet returns true if ALL the bits were 0 in this set, ie: Popcnt == 0 func (b BitBlockCount) NoneSet() bool { return b.Popcnt == 0 } // AllSet returns true if ALL the bits were 1 in this set, ie: Popcnt == Len func (b BitBlockCount) AllSet() bool { return b.Len == b.Popcnt } // BitBlockCounter is a utility for grabbing chunks of a bitmap at a time and efficiently // counting the number of bits which are 1. type BitBlockCounter struct { bitmap []byte bitsRemaining int64 bitOffset int8 } const ( wordBits int64 = 64 fourWordsBits int64 = wordBits * 4 ) // NewBitBlockCounter returns a BitBlockCounter for the passed bitmap starting at startOffset // of length nbits. func NewBitBlockCounter(bitmap []byte, startOffset, nbits int64) *BitBlockCounter { return &BitBlockCounter{ bitmap: bitmap[startOffset/8:], bitsRemaining: nbits, bitOffset: int8(startOffset % 8), } } // getBlockSlow is for returning a block of the requested size when there aren't // enough bits remaining to do a full word computation. func (b *BitBlockCounter) getBlockSlow(blockSize int64) BitBlockCount { runlen := int16(utils.Min(b.bitsRemaining, blockSize)) popcnt := int16(bitutil.CountSetBits(b.bitmap, int(b.bitOffset), int(runlen))) b.bitsRemaining -= int64(runlen) b.bitmap = b.bitmap[runlen/8:] return BitBlockCount{runlen, popcnt} } // NextFourWords returns the next run of available bits, usually 256. The // returned pair contains the size of run and the number of true values. // The last block will have a length less than 256 if the bitmap length // is not a multiple of 256, and will return 0-length blocks in subsequent // invocations. func (b *BitBlockCounter) NextFourWords() BitBlockCount { if b.bitsRemaining == 0 { return BitBlockCount{0, 0} } totalPopcnt := 0 if b.bitOffset == 0 { // if we're aligned at 0 bitoffset, then we can easily just jump from // word to word nice and easy. if b.bitsRemaining < fourWordsBits { return b.getBlockSlow(fourWordsBits) } totalPopcnt += bits.OnesCount64(loadWord(b.bitmap)) totalPopcnt += bits.OnesCount64(loadWord(b.bitmap[8:])) totalPopcnt += bits.OnesCount64(loadWord(b.bitmap[16:])) totalPopcnt += bits.OnesCount64(loadWord(b.bitmap[24:])) } else { // When the offset is > 0, we need there to be a word beyond the last // aligned word in the bitmap for the bit shifting logic. if b.bitsRemaining < 5*fourWordsBits-int64(b.bitOffset) { return b.getBlockSlow(fourWordsBits) } current := loadWord(b.bitmap) next := loadWord(b.bitmap[8:]) totalPopcnt += bits.OnesCount64(shiftWord(current, next, int64(b.bitOffset))) current = next next = loadWord(b.bitmap[16:]) totalPopcnt += bits.OnesCount64(shiftWord(current, next, int64(b.bitOffset))) current = next next = loadWord(b.bitmap[24:]) totalPopcnt += bits.OnesCount64(shiftWord(current, next, int64(b.bitOffset))) current = next next = loadWord(b.bitmap[32:]) totalPopcnt += bits.OnesCount64(shiftWord(current, next, int64(b.bitOffset))) } b.bitmap = b.bitmap[bitutil.BytesForBits(fourWordsBits):] b.bitsRemaining -= fourWordsBits return BitBlockCount{256, int16(totalPopcnt)} } // NextWord returns the next run of available bits, usually 64. The returned // pair contains the size of run and the number of true values. The last // block will have a length less than 64 if the bitmap length is not a // multiple of 64, and will return 0-length blocks in subsequent // invocations. func (b *BitBlockCounter) NextWord() BitBlockCount { if b.bitsRemaining == 0 { return BitBlockCount{0, 0} } popcnt := 0 if b.bitOffset == 0 { if b.bitsRemaining < wordBits { return b.getBlockSlow(wordBits) } popcnt = bits.OnesCount64(loadWord(b.bitmap)) } else { // When the offset is > 0, we need there to be a word beyond the last // aligned word in the bitmap for the bit shifting logic. if b.bitsRemaining < (2*wordBits - int64(b.bitOffset)) { return b.getBlockSlow(wordBits) } popcnt = bits.OnesCount64(shiftWord(loadWord(b.bitmap), loadWord(b.bitmap[8:]), int64(b.bitOffset))) } b.bitmap = b.bitmap[wordBits/8:] b.bitsRemaining -= wordBits return BitBlockCount{64, int16(popcnt)} } // OptionalBitBlockCounter is a useful counter to iterate through a possibly // non-existent validity bitmap to allow us to write one code path for both // the with-nulls and no-nulls cases without giving up a lot of performance. type OptionalBitBlockCounter struct { hasBitmap bool pos int64 len int64 counter *BitBlockCounter } // NewOptionalBitBlockCounter constructs and returns a new bit block counter that // can properly handle the case when a bitmap is null, if it is guaranteed that the // the bitmap is not nil, then prefer NewBitBlockCounter here. func NewOptionalBitBlockCounter(bitmap []byte, offset, length int64) *OptionalBitBlockCounter { var counter *BitBlockCounter if bitmap != nil { counter = NewBitBlockCounter(bitmap, offset, length) } return &OptionalBitBlockCounter{ hasBitmap: bitmap != nil, pos: 0, len: length, counter: counter, } } // NextBlock returns block count for next word when the bitmap is available otherwise // return a block with length up to INT16_MAX when there is no validity // bitmap (so all the referenced values are not null). func (obc *OptionalBitBlockCounter) NextBlock() BitBlockCount { const maxBlockSize = math.MaxInt16 if obc.hasBitmap { block := obc.counter.NextWord() obc.pos += int64(block.Len) return block } blockSize := int16(utils.Min(maxBlockSize, obc.len-obc.pos)) obc.pos += int64(blockSize) // all values are non-null return BitBlockCount{blockSize, blockSize} } // NextWord is like NextBlock, but returns a word-sized block even when there is no // validity bitmap func (obc *OptionalBitBlockCounter) NextWord() BitBlockCount { const wordsize = 64 if obc.hasBitmap { block := obc.counter.NextWord() obc.pos += int64(block.Len) return block } blockSize := int16(utils.Min(wordsize, obc.len-obc.pos)) obc.pos += int64(blockSize) // all values are non-null return BitBlockCount{blockSize, blockSize} } // VisitBitBlocks is a utility for easily iterating through the blocks of bits in a bitmap, // calling the appropriate visitValid/visitInvalid function as we iterate through the bits. // visitValid is called with the bitoffset of the valid bit. Don't use this inside a tight // loop when performance is needed and instead prefer manually constructing these loops // in that scenario. func VisitBitBlocks(bitmap []byte, offset, length int64, visitValid func(pos int64), visitInvalid func()) { counter := NewOptionalBitBlockCounter(bitmap, offset, length) pos := int64(0) for pos < length { block := counter.NextBlock() if block.AllSet() { for i := 0; i < int(block.Len); i, pos = i+1, pos+1 { visitValid(pos) } } else if block.NoneSet() { for i := 0; i < int(block.Len); i, pos = i+1, pos+1 { visitInvalid() } } else { for i := 0; i < int(block.Len); i, pos = i+1, pos+1 { if bitutil.BitIsSet(bitmap, int(offset+pos)) { visitValid(pos) } else { visitInvalid() } } } } } // VisitBitBlocks is a utility for easily iterating through the blocks of bits in a bitmap, // calling the appropriate visitValid/visitInvalid function as we iterate through the bits. // visitValid is called with the bitoffset of the valid bit. Don't use this inside a tight // loop when performance is needed and instead prefer manually constructing these loops // in that scenario. func VisitBitBlocksShort(bitmap []byte, offset, length int64, visitValid func(pos int64) error, visitInvalid func() error) error { counter := NewOptionalBitBlockCounter(bitmap, offset, length) pos := int64(0) for pos < length { block := counter.NextBlock() if block.AllSet() { for i := 0; i < int(block.Len); i, pos = i+1, pos+1 { if err := visitValid(pos); err != nil { return err } } } else if block.NoneSet() { for i := 0; i < int(block.Len); i, pos = i+1, pos+1 { if err := visitInvalid(); err != nil { return err } } } else { for i := 0; i < int(block.Len); i, pos = i+1, pos+1 { if bitutil.BitIsSet(bitmap, int(offset+pos)) { if err := visitValid(pos); err != nil { return err } } else { if err := visitInvalid(); err != nil { return err } } } } } return nil } type bitOp struct { bit func(bool, bool) bool word func(uint64, uint64) uint64 } var ( bitBlockAnd = bitOp{ bit: func(a, b bool) bool { return a && b }, word: func(a, b uint64) uint64 { return a & b }, } bitBlockAndNot = bitOp{ bit: func(a, b bool) bool { return a && !b }, word: func(a, b uint64) uint64 { return a &^ b }, } bitBlockOr = bitOp{ bit: func(a, b bool) bool { return a || b }, word: func(a, b uint64) uint64 { return a | b }, } bitBlockOrNot = bitOp{ bit: func(a, b bool) bool { return a || !b }, word: func(a, b uint64) uint64 { return a | ^b }, } ) // BinaryBitBlockCounter computes popcounts on the result of bitwise // operations between two bitmaps, 64 bits at a time. A 64-bit word // is loaded from each bitmap, then the popcount is computed on // e.g. the bitwise-and of the two words type BinaryBitBlockCounter struct { left []byte right []byte bitsRemaining int64 leftOffset, rightOffset int64 bitsRequiredForWords int64 } // NewBinaryBitBlockCounter constructs a binary bit block counter for // computing the popcounts on the results of operations between // the passed in bitmaps, with their respective offsets. func NewBinaryBitBlockCounter(left, right []byte, leftOffset, rightOffset int64, length int64) *BinaryBitBlockCounter { ret := &BinaryBitBlockCounter{ left: left[leftOffset/8:], right: right[rightOffset/8:], leftOffset: leftOffset % 8, rightOffset: rightOffset % 8, bitsRemaining: length, } leftBitsReq := int64(64) if ret.leftOffset != 0 { leftBitsReq = 64 + (64 - ret.leftOffset) } rightBitsReq := int64(64) if ret.rightOffset != 0 { rightBitsReq = 64 + (64 - ret.rightOffset) } if leftBitsReq > rightBitsReq { ret.bitsRequiredForWords = leftBitsReq } else { ret.bitsRequiredForWords = rightBitsReq } return ret } // NextAndWord returns the popcount of the bitwise-and of the next run // of available bits, up to 64. The returned pair contains the size of // the run and the number of true values. the last block will have a // length less than 64 if the bitmap length is not a multiple of 64, // and will return 0-length blocks in subsequent invocations func (b *BinaryBitBlockCounter) NextAndWord() BitBlockCount { return b.nextWord(bitBlockAnd) } // NextAndNotWord is like NextAndWord but performs x &^ y on each run func (b *BinaryBitBlockCounter) NextAndNotWord() BitBlockCount { return b.nextWord(bitBlockAndNot) } // NextOrWord is like NextAndWord but performs x | y on each run func (b *BinaryBitBlockCounter) NextOrWord() BitBlockCount { return b.nextWord(bitBlockOr) } // NextOrWord is like NextAndWord but performs x | ^y on each run func (b *BinaryBitBlockCounter) NextOrNotWord() BitBlockCount { return b.nextWord(bitBlockOrNot) } func (b *BinaryBitBlockCounter) nextWord(op bitOp) BitBlockCount { if b.bitsRemaining == 0 { return BitBlockCount{} } // when offset is >0, we need there to be a word beyond the last // aligned word in the bitmap for the bit shifting logic if b.bitsRemaining < b.bitsRequiredForWords { runLength := int16(b.bitsRemaining) if runLength > int16(wordBits) { runLength = int16(wordBits) } var popcount int16 for i := int16(0); i < runLength; i++ { if op.bit(bitutil.BitIsSet(b.left, int(b.leftOffset)+int(i)), bitutil.BitIsSet(b.right, int(b.rightOffset)+int(i))) { popcount++ } } // this code path should trigger _at most_ 2 times. in the "two times" // case, the first time the run length will be a multiple of 8. b.left = b.left[runLength/8:] b.right = b.right[runLength/8:] b.bitsRemaining -= int64(runLength) return BitBlockCount{Len: runLength, Popcnt: popcount} } var popcount int if b.leftOffset == 0 && b.rightOffset == 0 { popcount = bits.OnesCount64(op.word(loadWord(b.left), loadWord(b.right))) } else { leftWord := shiftWord(loadWord(b.left), loadWord(b.left[8:]), b.leftOffset) rightWord := shiftWord(loadWord(b.right), loadWord(b.right[8:]), b.rightOffset) popcount = bits.OnesCount64(op.word(leftWord, rightWord)) } b.left = b.left[wordBits/8:] b.right = b.right[wordBits/8:] b.bitsRemaining -= wordBits return BitBlockCount{Len: int16(wordBits), Popcnt: int16(popcount)} }