All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.timeandspace.smoothie.BitSetAndState Maven / Gradle / Ivy

/*
 * Copyright (C) The SmoothieMap Authors
 *
 * Licensed 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 io.timeandspace.smoothie;

import java.util.ConcurrentModificationException;

import static io.timeandspace.smoothie.SmoothieMap.SEGMENT_INTERMEDIATE_ALLOC_CAPACITY;
import static io.timeandspace.smoothie.SmoothieMap.SEGMENT_MAX_ALLOC_CAPACITY;
import static io.timeandspace.smoothie.Utils.verifyEqual;
import static io.timeandspace.smoothie.Utils.verifyThat;

/**
 * This class is a collection of static utility methods for working with {@link
 * io.timeandspace.smoothie.ContinuousSegment_BitSetAndStateArea#bitSetAndState}.
 *
 * The lower 48 bits ({@link #BIT_SET_BITS}; equal to {@link
 * SmoothieMap#SEGMENT_MAX_ALLOC_CAPACITY}) of a bitSetAndState is a bit set denoting free and
 * occupied allocation indexes. In intermediate-capacity segments, the higher of these bits are just
 * not used and always appear as free allocation indexes.
 *
 * The next 5 bits (from 48th to 52nd) represent the segment order. See {@link
 * SmoothieMap#averageSegmentOrder_lastComputed} and {@link SmoothieMap#order} for more info about
 * this concept.
 *
 * The next 5 bits (from 53rd to 57th) are used to codify the allocation capacity of the segment
 * when {@link ContinuousSegments} rather than {@link InterleavedSegments} are used. The value of 0
 * in these bits (i. e, all bits from 59th to 63rd are zeros) is assigned for {@link
 * ContinuousSegments.Segment17}, 1 - for {@link ContinuousSegments.Segment18}, etc. through 31 for
 * {@link ContinuousSegments.Segment48}.
 *
 * The final 6 bits (from 58th to 63rd) are used to hold special values to identify ordinary
 * segments (value 0), {@link SmoothieMap.InflatedSegment}s ({@link
 * #INFLATED_SEGMENT_SPECIAL_VALUE}), and segments in bulk operation (value 1; see {@link
 * #MIN_BULK_OPERATION_PLACEHOLDER_BIT_SET_AND_STATE}). This choice of special values allows
 * single-operation {@link #isInflatedBitSetAndState} and {@link
 * #isBulkOperationPlaceholderBitSetAndState}.
 *
 * All four values are packed into a single long rather than having their own fields in order to
 * save memory. The saving is 8 bytes if UseCompressedOops is false and
 * UseCompressedClassPointers is false, because ObjectAlignmentInBytes is at least 8 bytes, the
 * object header size is 16 bytes, and all other fields and logical units (such as allocation
 * indexes - key-value pairs) in segments (either {@link ContinuousSegments} or
 * {@link InterleavedSegments}) also take from 8 to 16 bytes. In addition, storing four values in
 * a single long field allows to reduce the number of memory reads and writes required during
 * insert and delete operations, when several of those values are read and/or updated.
 */
final class BitSetAndState {

    private static final int BIT_SET_BITS = SEGMENT_MAX_ALLOC_CAPACITY;
    private static final long BIT_SET_MASK = (1L << BIT_SET_BITS) - 1;

    static {
        // Rest of the logic of BitSetAndState depends on the exact value of BIT_SET_BITS and, thus,
        // SEGMENT_MAX_ALLOC_CAPACITY.
        verifyEqual(BIT_SET_BITS, 48);
    }

    /**
     * Free allocation slots correspond to set bits, and vice versa, occupied slots correspond to
     * clear bits in a bitSetAndState. This is done to avoid extra bitwise inversions for the
     * typical operation of finding the lowest free slot, because there are only {@link
     * Long#numberOfTrailingZeros(long)} and {@link Long#numberOfLeadingZeros(long)} methods in
     * {@code Long} class, no methods for leading/trailing "ones".
     */
    static final long EMPTY_BIT_SET = BIT_SET_MASK;

    private static final int SEGMENT_ORDER_SHIFT = BIT_SET_BITS;
    static final long SEGMENT_ORDER_UNIT = 1L << SEGMENT_ORDER_SHIFT;
    /**
     * 5 bits is enough to store values from 0 to {@link SmoothieMap#MAX_SEGMENTS_ARRAY_ORDER} = 30.
     */
    private static final int SEGMENT_ORDER_BITS = 5;
    private static final int SEGMENT_ORDER_MASK = (1 << SEGMENT_ORDER_BITS) - 1;

    /**
     * There are not enough bits to store the alloc capacity directly, because it requires 6 bits
     * (numbers up to {@link SmoothieMap#SEGMENT_MAX_ALLOC_CAPACITY}). Therefore it's not possible
     * to create a segment of any alloc capacity between 0 and {@link
     * SmoothieMap#SEGMENT_MAX_ALLOC_CAPACITY}. The minimum supported alloc capacity is {@link
     * #BASE_ALLOC_CAPACITY}.
     */
    private static final int EXTRA_ALLOC_CAPACITY_SHIFT =
            SEGMENT_ORDER_SHIFT + SEGMENT_ORDER_BITS;
    private static final int EXTRA_ALLOC_CAPACITY_BITS = 5;
    private static final int EXTRA_ALLOC_CAPACITY_MASK = (1 << EXTRA_ALLOC_CAPACITY_BITS) - 1;
    private static final int MAX_EXTRA_ALLOC_CAPACITY = (1 << EXTRA_ALLOC_CAPACITY_BITS) - 1;
    /**
     * Equals to 17.
     * Warning: {@link #allocCapacityMinusOneHalved} relies on this value to be odd. {@link
     * SmoothieMap.Segment#tryShrink1} relies on this value to be greater than 2.
     */
    private static final int BASE_ALLOC_CAPACITY =
            SEGMENT_MAX_ALLOC_CAPACITY - MAX_EXTRA_ALLOC_CAPACITY;

    private static final int SPECIAL_VALUE_SHIFT =
            EXTRA_ALLOC_CAPACITY_SHIFT + EXTRA_ALLOC_CAPACITY_BITS;
    private static final int SPECIAL_VALUE_BITS = 6;
    private static final int SPECIAL_VALUE_MASK = (1 << SPECIAL_VALUE_BITS) - 1;

    /**
     * A special value for {@link SmoothieMap.InflatedSegment} to make it possible to identify it
     * before checking the class of a segment object.
     *
     * Implementation of {@link #isInflatedBitSetAndState} depends on the fact that this value is
     * equal to 0b100000, so that all inflated segment's bitSetAndStates are negative and the state
     * of an inflated segment with order 0 is equal to {@link Long#MIN_VALUE}.
     *
     * @see #makeInflatedBitSetAndState
     */
    private static final int INFLATED_SEGMENT_SPECIAL_VALUE = 1 << (SPECIAL_VALUE_BITS - 1);

    /**
     * This value is OR-ed with bitSetAndStates of segments in the beginning of bulk operations
     * (like {@link SmoothieMap.Segment#tryShrink3} and {@link SmoothieMap#splitAndInsert})) so that
     * concurrent operations could catch this condition and throw a {@link
     * ConcurrentModificationException} more likely.
     */
    private static final long MIN_BULK_OPERATION_PLACEHOLDER_BIT_SET_AND_STATE =
            1L << SPECIAL_VALUE_SHIFT;

    static {
        // Checking that all bitSetAndState's areas are set up correctly
        long fullBitSetAndState = BIT_SET_MASK ^
                ((long) SEGMENT_ORDER_MASK << SEGMENT_ORDER_SHIFT) ^
                ((long) EXTRA_ALLOC_CAPACITY_MASK << EXTRA_ALLOC_CAPACITY_SHIFT) ^
                ((long) SPECIAL_VALUE_MASK << SPECIAL_VALUE_SHIFT);
        verifyEqual(fullBitSetAndState, -1L);
        verifyEqual(BASE_ALLOC_CAPACITY, 17);
    }

    static long makeNewBitSetAndState(int allocCapacity, int segmentOrder) {
        int extraAllocCapacity = allocCapacity - BASE_ALLOC_CAPACITY;
        return EMPTY_BIT_SET |
                (((long) segmentOrder) << SEGMENT_ORDER_SHIFT) |
                (((long) extraAllocCapacity) << EXTRA_ALLOC_CAPACITY_SHIFT);
    }

    static long clearBitSet(long bitSetAndState) {
        return bitSetAndState | EMPTY_BIT_SET;
    }

    /**
     * A privately populated segment has the first {@code segmentSize} contiguous allocation slots
     * occupied.
     */
    static long makeBitSetAndStateForPrivatelyPopulatedContinuousSegment(
            int allocCapacity, int segmentOrder, int segmentSize) {
        long bitSet = ((EMPTY_BIT_SET << segmentSize) & EMPTY_BIT_SET);
        int extraAllocCapacity = allocCapacity - BASE_ALLOC_CAPACITY;
        return bitSet |
                (((long) segmentOrder) << SEGMENT_ORDER_SHIFT) |
                (((long) extraAllocCapacity) << EXTRA_ALLOC_CAPACITY_SHIFT);
    }

    static long incrementSegmentOrder(long bitSetAndState) {
        bitSetAndState += SEGMENT_ORDER_UNIT;
        return bitSetAndState;
    }

    static long makeBitSetAndStateWithNewAllocCapacity(
            long bitSetAndState, int newAllocCapacity) {
        long negativeExtraAllocCapacityMask =
                ~(((long) EXTRA_ALLOC_CAPACITY_MASK) << EXTRA_ALLOC_CAPACITY_SHIFT);
        int newExtraAllocCapacity = newAllocCapacity - BASE_ALLOC_CAPACITY;
        return (bitSetAndState & negativeExtraAllocCapacityMask) |
                (((long) newExtraAllocCapacity) << EXTRA_ALLOC_CAPACITY_SHIFT);
    }

    /**
     * Returns a bitSetAndState that includes a full bit set, an extra alloc capacity of 0 for
     * {@link ContinuousSegments} and {@link SmoothieMap#SEGMENT_INTERMEDIATE_ALLOC_CAPACITY} -
     * {@link #BASE_ALLOC_CAPACITY} for {@link InterleavedSegments}, the specified segment order,
     * and a special value {@link #INFLATED_SEGMENT_SPECIAL_VALUE}.
     */
    static long makeInflatedBitSetAndState(int segmentOrder) {
        int extraAllocCapacity = SEGMENT_INTERMEDIATE_ALLOC_CAPACITY - BASE_ALLOC_CAPACITY;
        return (((long) INFLATED_SEGMENT_SPECIAL_VALUE) << SPECIAL_VALUE_SHIFT) |
                (((long) extraAllocCapacity) << EXTRA_ALLOC_CAPACITY_SHIFT) |
                (((long) segmentOrder) << SEGMENT_ORDER_SHIFT);
    }

    /**
     * Bulk operation placeholder includes a full bit set (see {@link #EMPTY_BIT_SET}), a special
     * value 1. Any entry insertion into the segment triggers {@link SmoothieMap#makeSpaceAndInsert}
     * if encounters this bitSetAndState. Bulk operation placeholder bitSetAndState is identified
     * there and then a {@link ConcurrentModificationException} is thrown.
     */
    static long makeBulkOperationPlaceholderBitSetAndState(long bitSetAndState) {
        return (bitSetAndState | MIN_BULK_OPERATION_PLACEHOLDER_BIT_SET_AND_STATE) &
                (~EMPTY_BIT_SET);
    }

    static boolean isInflatedBitSetAndState(long bitSetAndState) {
        long inflatedSegment_maxBitSetAndState = ((1L << SPECIAL_VALUE_SHIFT) - 1) |
                ((long) INFLATED_SEGMENT_SPECIAL_VALUE) << SPECIAL_VALUE_SHIFT;
        // This kind of comparison is possible because of INFLATED_SEGMENT_SPECIAL_VALUE which is
        // 0b100000.
        return bitSetAndState <= inflatedSegment_maxBitSetAndState;
    }

    static boolean isBulkOperationPlaceholderBitSetAndState(long bitSetAndState) {
        return bitSetAndState >= MIN_BULK_OPERATION_PLACEHOLDER_BIT_SET_AND_STATE;
    }

    /**
     * Inverts the bitSet's bits so that set bits correspond to occupied slots (by default, it's
     * vice versa in bitSetAndState, see the comment for {@link #EMPTY_BIT_SET}) to make it
     * convenient for branchless iteration via {@link Long#numberOfLeadingZeros} or {@link
     * Long#numberOfTrailingZeros}.
     */
    static long extractBitSetForIteration(long bitSetAndState) {
        return (~bitSetAndState) & BIT_SET_MASK;
    }

    /**
     * This method might return a value outside of the allowed range if the segment is already
     * full. The result must be checked for being less than {@link #allocCapacity}.
     */
    static int lowestFreeAllocIndex(long bitSetAndState) {
        // TODO consider organizing bitSetAndState so that LZCNT rather than TZCNT is used because
        //  it's cheaper on AMD CPUs up to Ryzen.
        // TODO in a SmoothieMap's specialization without support for entry removals, consider
        //  optimizing this as bitCount(extractBitSetForIteration()) to employ POPCNT instruction
        //  which is available for Intel Nehalem through Ivy Bridge, while TZCNT/LZCNT is available
        //  only starting from Intel Haswell. However, note that POPCNT is more exprensive that
        //  LZCNT/TZCNT on pre-Ryzen AMD's architectures.
        return Long.numberOfTrailingZeros(bitSetAndState);
    }

    /**
     * @param allocIndexBoundary must be positive and less than {@link #allocCapacity} called on
     *        the same bitSetAndState
     * @return a free alloc index closest to the specified boundary (e. g. 3 is a boundary between
     *         alloc indexes 2 and 3), or a value equal to or greater than {@link #allocCapacity}
     *         if the segment is full.
     * @see InterleavedSegments#allocIndexBoundaryForLocalAllocation
     *
     * @implNote TODO document pass allocCapacity vs. compute it from bitSetAndState tradeoff.
     */
    static int freeAllocIndexClosestTo(long bitSetAndState, int allocIndexBoundary
            , int allocCapacity) {

        long bitSetMask = BIT_SET_MASK >>> (SEGMENT_MAX_ALLOC_CAPACITY - allocCapacity);
        // If there are no free alloc indexes beyond allocIndexBoundary
        // distanceToClosestNextFreeAllocIndex must be at least BIT_SET_BITS so that it will be
        // greater than distanceToClosestPrevFreeAllocIndex if the latter points to a free alloc
        // index and, therefore, closestPrevFreeAllocIndex is "chosen" in the code below. (If there
        // are no free alloc indexes before allocIndexBoundary, too, then
        // distanceToClosestPrevFreeAllocIndex will be 64.) The simplest way to achieve that is to
        // mask the bit set: `bitSetAndState & BIT_SET_MASK`, which makes
        // distanceToClosestNextFreeAllocIndex computed to 64 if there are no free alloc indexes
        // beyond allocIndexBoundary.
        int distanceToClosestNextFreeAllocIndex =
                Long.numberOfTrailingZeros((bitSetAndState & bitSetMask) >>> allocIndexBoundary);
        // If all alloc indexes before the boundary are occupied (that is, the corresponding bits
        // are zero), the left shift just extends the bitSetAndState's values with zeros so that it
        // is all zeros and numberOfLeadingZeros() returns 64. distanceToClosestNextFreeAllocIndex
        // cannot be less than that distanceToClosestPrevFreeAllocIndex. Therefore, the next
        // closest free alloc index (perhaps, beyond allocCapacity) will be returned from this
        // method because the logic below is organized to choose the next rather than the previous
        // index if the distances from the boundary are equal. Therefore, this method never returns
        // negative result.
        int distanceToClosestPrevFreeAllocIndex =
                // `<< -allocIndexBoundary` doesn't need to consider allocIndexBoundary = 0 because
                // the method's contract requires allocIndexBoundary to be positive.
                Long.numberOfLeadingZeros(bitSetAndState << -allocIndexBoundary);

        // The following code is a branchless version of
        //
        // if (distanceToClosestNextFreeAllocIndex <= distanceToClosestPrevFreeAllocIndex) {
        //     return allocIndexBoundary + distanceToClosestNextFreeAllocIndex;
        // } else {
        //     return allocIndexBoundary - 1 - distanceToClosestPrevFreeAllocIndex;
        // }
        //
        // It's important that if distanceToClosestNextFreeAllocIndex and
        // distanceToClosestPrevFreeAllocIndex are equal then the preference is given to
        // `allocIndexBoundary + distanceToClosestNextFreeAllocIndex` because it covers the edge
        // case of a full bit set: distanceToClosestNextFreeAllocIndex will be 64 (see the
        // definition of this variable) and distanceToClosestPrevFreeAllocIndex will be 64 too. We
        // have to return `allocIndexBoundary + distanceToClosestNextFreeAllocIndex` to comply with
        // the specification of this method which states that this method should return an
        // impossible positive (rather than negative) value if the bit set is full.

        // This value contains all zeros if distanceToClosestPrevFreeAllocIndex <
        // distanceToClosestNextFreeAllocIndex, all ones otherwise.
        int distanceToPrevIsLessThanDistanceToPrev = signExtend(
                distanceToClosestPrevFreeAllocIndex - distanceToClosestNextFreeAllocIndex);

        int closestNextFreeAllocIndex = allocIndexBoundary + distanceToClosestNextFreeAllocIndex;
        int closestPrevFreeAllocIndex =
                allocIndexBoundary - 1 - distanceToClosestPrevFreeAllocIndex;

        // Hopefully, JIT will simplify this expression and avoid computing
        // closestPrevFreeAllocIndex altogether.
        return closestNextFreeAllocIndex + ((closestPrevFreeAllocIndex - closestNextFreeAllocIndex)
                & distanceToPrevIsLessThanDistanceToPrev);
    }

    private static int signExtend(int v) {
        return v >> (Integer.SIZE - 1);
    }

    static long clearAllocBit(long bitSetAndState, long allocIndex) {
        return bitSetAndState | (1L << allocIndex);
    }

    static long setAllocBit(long bitSetAndState, int allocIndex) {
        return bitSetAndState & ~(1L << allocIndex);
    }

    static long setLowestAllocBit(long bitSetAndState) {
        return bitSetAndState & (bitSetAndState - 1);
    }

    static int segmentSize(long bitSetAndState) {
        // Using this form rather than Long.bitCount((~bitSetAndState) & BIT_SET_MASK), because
        // segmentSize() method is typically used in other arithmetic operations and
        // comparisons, so the `BIT_SET_BITS -` part could be applied by the compiler to some
        // earlier computed expression, shortening the longest data dependency chain.
        return BIT_SET_BITS - Long.bitCount(bitSetAndState & BIT_SET_MASK);
    }

    static boolean isEmpty(long bitSetAndState) {
        return (bitSetAndState & BIT_SET_MASK) == EMPTY_BIT_SET;
    }

    static int segmentOrder(long bitSetAndState) {
        return ((int) (bitSetAndState >>> SEGMENT_ORDER_SHIFT)) & SEGMENT_ORDER_MASK;
    }

    static int allocCapacity(long bitSetAndState) {
        int extraAllocCapacity = ((int) (bitSetAndState >>> EXTRA_ALLOC_CAPACITY_SHIFT)) &
                EXTRA_ALLOC_CAPACITY_MASK;
        return BASE_ALLOC_CAPACITY + extraAllocCapacity;
    }

    /**
     * Works consistently for ordinary and inflated segments given that {@link
     * SmoothieMap.InflatedSegment} is *not* a full-capacity segment (it extends {@link
     * InterleavedSegments.IntermediateCapacitySegment}) and {@link #makeInflatedBitSetAndState}
     * returns a bitSetAndState with the corresponding extra alloc capacity.
     */
    static boolean isFullCapacity(long bitSetAndState) {
        long fullAllocCapacityMask = ((long) SEGMENT_MAX_ALLOC_CAPACITY - BASE_ALLOC_CAPACITY) <<
                EXTRA_ALLOC_CAPACITY_SHIFT;
        return (bitSetAndState & fullAllocCapacityMask) == fullAllocCapacityMask;
    }

    /**
     * Computes `(allocCapacity(bitSetAndState) - 1) / 2` with the same complexity as {@link
     * #allocCapacity} itself.
     */
    static int allocCapacityMinusOneHalved(long bitSetAndState) {
        int extraAllocCapacityHalved =
                ((int) (bitSetAndState >>> (EXTRA_ALLOC_CAPACITY_SHIFT + 1)))
                        & (EXTRA_ALLOC_CAPACITY_MASK >>> 1);
        // For this method to return the correct value of `(allocCapacity(bitSetAndState) - 1) / 2`,
        // BASE_ALLOC_CAPACITY should be odd.
        return ((BASE_ALLOC_CAPACITY - 1) / 2) + extraAllocCapacityHalved;
    }

    /** A structured view on a bitSetAndState for debugging. */
    static class DebugBitSetAndState {
        enum Type {
            ORDINARY, INFLATED, BULK_OPERATION_PLACEHOLDER, UNKNOWN;

            static Type fromSpecialValue(int specialValue) {
                switch (specialValue) {
                    case 0: return ORDINARY;
                    case 1: return BULK_OPERATION_PLACEHOLDER;
                    case INFLATED_SEGMENT_SPECIAL_VALUE: return INFLATED;
                    default: return UNKNOWN;
                }
            }
        }

        final Type type;
        final long bitSet;
        final int size;
        final int order;
        final int allocCapacity;

        DebugBitSetAndState(long bitSetAndState) {
            type = Type.fromSpecialValue((int) (bitSetAndState >>> SPECIAL_VALUE_SHIFT));
            bitSet = extractBitSetForIteration(bitSetAndState);
            size = Long.bitCount(bitSet);
            order = segmentOrder(bitSetAndState);
            allocCapacity = allocCapacity(bitSetAndState);
        }

        @Override
        public String toString() {
            return "DebugBitSetAndState{" +
                    "type=" + type +
                    ", bitSet=" + bitSet +
                    ", size=" + size +
                    ", order=" + order +
                    ", allocCapacity=" + allocCapacity +
                    '}';
        }
    }

    private BitSetAndState() {}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy