![JAR search and dependency download from the Maven repository](/logo.png)
io.timeandspace.smoothie.InterleavedSegments Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of smoothie-map Show documentation
Show all versions of smoothie-map Show documentation
Map implementation with low footprint and no latency spikes
/*
* 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 io.timeandspace.smoothie.BitSetAndState.DebugBitSetAndState;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ConcurrentModificationException;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static io.timeandspace.smoothie.BitSetAndState.clearBitSet;
import static io.timeandspace.smoothie.BitSetAndState.extractBitSetForIteration;
import static io.timeandspace.smoothie.BitSetAndState.freeAllocIndexClosestTo;
import static io.timeandspace.smoothie.BitSetAndState.makeNewBitSetAndState;
import static io.timeandspace.smoothie.BitSetAndState.segmentOrder;
import static io.timeandspace.smoothie.BitSetAndState.segmentSize;
import static io.timeandspace.smoothie.BitSetAndState.setAllocBit;
import static io.timeandspace.smoothie.HashTable.EMPTY_DATA_GROUP;
import static io.timeandspace.smoothie.HashTable.GROUP_SLOTS;
import static io.timeandspace.smoothie.HashTable.HASH_TABLE_GROUPS;
import static io.timeandspace.smoothie.HashTable.HASH_TABLE_SLOTS;
import static io.timeandspace.smoothie.HashTable.assertValidGroupIndex;
import static io.timeandspace.smoothie.HashTable.assertValidSlotIndexWithinGroup;
import static io.timeandspace.smoothie.HashTable.baseGroupIndex;
import static io.timeandspace.smoothie.HashTable.copyOutboundOverflowBits;
import static io.timeandspace.smoothie.HashTable.extractAllocIndex;
import static io.timeandspace.smoothie.HashTable.extractDataByte;
import static io.timeandspace.smoothie.HashTable.extractTagByte;
import static io.timeandspace.smoothie.HashTable.firstAllocIndex;
import static io.timeandspace.smoothie.HashTable.lowestMatchingSlotIndexFromTrailingZeros;
import static io.timeandspace.smoothie.HashTable.makeData;
import static io.timeandspace.smoothie.HashTable.makeDataWithZeroOutboundOverflowBit;
import static io.timeandspace.smoothie.HashTable.matchFull;
import static io.timeandspace.smoothie.HashTable.setSlotEmpty;
import static io.timeandspace.smoothie.HashTable.slotByteOffset;
import static io.timeandspace.smoothie.LongMath.clearLowestSetBit;
import static io.timeandspace.smoothie.OutboundOverflowCounts.computeOutboundOverflowCount_perGroupChanges;
import static io.timeandspace.smoothie.Segments.valueOffsetFromAllocOffset;
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.UnsafeUtils.ARRAY_OBJECT_INDEX_SCALE_AS_LONG;
import static io.timeandspace.smoothie.UnsafeUtils.U;
import static io.timeandspace.smoothie.UnsafeUtils.minInstanceFieldOffset;
import static io.timeandspace.smoothie.Utils.verifyEqual;
import static io.timeandspace.smoothie.Utils.verifyThat;
final class InterleavedSegments {
private static final int NUM_FULL_STRIDES = 7;
/** `* 2` means that each allocation contains a key and a value, i. e. two objects. */
private static final long ALLOCATION_INDEX_SIZE_IN_BYTES = ARRAY_OBJECT_INDEX_SCALE_AS_LONG * 2;
/** @see BitSetAndState#freeAllocIndexClosestTo */
@HotPath
static int allocIndexBoundaryForLocalAllocation(int groupIndex
, int isFullCapacitySegment) {
// Branchless version of the following logic:
// if (isFullCapacitySegment == 1)
// return FullCapacitySegment.allocIndexBoundaryForLocalAllocation(groupIndex);
// else // assuming isFullCapacitySegment == 0
// return IntermediateCapacitySegment.allocIndexBoundaryForLocalAllocation(groupIndex);
return (IntermediateCapacitySegment.STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES +
// Just adding isFullCapacitySegment relying on the fact that
// FullCapacitySegment.STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES -
// IntermediateCapacitySegment.STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES = 1.
isFullCapacitySegment) +
groupIndex * (IntermediateCapacitySegment.STRIDE_SIZE_IN_ALLOC_INDEXES +
isFullCapacitySegment * (FullCapacitySegment.STRIDE_SIZE_IN_ALLOC_INDEXES -
IntermediateCapacitySegment.STRIDE_SIZE_IN_ALLOC_INDEXES));
}
static long allocOffset(long allocIndex
, long isFullCapacitySegment) {
// Both FullCapacitySegment.allocOffset() and IntermediateCapacitySegment.allocOffset()
// include a fair amount of work for CPU. The alternative is to inline these methods and
// choose all constants in a branchless manner, but there are as many as 5 constants to be
// chosen: strideIncrement, divMultiplier, strideSizeInAllocIndexes, stride0_offset, and
// strideSizeInBytes so there is even more work for CPU in this approach.
long fullCap_allocOffset = FullCapacitySegment.allocOffset(allocIndex);
long intermediateCap_allocOffset = IntermediateCapacitySegment.allocOffset(allocIndex);
return intermediateCap_allocOffset +
isFullCapacitySegment * (fullCap_allocOffset - intermediateCap_allocOffset);
}
static long readTagGroupAtOffset(Object segment, long tagGroupOffset) {
return U.getLong(segment, tagGroupOffset);
}
static void writeTagGroupAtOffset(Object segment, long tagGroupOffset, long tagGroup) {
U.putLong(segment, tagGroupOffset, tagGroup);
}
static long readDataGroupAtOffset(Object segment, long dataGroupOffset) {
return U.getLong(segment, dataGroupOffset);
}
static void writeDataGroupAtOffset(Object segment, long dataGroupOffset, long dataGroup) {
U.putLong(segment, dataGroupOffset, dataGroup);
}
@HotPath
static void writeTagAndData(Object segment,
long isFullCapacitySegment,
long groupIndex, int slotIndexWithinGroup, byte tag, byte data) {
long slotByteOffset = slotByteOffset(slotIndexWithinGroup);
long tagGroupOffset = tagGroupOffset(groupIndex, isFullCapacitySegment);
U.putByte(segment, tagGroupOffset + slotByteOffset, tag);
long dataGroupOffset = dataGroupFromTagGroupOffset(tagGroupOffset);
U.putByte(segment, dataGroupOffset + slotByteOffset, data);
}
static long tagGroupOffset(long groupIndex
, long isFullCapacitySegment) {
// Branchless version of the following logic:
// if (isFullCapacitySegment == 1)
// return FullCapacitySegment.tagGroupOffset(groupIndex);
// else // assuming isFullCapacitySegment == 0
// return IntermediateCapacitySegment.tagGroupOffset(groupIndex);
return IntermediateCapacitySegment.TAG_GROUP_0_OFFSET + isFullCapacitySegment *
(FullCapacitySegment.TAG_GROUP_0_OFFSET -
IntermediateCapacitySegment.TAG_GROUP_0_OFFSET) +
groupIndex * (IntermediateCapacitySegment.STRIDE_SIZE_IN_BYTES +
isFullCapacitySegment * (FullCapacitySegment.STRIDE_SIZE_IN_BYTES -
IntermediateCapacitySegment.STRIDE_SIZE_IN_BYTES));
}
static long dataGroupOffset(long groupIndex
, long isFullCapacitySegment) {
// Branchless version of the following logic:
// if (isFullCapacitySegment == 1)
// return FullCapacitySegment.dataGroupOffset(groupIndex);
// else // assuming isFullCapacitySegment == 0
// return IntermediateCapacitySegment.dataGroupOffset(groupIndex);
return IntermediateCapacitySegment.DATA_GROUP_0_OFFSET + isFullCapacitySegment *
(FullCapacitySegment.DATA_GROUP_0_OFFSET -
IntermediateCapacitySegment.DATA_GROUP_0_OFFSET) +
groupIndex * (IntermediateCapacitySegment.STRIDE_SIZE_IN_BYTES +
isFullCapacitySegment * (FullCapacitySegment.STRIDE_SIZE_IN_BYTES -
IntermediateCapacitySegment.STRIDE_SIZE_IN_BYTES));
}
/**
* @implNote the implementation of this method depends on how {@link
* FullCapacitySegment#DATA_GROUP_0_OFFSET} is initialized with respect to {@link
* FullCapacitySegment#TAG_GROUP_0_OFFSET} which also aligns with how the corresponding fields
* are initialized in {@link IntermediateCapacitySegment}.
*/
static long tagGroupFromDataGroupOffset(long dataGroupOffset) {
return dataGroupOffset - Long.BYTES;
}
/**
* @implNote the implementation of this method depends on how {@link
* FullCapacitySegment#DATA_GROUP_0_OFFSET} is initialized with respect to {@link
* FullCapacitySegment#TAG_GROUP_0_OFFSET} which also aligns with how the corresponding fields
* are initialized in {@link IntermediateCapacitySegment}.
*/
static long dataGroupFromTagGroupOffset(long tagGroupOffset) {
return tagGroupOffset + Long.BYTES;
}
/**
* Swaps segments' contents, namely:
* - Hash tables (see {@link HashTable})
* - Alloc areas
* - Outbound overflow counts (see {@link
* ContinuousSegment_BitSetAndStateArea#outboundOverflowCountsPerGroup})
* - Bit set parts of bitSetAndStates (see {@link BitSetAndState})
*
* Conceptually, swapping hash tables and alloc areas in InterleavedSegments is more complicated
* than in {@link ContinuousSegments} because the need to preserve "affinity" between hash
* table's groups and alloc indexes of entries stored in the corresponding groups: see {@link
* FullCapacitySegment#allocIndexBoundaryForLocalAllocation}, {@link
* FullCapacitySegment#insertDuringContentsMove}, and the parallel methods in {@link
* IntermediateCapacitySegment}. This method changes the layout of entries in the allocation
* areas of both segments.
*
* This method is placed outside of both {@link FullCapacitySegment} and {@link
* IntermediateCapacitySegment} to not accidentally call a wrong static method (there are
* static methods with same names in both).
*
* @return the updated fullCapacitySegment_bitSetAndState, not yet written into
* fullCapacitySegment; Note that the updated intermediateCapacitySegment_bitSetAndState
* is written into intermediateCapacitySegment's {@link
* ContinuousSegment_BitSetAndStateArea#bitSetAndState} inside this method. Note that
* it's opposite of the contract of the parallel {@link
* ContinuousSegments.SegmentBase#swapContentsDuringSplit} method. It needs to be so
* because fullCapacitySegment's bitSetAndState is expected to be a bulk operation
* placeholder (see {@link BitSetAndState#makeBulkOperationPlaceholderBitSetAndState})
* when this method is called and should remain so.
*
* @implNote this method operates by first copying intermediateCapacitySegment's hash table
* groups and entries to temporary arrays, then copying fullCapacitySegment's contents into
* intermediateCapacitySegment, then copying intermediateCapacitySegment's contents from the
* temporary arrays to fullCapacitySegment. So this method is not garbage-free: it allocates
* temporary arrays. It's possible to swap segments' contents in-place, but it's very
* complicated (requires something like several bit sets stored in long values for tracking
* individual swapping of hash table's slots and alloc indexes in both segments) and doesn't
* make much sense since the probability of calling this method is just 0.7% (see
* [Swap segments] in {@link SmoothieMap#doSplit}) and since intermediate-capacity segments are
* used at all, the garbage produce of the SmoothieMap is much higher already so temporary array
* allocations in this method are an insignificant contribution to the total garbage produce.
*/
@RarelyCalledAmortizedPerSegment
static long swapContentsDuringSplit(SmoothieMap.Segment, ?> fullCapacitySegment,
long fullCapSegment_bitSetAndState,
SmoothieMap.Segment, ?> intermediateCapacitySegment,
long intermediateCapSegment_bitSetAndState) {
FullCapacitySegment fullCapSegment = (FullCapacitySegment) fullCapacitySegment;
IntermediateCapacitySegment intermediateCapSegment =
(IntermediateCapacitySegment) intermediateCapacitySegment;
// Copy contents from intermediateCapSegment into temporary arrays.
long[] intermediateCapSegment_hashTable =
intermediateCapSegment.copyHashTableToArrayDuringSegmentSwap();
Object[] intermediateCapSegment_entries =
intermediateCapSegment.copyEntriesToArrayDuringSegmentSwap();
long intermediateCapSegment_outboundOverflowCountsPerGroup =
intermediateCapSegment.outboundOverflowCountsPerGroup;
// Zero intermediateCapSegment_outboundOverflowCountsPerGroup: this method expects
// intermediateCapacitySegment's outbound overflow counts to be zero for all groups. This is
// because swapContentsDuringSplit() is called from SmoothieMap.doSplit() where
// intermediateCapacitySegment is created privately (so no other thread can possibly modify
// it in parallel) and in the logic of SmoothieMap.doSplit() itself outbound overflow counts
// are not written until the end of the method (after the call to
// swapContentsDuringSplit()).
//
// However, FullCapacitySegment.copyContentsFromArrays() which is called below doesn't
// specialize for this fact for symmetry with
// moveContentsFromFullToIntermediateCapacitySegment() and flexibility wrt. future change.
verifyEqual(intermediateCapSegment_outboundOverflowCountsPerGroup, 0);
// Need to clear intermediateCapSegment's alloc area before copying data from fullCapSegment
// into it because the current size of fullCapSegment is less than
// SEGMENT_INTERMEDIATE_ALLOC_CAPACITY (which is because intermediateCapSegment is full so
// its size is SEGMENT_INTERMEDIATE_ALLOC_CAPACITY, and the total size of fullCapSegment and
// intermediateCapSegment can't be more than SEGMENT_MAX_ALLOC_CAPACITY, and
// SEGMENT_INTERMEDIATE_ALLOC_CAPACITY (32) > SEGMENT_MAX_ALLOC_CAPACITY / 2 (24)) so
// moveContentsFromFullToIntermediateCapacitySegment() won't overwrite all indexes in
// intermediateCapSegment's alloc area.
intermediateCapSegment.clearHashTableAndAllocArea();
moveContentsFromFullToIntermediateCapacitySegment(
fullCapSegment, intermediateCapSegment, intermediateCapSegment_bitSetAndState);
// Need to clear fullCapSegment's alloc area because although the number of entries used to
// reside it is smaller than the number of entries in intermediateCapSegment_entries (see
// the explanation in the comment for intermediateCapSegment.clearHashTableAndAllocArea())
// the capacity of fullCapSegment (SEGMENT_MAX_ALLOC_CAPACITY) allows the old entries to be
// distributed in alloc indexes that are not going to be overwritten with
// intermediateCapSegment_entries.
fullCapSegment.clearHashTableAndAllocArea();
fullCapSegment_bitSetAndState = fullCapSegment.copyContentsFromArrays(
fullCapSegment_bitSetAndState, intermediateCapSegment_hashTable,
intermediateCapSegment_entries,
intermediateCapSegment_outboundOverflowCountsPerGroup);
return fullCapSegment_bitSetAndState;
}
/**
* This method is completely symmetric with {@link
* #moveContentsFromIntermediateToFullCapacitySegment} and has a similar structure with {@link
* FullCapacitySegment#copyContentsFromArrays}. These methods should all be updated in parallel.
*
* This method writes the updated intermediateCapSegment_bitSetAndState into
* intermediateCapSegment's {@link ContinuousSegment_BitSetAndStateArea#bitSetAndState}.
*/
@RarelyCalledAmortizedPerSegment
private static void moveContentsFromFullToIntermediateCapacitySegment(
FullCapacitySegment fullCapSegment, IntermediateCapacitySegment intermediateCapSegment,
long intermediateCapSegment_bitSetAndState) {
intermediateCapSegment_bitSetAndState = clearBitSet(intermediateCapSegment_bitSetAndState);
// Same hash table iteration mode in symmetric methods: the performance considerations of
// branchless vs. byte-by-byte hash table iteration (see the description of
// [Branchless hash table iteration] for details) are not important in this method, but
// since moveContentsFromIntermediateToFullCapacitySegment() takes the branchless approach
// this method follows to preserve the symmetry between these two methods. Similar reasoning
// is applied in [Sticking to [Branchless hash table iteration] in a cold method].
// [Branchless hash table iteration]:
for (long groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
long tagGroup = FullCapacitySegment.readTagGroup(fullCapSegment, groupIndex);
IntermediateCapacitySegment.writeTagGroup(intermediateCapSegment, groupIndex, tagGroup);
long fullCapSegment_dataGroup =
FullCapacitySegment.readDataGroup(fullCapSegment, groupIndex);
for (long bitMask = matchFull(fullCapSegment_dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
int trailingZeros = Long.numberOfTrailingZeros(bitMask);
Object key;
Object value;
// Read key and value from the full-capacity segment.
{
long allocIndex = extractAllocIndex(fullCapSegment_dataGroup, trailingZeros);
long allocOffset = FullCapacitySegment.allocOffset(allocIndex);
key = FullCapacitySegment.readKeyAtOffset(fullCapSegment, allocOffset);
value = FullCapacitySegment.readValueAtOffset(fullCapSegment, allocOffset);
}
int slotIndexWithinGroup = lowestMatchingSlotIndexFromTrailingZeros(trailingZeros);
intermediateCapSegment_bitSetAndState =
intermediateCapSegment.insertDuringContentsMove(
intermediateCapSegment_bitSetAndState, groupIndex,
slotIndexWithinGroup, key, value);
}
intermediateCapSegment.copyOutboundOverflowBitsFrom(
groupIndex, fullCapSegment_dataGroup);
}
intermediateCapSegment.bitSetAndState = intermediateCapSegment_bitSetAndState;
intermediateCapSegment.outboundOverflowCountsPerGroup =
fullCapSegment.outboundOverflowCountsPerGroup;
}
/**
* intermediateCapSegment's bitSetAndState is passed as a parameter to this method because
* intermediateCapSegment.bitSetAndState is already set to bulk operation placeholder value
* before calling this method.
*/
@AmortizedPerSegment
static FullCapacitySegment grow(SmoothieMap.Segment intermediateCapSegment,
long intermediateCapSegment_bitSetAndState, int toAllocCapacity) {
verifyEqual(toAllocCapacity, SEGMENT_MAX_ALLOC_CAPACITY);
FullCapacitySegment fullCapSegment = new FullCapacitySegment<>();
int segmentOrder = segmentOrder(intermediateCapSegment_bitSetAndState);
long fullCapSegment_bitSetAndState =
makeNewBitSetAndState(SEGMENT_MAX_ALLOC_CAPACITY, segmentOrder);
fullCapSegment_bitSetAndState = moveContentsFromIntermediateToFullCapacitySegment(
(IntermediateCapacitySegment) intermediateCapSegment, fullCapSegment,
fullCapSegment_bitSetAndState);
fullCapSegment.bitSetAndState = fullCapSegment_bitSetAndState;
U.storeFence(); // [Safe segment publication]
return fullCapSegment;
}
/**
* This method is completely symmetric with {@link
* #moveContentsFromFullToIntermediateCapacitySegment} and has a similar structure with {@link
* FullCapacitySegment#copyContentsFromArrays}. These methods should all be updated in parallel.
*
* @param fullCapSegment_bitSetAndState must have all bits clear in the bit set, identifying
* that all alloc indexes are clear
* @return the updated fullCapSegment_bitSetAndState, not yet written into fullCapSegment
*/
@AmortizedPerSegment
private static long moveContentsFromIntermediateToFullCapacitySegment(
IntermediateCapacitySegment intermediateCapSegment, FullCapacitySegment fullCapSegment,
long fullCapSegment_bitSetAndState) {
// [Branchless hash table iteration]: TODO discuss
for (long groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
long tagGroup =
IntermediateCapacitySegment.readTagGroup(intermediateCapSegment, groupIndex);
FullCapacitySegment.writeTagGroup(fullCapSegment, groupIndex, tagGroup);
long intermediateCapSegment_dataGroup =
IntermediateCapacitySegment.readDataGroup(intermediateCapSegment, groupIndex);
for (long bitMask = matchFull(intermediateCapSegment_dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
int trailingZeros = Long.numberOfTrailingZeros(bitMask);
Object key;
Object value;
// Read key and value from the intermediate-capacity segment.
{
long allocIndex =
extractAllocIndex(intermediateCapSegment_dataGroup, trailingZeros);
long allocOffset = IntermediateCapacitySegment.allocOffset(allocIndex);
key = IntermediateCapacitySegment.readKeyAtOffset(
intermediateCapSegment, allocOffset);
value = IntermediateCapacitySegment.readValueAtOffset(
intermediateCapSegment, allocOffset);
}
int slotIndexWithinGroup = lowestMatchingSlotIndexFromTrailingZeros(trailingZeros);
fullCapSegment_bitSetAndState =
fullCapSegment.insertDuringContentsMove(
fullCapSegment_bitSetAndState, groupIndex,
slotIndexWithinGroup, key, value);
}
fullCapSegment.copyOutboundOverflowBitsFrom(
groupIndex, intermediateCapSegment_dataGroup);
}
fullCapSegment.outboundOverflowCountsPerGroup =
intermediateCapSegment.outboundOverflowCountsPerGroup;
return fullCapSegment_bitSetAndState;
}
//region FullCapacitySegment's layout classes
static abstract class FullCapacitySegment_AllocationSpaceBeforeGroup0
extends SmoothieMap.Segment {
@SuppressWarnings("unused")
@Nullable Object k0, v0, k1, v1, k2, v2;
}
static abstract class FullCapacitySegment_Group0
extends FullCapacitySegment_AllocationSpaceBeforeGroup0 {
@SuppressWarnings("unused")
private long tagGroup0, dataGroup0;
}
static abstract class FullCapacitySegment_AllocationSpaceBetweenGroups0And1
extends FullCapacitySegment_Group0 {
@SuppressWarnings("unused")
@Nullable Object k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8;
}
static abstract class FullCapacitySegment_Group1
extends FullCapacitySegment_AllocationSpaceBetweenGroups0And1 {
@SuppressWarnings("unused")
private long tagGroup1, dataGroup1;
}
static abstract class FullCapacitySegment_AllocationSpaceBetweenGroups1And2
extends FullCapacitySegment_Group1 {
@SuppressWarnings("unused")
@Nullable Object k9, v9, k10, v10, k11, v11, k12, v12, k13, v13, k14, v14;
}
static abstract class FullCapacitySegment_Group2
extends FullCapacitySegment_AllocationSpaceBetweenGroups1And2 {
@SuppressWarnings("unused")
private long tagGroup2, dataGroup2;
}
static abstract class FullCapacitySegment_AllocationSpaceBetweenGroups2And3
extends FullCapacitySegment_Group2 {
@SuppressWarnings("unused")
@Nullable Object k15, v15, k16, v16, k17, v17, k18, v18, k19, v19, k20, v20;
}
static abstract class FullCapacitySegment_Group3
extends FullCapacitySegment_AllocationSpaceBetweenGroups2And3 {
@SuppressWarnings("unused")
private long tagGroup3, dataGroup3;
}
static abstract class FullCapacitySegment_AllocationSpaceBetweenGroups3And4
extends FullCapacitySegment_Group3 {
@SuppressWarnings("unused")
@Nullable Object k21, v21, k22, v22, k23, v23, k24, v24, k25, v25, k26, v26;
}
static abstract class FullCapacitySegment_Group4
extends FullCapacitySegment_AllocationSpaceBetweenGroups3And4 {
@SuppressWarnings("unused")
private long tagGroup4, dataGroup4;
}
static abstract class FullCapacitySegment_AllocationSpaceBetweenGroups4And5
extends FullCapacitySegment_Group4 {
@SuppressWarnings("unused")
@Nullable Object k27, v27, k28, v28, k29, v29, k30, v30, k31, v31, k32, v32;
}
static abstract class FullCapacitySegment_Group5
extends FullCapacitySegment_AllocationSpaceBetweenGroups4And5 {
@SuppressWarnings("unused")
private long tagGroup5, dataGroup5;
}
static abstract class FullCapacitySegment_AllocationSpaceBetweenGroups5And6
extends FullCapacitySegment_Group5 {
@SuppressWarnings("unused")
@Nullable Object k33, v33, k34, v34, k35, v35, k36, v36, k37, v37, k38, v38;
}
static abstract class FullCapacitySegment_Group6
extends FullCapacitySegment_AllocationSpaceBetweenGroups5And6 {
@SuppressWarnings("unused")
private long tagGroup6, dataGroup6;
}
static abstract class FullCapacitySegment_AllocationSpaceBetweenGroups6And7
extends FullCapacitySegment_Group6 {
@SuppressWarnings("unused")
@Nullable Object k39, v39, k40, v40, k41, v41, k42, v42, k43, v43, k44, v44;
}
static abstract class FullCapacitySegment_Group7
extends FullCapacitySegment_AllocationSpaceBetweenGroups6And7 {
@SuppressWarnings("unused")
private long tagGroup7, dataGroup7;
}
static abstract class FullCapacitySegment_AllocationSpaceAfterGroup7
extends FullCapacitySegment_Group7 {
@SuppressWarnings("unused")
@Nullable Object k45, v45, k46, v46, k47, v47;
}
//endregion
/**
* The memory layout of a full-capacity segment is the following:
* ...td......td......td......td......td......td......td......td...
* Where each dot identifies an allocation index (space for a key and a value), letter 't'
* identifies a tag group, and letter 'd' identifies a data group.
* 48 (= {@link SmoothieMap#SEGMENT_MAX_ALLOC_CAPACITY}) dots (that is, allocation indexes) in
* total.
*/
static class FullCapacitySegment
extends FullCapacitySegment_AllocationSpaceAfterGroup7 {
static final long TAG_GROUP_0_OFFSET;
private static final long DATA_GROUP_0_OFFSET;
static final long STRIDE_SIZE_IN_BYTES;
/**
* = {@link SmoothieMap#SEGMENT_MAX_ALLOC_CAPACITY} / {@link HashTable#HASH_TABLE_GROUPS}
*/
static final int STRIDE_SIZE_IN_ALLOC_INDEXES = 6;
static {
verifyEqual(STRIDE_SIZE_IN_ALLOC_INDEXES,
SEGMENT_MAX_ALLOC_CAPACITY / HASH_TABLE_GROUPS);
}
/**
* Stride 0 begins before actual data starts, at an impossible location "before" {@link
* FullCapacitySegment_AllocationSpaceBeforeGroup0}, to simplify the implementation of
* {@link #allocOffset}.
*/
static final long STRIDE_0_OFFSET;
static final long ALLOC_INDEX_0_OFFSET;
static final long ALLOC_INDEX_45_OFFSET;
/**
* The number of "real" alloc indexes in stride 0, in other words, in {@link
* FullCapacitySegment_AllocationSpaceBeforeGroup0}.
*/
static final int STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES = 3;
/**
* The number of "real" alloc indexes in stride 8, in other words, in {@link
* FullCapacitySegment_AllocationSpaceAfterGroup7}.
*/
static final int STRIDE_8__NUM_ACTUAL_ALLOC_INDEXES = 3;
static {
// IntermediateCapacitySegment has the same initialization block. These blocks should
// be updated in parallel.
TAG_GROUP_0_OFFSET = minInstanceFieldOffset(FullCapacitySegment_Group0.class);
// TODO detect negative field offsets and reverse all offset increments
// tagGroupFromDataGroupOffset() depends on the fact that we lay tag groups before
// data groups.
DATA_GROUP_0_OFFSET = TAG_GROUP_0_OFFSET + Long.BYTES;
long tagGroup1_offset = minInstanceFieldOffset(FullCapacitySegment_Group1.class);
STRIDE_SIZE_IN_BYTES = tagGroup1_offset - TAG_GROUP_0_OFFSET;
long stride1_offset = minInstanceFieldOffset(
FullCapacitySegment_AllocationSpaceBetweenGroups0And1.class);
STRIDE_0_OFFSET = stride1_offset - STRIDE_SIZE_IN_BYTES;
verifyEqual(STRIDE_0_OFFSET,
// Long.BYTES * 2 means tag group and data group.
TAG_GROUP_0_OFFSET - STRIDE_SIZE_IN_BYTES + (Long.BYTES * 2));
ALLOC_INDEX_0_OFFSET = allocOffset(0);
verifyEqual(ALLOC_INDEX_0_OFFSET,
minInstanceFieldOffset(FullCapacitySegment_AllocationSpaceBeforeGroup0.class));
ALLOC_INDEX_45_OFFSET = allocOffset(45);
verifyEqual(ALLOC_INDEX_45_OFFSET,
minInstanceFieldOffset(FullCapacitySegment_AllocationSpaceAfterGroup7.class));
}
@HotPath
private static long tagGroupOffset(long groupIndex) {
return TAG_GROUP_0_OFFSET + (groupIndex * STRIDE_SIZE_IN_BYTES);
}
@HotPath
private static long readTagGroup(Object segment, long groupIndex) {
return U.getLong(segment, tagGroupOffset(groupIndex));
}
private static void writeTagGroup(Object segment, long groupIndex, long tagGroup) {
U.putLong(segment, tagGroupOffset(groupIndex), tagGroup);
}
@HotPath
private static long dataGroupOffset(long groupIndex) {
return DATA_GROUP_0_OFFSET + (groupIndex * STRIDE_SIZE_IN_BYTES);
}
@HotPath
static long readDataGroup(Object segment, long groupIndex) {
return U.getLong(segment, dataGroupOffset(groupIndex));
}
static void writeDataGroup(Object segment, long groupIndex, long dataGroup) {
U.putLong(segment, dataGroupOffset(groupIndex), dataGroup);
}
@HotPath
static void writeTagAndData(
Object segment, long groupIndex, int slotIndexWithinGroup, byte tag, byte data) {
long slotByteOffset = slotByteOffset(slotIndexWithinGroup);
U.putByte(segment, tagGroupOffset(groupIndex) + slotByteOffset, tag);
U.putByte(segment, dataGroupOffset(groupIndex) + slotByteOffset, data);
}
private static void writeData(
Object segment, long groupIndex, int slotIndexWithinGroup, byte data) {
long slotByteOffset = slotByteOffset(slotIndexWithinGroup);
U.putByte(segment, dataGroupOffset(groupIndex) + slotByteOffset, data);
}
/** @see BitSetAndState#freeAllocIndexClosestTo */
static int allocIndexBoundaryForLocalAllocation(int groupIndex) {
return STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES + groupIndex * STRIDE_SIZE_IN_ALLOC_INDEXES;
}
@HotPath
static long allocOffset(long allocIndex) {
// Making allocIndex to be relative to stride 0. [Reusing local variable].
allocIndex += STRIDE_SIZE_IN_ALLOC_INDEXES - STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES;
// Equivalent to allocIndex / STRIDE_SIZE_IN_ALLOC_INDEXES (= 6).
// Replacing division by 3 with multiplication and shift, similarly to what is done in
// doComputeAverageSegmentOrder().
long strideIndex = ((allocIndex) * 2863311531L) >>> 34;
// TODO check if can apply the hack from
// https://lemire.me/blog/2019/02/08/
// faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/
// to speed this up
long allocIndexWithinStride = allocIndex - strideIndex * STRIDE_SIZE_IN_ALLOC_INDEXES;
return STRIDE_0_OFFSET + strideIndex * STRIDE_SIZE_IN_BYTES +
allocIndexWithinStride * ALLOCATION_INDEX_SIZE_IN_BYTES;
}
/**
* Parallel to {@link IntermediateCapacitySegment#writeKeyAndValueAtIndex}. A generalized
* version is {@link SmoothieMap.Segment#writeKeyAndValueAtIndex}.
*/
static void writeKeyAndValueAtIndex(
Object segment, int allocIndex, Object key, Object value) {
long allocOffset = allocOffset((long) allocIndex);
U.putObject(segment, allocOffset, key);
U.putObject(segment, valueOffsetFromAllocOffset(allocOffset), value);
}
/** Specialized version of {@link SmoothieMap.Segment#writeEntry}. */
static void writeEntry(Object segment, K key, byte tag, V value, long groupIndex,
long dataGroup, int slotIndexWithinGroup, int allocIndex) {
byte data = makeData(dataGroup, allocIndex);
writeTagAndData(segment, groupIndex, slotIndexWithinGroup, tag, data);
writeKeyAndValueAtIndex(segment, allocIndex, key, value);
}
/**
* A special method which must only be called from segment contents moving operations:
* {@link IntermediateCapacitySegment#grow} and {@link #copyContentsFromArrays}. It finds
* the best free alloc index for the given groupIndex and inserts a data byte at the given
* slotIndexWithinGroup (with {@link HashTable#DATA__OUTBOUND_OVERFLOW_BIT} set to zero) and
* writes the key and the value at the found alloc index.
*
* This method is parallel to {@link IntermediateCapacitySegment#insertDuringContentsMove}.
*
* @return the updated bitSetAndState
*/
private long insertDuringContentsMove(long bitSetAndState,
long groupIndex, int slotIndexWithinGroup, Object key, Object value) {
// TODO check if specialization of freeAllocIndexClosestTo() makes any difference here
// or JIT compiler scalarizes the SEGMENT_MAX_ALLOC_CAPACITY argument cleanly.
int insertionAllocIndex = freeAllocIndexClosestTo(bitSetAndState,
allocIndexBoundaryForLocalAllocation((int) groupIndex)
, SEGMENT_MAX_ALLOC_CAPACITY);
// Write the new data slot into the segment's hash table.
byte dataToInsert = makeDataWithZeroOutboundOverflowBit(insertionAllocIndex);
writeData(this, groupIndex, slotIndexWithinGroup, dataToInsert);
writeKeyAndValueAtIndex(this, insertionAllocIndex, key, value);
bitSetAndState = setAllocBit(bitSetAndState, insertionAllocIndex);
return bitSetAndState;
}
/** Parallel to {@link IntermediateCapacitySegment#copyOutboundOverflowBitsFrom}. */
private void copyOutboundOverflowBitsFrom(long groupIndex, long fromDataGroup) {
long dataGroup = readDataGroup(this, groupIndex);
dataGroup = copyOutboundOverflowBits(fromDataGroup, dataGroup);
writeDataGroup(this, groupIndex, dataGroup);
}
/**
* This method has the same structure as {@link
* #moveContentsFromFullToIntermediateCapacitySegment}. These two methods should be updated
* in parallel.
*
* This method writes zeros into this segment's {@link
* ContinuousSegment_BitSetAndStateArea#outboundOverflowCountsPerGroup}: see
* [Zero intermediateCapSegment_outboundOverflowCountsPerGroup].
*
* @param entries an array produced by {@link
* IntermediateCapacitySegment#copyEntriesToArrayDuringSegmentSwap} (see the
* structure of the array in the documentation for that method).
* @return the updated bitSetAndState, not yet written into this segment's memory
*/
@RarelyCalledAmortizedPerSegment
private long copyContentsFromArrays(long bitSetAndState, long[] hashTable, Object[] entries,
long outboundOverflowCountsPerGroup) {
bitSetAndState = clearBitSet(bitSetAndState);
for (int groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
long tagGroup = hashTable[groupIndex * 2];
writeTagGroup(this, (long) groupIndex, tagGroup);
long dataGroupToCopy = hashTable[groupIndex * 2 + 1];
for (long bitMask = matchFull(dataGroupToCopy);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
int trailingZeros = Long.numberOfTrailingZeros(bitMask);
Object key;
Object value;
{
int allocIndex = (int) extractAllocIndex(dataGroupToCopy, trailingZeros);
key = entries[allocIndex * 2];
value = entries[allocIndex * 2 + 1];
}
int slotIndexWithinGroup =
lowestMatchingSlotIndexFromTrailingZeros(trailingZeros);
bitSetAndState = insertDuringContentsMove(
bitSetAndState, (long) groupIndex, slotIndexWithinGroup, key, value);
}
copyOutboundOverflowBitsFrom((long) groupIndex, dataGroupToCopy);
}
this.outboundOverflowCountsPerGroup = outboundOverflowCountsPerGroup;
return bitSetAndState;
}
@Override
@RarelyCalledAmortizedPerSegment
void copyEntriesDuringInflate(
SmoothieMap map, SmoothieMap.InflatedSegment intoSegment) {
// Just iterating all alloc indexes until constant SEGMENT_MAX_ALLOC_CAPACITY expecting
// all keys and values to be non-null (otherwise ConcurrentModificationException should
// be thrown from readKeyAtOffset() or readValueAtOffset() calls) because it must be
// true in inflateAndInsert() from where this method is called that this segment is
// full.
// This loop calls expensive allocOffset(). Alternative is to unroll three separate
// loops for stride 0, strides 1-7 and stride 8 respectively. But the method is
// RarelyCalledAmortizedPerSegment so simplicity is valued more than performance.
for (int allocIndex = 0; allocIndex < SEGMENT_MAX_ALLOC_CAPACITY; allocIndex++) {
long allocOffset = allocOffset((long) allocIndex);
K key = readKeyAtOffset(this, allocOffset);
V value = readValueAtOffset(this, allocOffset);
intoSegment.putDuringInflation(map, key, map.keyHashCode(key), value);
}
}
/** Parallel to {@link IntermediateCapacitySegment#clearHashTableAndAllocArea}. */
@Override
void clearHashTableAndAllocArea() {
// Clear the hash table.
setAllDataGroups(EMPTY_DATA_GROUP);
// Leaving garbage in tag groups.
clearAllocArea();
}
/** Parallel to {@link IntermediateCapacitySegment#setAllDataGroups}. */
@Override
void setAllDataGroups(long dataGroup) {
// [Int-indexed loop to avoid a safepoint poll]
for (int groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
writeDataGroup(this, (long) groupIndex, dataGroup);
}
}
/** Parallel to {@link IntermediateCapacitySegment#clearAllocArea}. */
private void clearAllocArea() {
// Having three separate loops instead of a simple loop for allocIndex from 0 to
// SEGMENT_MAX_ALLOC_CAPACITY because allocOffset(allocIndex) operation is expensive.
// ### Clearing partial stride 0.
{
U.putObject(this, ALLOC_INDEX_0_OFFSET, null);
U.putObject(this, valueOffsetFromAllocOffset(ALLOC_INDEX_0_OFFSET), null);
{
long allocIndex1_offset = ALLOC_INDEX_0_OFFSET + ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocIndex1_offset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocIndex1_offset), null);
}
{
long allocIndex2_offset =
ALLOC_INDEX_0_OFFSET + 2 * ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocIndex2_offset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocIndex2_offset), null);
}
}
// ### Iterating full strides 1-7.
// [Int-indexed loop to avoid a safepoint poll]
for (int strideIndex = 1; strideIndex <= NUM_FULL_STRIDES; strideIndex++) {
long strideOffset = STRIDE_0_OFFSET + ((long) strideIndex) * STRIDE_SIZE_IN_BYTES;
// [Int-indexed loop to avoid a safepoint poll]
for (int allocIndexWithinStride = 0;
allocIndexWithinStride < STRIDE_SIZE_IN_ALLOC_INDEXES;
allocIndexWithinStride++) {
long allocOffset = strideOffset +
((long) allocIndexWithinStride) * ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocOffset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocOffset), null);
}
}
// ### Clearing partial stride 8.
{
U.putObject(this, ALLOC_INDEX_45_OFFSET, null);
U.putObject(this, valueOffsetFromAllocOffset(ALLOC_INDEX_45_OFFSET), null);
{
long allocIndex46_offset =
ALLOC_INDEX_45_OFFSET + ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocIndex46_offset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocIndex46_offset), null);
}
{
long allocIndex47_offset =
ALLOC_INDEX_45_OFFSET + 2 * ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocIndex47_offset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocIndex47_offset), null);
}
}
}
/**
* Method cloned in FullCapacitySegment and IntermediateCapacitySegment: this method is an
* exact textual clone of {@link IntermediateCapacitySegment#aggregateStats}. The effective
* (bytecode) difference between these methods is that they call to different static methods
* with the same names defined in FullCapacitySegment and {@link
* IntermediateCapacitySegment} respectively.
*/
@Override
void aggregateStats(SmoothieMap map, OrdinarySegmentStats ordinarySegmentStats) {
ordinarySegmentStats.incrementAggregatedSegments(bitSetAndState);
// Sticking to [Branchless hash table iteration] in a cold method: the performance
// considerations of branchless vs. byte-by-byte hash table iteration (see the
// description of [Branchless hash table iteration] for details) are not important in
// this method, but since performance-critical methods (such as SmoothieMap.doSplit())
// already take the branchless approach other methods such as aggregateStats() follow
// the performance-critical methods to reduce the variability in the codebase, that is,
// to maintain just one mode of hash table iteration. Similar reasoning is applied in
// [Same hash table iteration mode in symmetric methods]
for (long groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
long dataGroup = readDataGroup(this, groupIndex);
int allocIndexBoundaryForGroup =
allocIndexBoundaryForLocalAllocation((int) groupIndex);
for (long bitMask = matchFull(dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
long allocIndex = firstAllocIndex(dataGroup, bitMask);
long allocOffset = allocOffset(allocIndex);
K key = readKeyAtOffset(this, allocOffset);
long hash = map.keyHashCode(key);
long baseGroupIndex = baseGroupIndex(hash);
ordinarySegmentStats.aggregateFullSlot(baseGroupIndex, groupIndex,
map.countCollisionKeyComparisons(this, key, hash),
(int) allocIndex, allocIndexBoundaryForGroup);
}
}
}
/** [Method cloned in FullCapacitySegment and IntermediateCapacitySegment] */
@Override
String debugToString() {
DebugBitSetAndState bitSetAndState = new DebugBitSetAndState(this.bitSetAndState);
StringBuilder sb = new StringBuilder();
sb.append(bitSetAndState).append('\n');
sb.append("Slots:\n");
for (int allocIndex = 0; allocIndex < bitSetAndState.allocCapacity; allocIndex++) {
long allocOffset = allocOffset((long) allocIndex);
Object key = U.getObject(this, allocOffset);
Object value = U.getObject(this, valueOffsetFromAllocOffset(allocOffset));
sb.append(key).append('=').append(value).append('\n');
}
return sb.toString();
}
/** [Method cloned in FullCapacitySegment and IntermediateCapacitySegment] */
@Override
@Nullable DebugHashTableSlot[] debugHashTable(SmoothieMap map) {
@Nullable DebugHashTableSlot[] debugHashTableSlots =
new DebugHashTableSlot[HASH_TABLE_SLOTS];
// [Sticking to [Branchless hash table iteration] in a cold method]
for (long groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
long dataGroup = readDataGroup(this, groupIndex);
long tagGroup = readTagGroup(this, groupIndex);
for (long bitMask = matchFull(dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
int trailingZeros = Long.numberOfTrailingZeros(bitMask);
long allocIndex = extractAllocIndex(dataGroup, trailingZeros);
long valueOffset = valueOffsetFromAllocOffset(allocOffset(allocIndex));
byte tagByte = extractTagByte(tagGroup, trailingZeros);
int dataByte = Byte.toUnsignedInt(
extractDataByte(dataGroup, trailingZeros));
int slotIndexWithinGroup =
lowestMatchingSlotIndexFromTrailingZeros(trailingZeros);
int slotIndex = (int) (groupIndex * GROUP_SLOTS) + slotIndexWithinGroup;
debugHashTableSlots[slotIndex] = new DebugHashTableSlot<>(
map, this, (int) allocIndex, valueOffset, tagByte, dataByte);
}
}
//noinspection unchecked
return debugHashTableSlots;
}
//region specialized bulk methods
/**
* Mirror of {@link SmoothieMap.Segment#hashCode(SmoothieMap)}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
int hashCode(SmoothieMap map) {
int h = 0;
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the hash code computation, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
h += map.keyHashCodeForAggregateHashCodes(key) ^
map.valueHashCodeForAggregateHashCodes(value);
}
return h;
}
/**
* Mirror of {@link SmoothieMap.Segment#keySetHashCode}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
int keySetHashCode(SmoothieMap.KeySet keySet) {
SmoothieMap map = keySet.smoothie;
int h = 0;
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the hash code computation, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
h += map.keyHashCodeForAggregateHashCodes(key);
}
return h;
}
/**
* Mirror of {@link SmoothieMap.Segment#forEach}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void forEach(BiConsumer super K, ? super V> action) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
action.accept(key, value);
}
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachWhile}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean forEachWhile(BiPredicate super K, ? super V> predicate) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the predicate check, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
if (!predicate.test(key, value)) {
return false;
}
}
return true;
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachKey}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void forEachKey(Consumer super K> action) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
action.accept(key);
}
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachKeyWhile}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean forEachKeyWhile(Predicate super K> predicate) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the predicate check, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
if (!predicate.test(key)) {
return false;
}
}
return true;
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachValue}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void forEachValue(Consumer super V> action) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
action.accept(value);
}
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachValueWhile}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean forEachValueWhile(Predicate super V> predicate) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the predicate check, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
if (!predicate.test(value)) {
return false;
}
}
return true;
}
/**
* Mirror of {@link SmoothieMap.Segment#replaceAll}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
writeValueAtOffset(this, iterAllocOffset, function.apply(key, value));
}
}
/**
* Mirror of {@link SmoothieMap.Segment#containsValue}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean containsValue(SmoothieMap map, V queriedValue) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
V internalVal = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the value objects comparison, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
//noinspection ObjectEquality: identity comparision is intended
boolean valuesIdentical = queriedValue == internalVal;
if (valuesIdentical || map.valuesEqual(queriedValue, internalVal)) {
return true;
}
}
return false;
}
/**
* Mirror of {@link SmoothieMap.Segment#removeIf}.
*
* Unlike other similar methods above belonging to the category
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment], this method is
* very similar but not an exact clone of {@link IntermediateCapacitySegment#removeIf}:
* isFullCapacitySegment local variable is initialized differently and there is a
* difference in comments.
*/
@Override
int removeIf(
SmoothieMap map, BiPredicate super K, ? super V> filter, int modCount) {
// TODO update according to other bulk methods above
long bitSetAndState = this.bitSetAndState;
int initialModCount = modCount;
try {
// [Branchless hash table iteration in removeIf()]
// [Int-indexed loop to avoid a safepoint poll]
for (int iterGroupIndex = 0; iterGroupIndex < HASH_TABLE_GROUPS; iterGroupIndex++) {
long iterDataGroupOffset = dataGroupOffset((long) iterGroupIndex);
long dataGroup = readDataGroupAtOffset(this, iterDataGroupOffset);
groupIteration:
for (long bitMask = matchFull(dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
// [Inlined lowestMatchingSlotIndex]
int trailingZeros = Long.numberOfTrailingZeros(bitMask);
long allocIndex = extractAllocIndex(dataGroup, trailingZeros);
long allocOffset = allocOffset(allocIndex);
K key = readKeyAtOffset(this, allocOffset);
V value = readValueAtOffset(this, allocOffset);
if (!filter.test(key, value)) {
continue groupIteration;
}
// TODO use hosted overflow mask (when implemented; inspired by
// hostedOverflowCounts in F14) to avoid a potentially expensive
// keyHashCode() call here.
long baseGroupIndex = baseGroupIndex(map.keyHashCode(key));
long outboundOverflowCount_perGroupDecrements =
computeOutboundOverflowCount_perGroupChanges(
baseGroupIndex, (long) iterGroupIndex);
int isFullCapacitySegment = 1;
// TODO research if it's possible to do something better than just call
// removeAtSlotNoShrink() in a loop, which may result in calling expensive
// operations a lot of times if nearly all entries are removed from the
// segment during this loop.
bitSetAndState = map.removeAtSlotNoShrink(bitSetAndState, this,
// Not specializing removeAtSlotNoShrink(): it doesn't provide much
// performance benefit to specialize removeAtSlotNoShrink() for
// full-capacity segments because isFullCapacitySegment is rarely
// used inside that method, while having a copy of
// removeAtSlotNoShrink() is an additional maintenance burden. JVM's
// code cache and instruction cache are unlikely to be important
// performance factors here because this removeAtSlotNoShrink() call
// is expected to be inlined into this method.
isFullCapacitySegment,
outboundOverflowCount_perGroupDecrements, iterDataGroupOffset,
setSlotEmpty(dataGroup, trailingZeros), allocIndex, allocOffset);
// Matches the modCount field increment performed in removeAtSlotNoShrink().
modCount++;
}
}
} finally {
// [Writing bitSetAndState in a finally block]
if (modCount != initialModCount) {
this.bitSetAndState = bitSetAndState;
}
}
return modCount;
}
//endregion
}
//region IntermediateCapacitySegment's layout classes
static abstract class IntermediateCapacitySegment_AllocationSpaceBeforeGroup0
extends SmoothieMap.Segment {
@SuppressWarnings("unused")
private @Nullable Object k0, v0, k1, v1;
}
static abstract class IntermediateCapacitySegment_Group0
extends IntermediateCapacitySegment_AllocationSpaceBeforeGroup0 {
@SuppressWarnings("unused")
private long tagGroup0, dataGroup0;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceBetweenGroups0And1
extends IntermediateCapacitySegment_Group0 {
@SuppressWarnings("unused")
private @Nullable Object k2, v2, k3, v3, k4, v4, k5, v5;
}
static abstract class IntermediateCapacitySegment_Group1
extends IntermediateCapacitySegment_AllocationSpaceBetweenGroups0And1 {
@SuppressWarnings("unused")
private long tagGroup1, dataGroup1;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceBetweenGroups1And2
extends IntermediateCapacitySegment_Group1 {
@SuppressWarnings("unused")
private @Nullable Object k6, v6, k7, v7, k8, v8, k9, v9;
}
static abstract class IntermediateCapacitySegment_Group2
extends IntermediateCapacitySegment_AllocationSpaceBetweenGroups1And2 {
@SuppressWarnings("unused")
private long tagGroup2, dataGroup2;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceBetweenGroups2And3
extends IntermediateCapacitySegment_Group2 {
@SuppressWarnings("unused")
private @Nullable Object k10, v10, k11, v11, k12, v12, k13, v13;
}
static abstract class IntermediateCapacitySegment_Group3
extends IntermediateCapacitySegment_AllocationSpaceBetweenGroups2And3 {
@SuppressWarnings("unused")
private long tagGroup3, dataGroup3;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceBetweenGroups3And4
extends IntermediateCapacitySegment_Group3 {
@SuppressWarnings("unused")
private @Nullable Object k14, v14, k15, v15, k16, v16, k17, v17;
}
static abstract class IntermediateCapacitySegment_Group4
extends IntermediateCapacitySegment_AllocationSpaceBetweenGroups3And4 {
@SuppressWarnings("unused")
private long tagGroup4, dataGroup4;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceBetweenGroups4And5
extends IntermediateCapacitySegment_Group4 {
@SuppressWarnings("unused")
private @Nullable Object k18, v18, k19, v19, k20, v20, k21, v21;
}
static abstract class IntermediateCapacitySegment_Group5
extends IntermediateCapacitySegment_AllocationSpaceBetweenGroups4And5 {
@SuppressWarnings("unused")
private long tagGroup5, dataGroup5;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceBetweenGroups5And6
extends IntermediateCapacitySegment_Group5 {
@SuppressWarnings("unused")
private @Nullable Object k22, v22, k23, v23, k24, v24, k25, v25;
}
static abstract class IntermediateCapacitySegment_Group6
extends IntermediateCapacitySegment_AllocationSpaceBetweenGroups5And6 {
@SuppressWarnings("unused")
private long tagGroup6, dataGroup6;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceBetweenGroups6And7
extends IntermediateCapacitySegment_Group6 {
@SuppressWarnings("unused")
private @Nullable Object k26, v26, k27, v27, k28, v28, k29, v29;
}
static abstract class IntermediateCapacitySegment_Group7
extends IntermediateCapacitySegment_AllocationSpaceBetweenGroups6And7 {
@SuppressWarnings("unused")
private long tagGroup7, dataGroup7;
}
static abstract class IntermediateCapacitySegment_AllocationSpaceAfterGroup7
extends IntermediateCapacitySegment_Group7 {
@SuppressWarnings("unused")
private @Nullable Object k30, v30, k31, v31;
}
//endregion
/**
* The memory layout of an intermediate-capacity segment is the following:
* ..td....td....td....td....td....td....td....td..
* Where each dot identifies an allocation index (space for a key and a value), letter 't'
* identifies a tag group, and letter 'd' identifies a data group. 32 (=
* {@link SmoothieMap#SEGMENT_INTERMEDIATE_ALLOC_CAPACITY}) dots (that is, allocation indexes)
* in total.
*/
static class IntermediateCapacitySegment
extends IntermediateCapacitySegment_AllocationSpaceAfterGroup7 {
static final long TAG_GROUP_0_OFFSET;
private static final long DATA_GROUP_0_OFFSET;
static final long STRIDE_SIZE_IN_BYTES;
/**
* = {@link SmoothieMap#SEGMENT_INTERMEDIATE_ALLOC_CAPACITY} /
* {@link HashTable#HASH_TABLE_GROUPS}
*/
static final int STRIDE_SIZE_IN_ALLOC_INDEXES = 4;
/** = numberOfTrailingZeros({@link #STRIDE_SIZE_IN_ALLOC_INDEXES}) */
@CompileTimeConstant
static final int STRIDE_SIZE_IN_ALLOC_INDEXES__SHIFT = 2;
static final int STRIDE_SIZE_IN_ALLOC_INDEXES__MASK = STRIDE_SIZE_IN_ALLOC_INDEXES - 1;
static {
verifyEqual(STRIDE_SIZE_IN_ALLOC_INDEXES,
SEGMENT_INTERMEDIATE_ALLOC_CAPACITY / HASH_TABLE_GROUPS);
verifyEqual(STRIDE_SIZE_IN_ALLOC_INDEXES__SHIFT,
Integer.numberOfTrailingZeros(STRIDE_SIZE_IN_ALLOC_INDEXES));
}
/**
* Stride 0 begins before actual data starts, at an impossible location "before" {@link
* IntermediateCapacitySegment_AllocationSpaceBeforeGroup0}, to simplify the implementation
* of {@link #allocOffset}.
*/
static final long STRIDE_0_OFFSET;
static final long ALLOC_INDEX_0_OFFSET;
static final long ALLOC_INDEX_30_OFFSET;
/**
* The number of "real" alloc indexes in stride 0, in other words, in {@link
* IntermediateCapacitySegment_AllocationSpaceBeforeGroup0}.
*/
static final int STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES = 2;
/**
* The number of "real" alloc indexes in stride 8, in other words, in {@link
* IntermediateCapacitySegment_AllocationSpaceAfterGroup7}.
*/
static final int STRIDE_8__NUM_ACTUAL_ALLOC_INDEXES = 2;
static {
// FullCapacitySegment has the same initialization block. These blocks should be updated
// in parallel.
TAG_GROUP_0_OFFSET = minInstanceFieldOffset(IntermediateCapacitySegment_Group0.class);
// tagGroupFromDataGroupOffset() depends on the fact that we lay tag groups before
// data groups.
// TODO detect negative field offsets and reverse all offset increments
DATA_GROUP_0_OFFSET = TAG_GROUP_0_OFFSET + Long.BYTES;
long tagGroup1_offset =
minInstanceFieldOffset(IntermediateCapacitySegment_Group1.class);
STRIDE_SIZE_IN_BYTES = tagGroup1_offset - TAG_GROUP_0_OFFSET;
long stride1_offset = minInstanceFieldOffset(
IntermediateCapacitySegment_AllocationSpaceBetweenGroups0And1.class);
STRIDE_0_OFFSET = stride1_offset - STRIDE_SIZE_IN_BYTES;
verifyEqual(STRIDE_0_OFFSET,
// Long.BYTES * 2 means tag group and data group.
TAG_GROUP_0_OFFSET - STRIDE_SIZE_IN_BYTES + (Long.BYTES * 2));
ALLOC_INDEX_0_OFFSET = allocOffset(0);
long allocIndex0_offset_obtainedFromFields = minInstanceFieldOffset(
IntermediateCapacitySegment_AllocationSpaceBeforeGroup0.class);
verifyEqual(ALLOC_INDEX_0_OFFSET, allocIndex0_offset_obtainedFromFields);
ALLOC_INDEX_30_OFFSET = allocOffset(30);
long allocIndex30_offset_obtainedFromFields = minInstanceFieldOffset(
IntermediateCapacitySegment_AllocationSpaceAfterGroup7.class);
verifyEqual(ALLOC_INDEX_30_OFFSET, allocIndex30_offset_obtainedFromFields);
}
@HotPath
private static long tagGroupOffset(long groupIndex) {
return TAG_GROUP_0_OFFSET + (groupIndex * STRIDE_SIZE_IN_BYTES);
}
@HotPath
private static long readTagGroup(Object segment, long groupIndex) {
return U.getLong(segment, tagGroupOffset(groupIndex));
}
private static void writeTagGroup(Object segment, long groupIndex, long tagGroup) {
U.putLong(segment, tagGroupOffset(groupIndex), tagGroup);
}
@HotPath
private static long dataGroupOffset(long groupIndex) {
return DATA_GROUP_0_OFFSET + (groupIndex * STRIDE_SIZE_IN_BYTES);
}
@HotPath
private static long readDataGroup(Object segment, long groupIndex) {
return U.getLong(segment, dataGroupOffset(groupIndex));
}
private static void writeDataGroup(Object segment, long groupIndex, long dataGroup) {
U.putLong(segment, dataGroupOffset(groupIndex), dataGroup);
}
private static void writeData(
Object segment, long groupIndex, int slotIndexWithinGroup, byte data) {
long slotByteOffset = slotByteOffset(slotIndexWithinGroup);
U.putByte(segment, dataGroupOffset(groupIndex) + slotByteOffset, data);
}
/** @see BitSetAndState#freeAllocIndexClosestTo */
private static int allocIndexBoundaryForLocalAllocation(int groupIndex) {
return STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES + groupIndex * STRIDE_SIZE_IN_ALLOC_INDEXES;
}
@HotPath
private static long allocOffset(long allocIndex) {
// Making allocIndex to be relative to stride 0. [Reusing local variable].
allocIndex += STRIDE_SIZE_IN_ALLOC_INDEXES - STRIDE_0__NUM_ACTUAL_ALLOC_INDEXES;
long strideIndex = allocIndex >>> STRIDE_SIZE_IN_ALLOC_INDEXES__SHIFT;
long allocIndexWithinStride = allocIndex & STRIDE_SIZE_IN_ALLOC_INDEXES__MASK;
return STRIDE_0_OFFSET + strideIndex * STRIDE_SIZE_IN_BYTES +
allocIndexWithinStride * ALLOCATION_INDEX_SIZE_IN_BYTES;
}
/**
* Parallel to {@link FullCapacitySegment#writeKeyAndValueAtIndex}. A generalized version is
* {@link SmoothieMap.Segment#writeKeyAndValueAtIndex}.
*/
static void writeKeyAndValueAtIndex(
Object segment, int allocIndex, Object key, Object value) {
long allocOffset = allocOffset((long) allocIndex);
U.putObject(segment, allocOffset, key);
U.putObject(segment, valueOffsetFromAllocOffset(allocOffset), value);
}
/**
* A special method which must only be called from {@link #swapContentsDuringSplit}. It
* finds the best free alloc index for the given groupIndex and inserts a data byte at the
* given slotIndexWithinGroup (with {@link HashTable#DATA__OUTBOUND_OVERFLOW_BIT} set to
* zero) and writes the key and the value at the found alloc index.
*
* This method is parallel to {@link FullCapacitySegment#insertDuringContentsMove}.
*
* @return the updated bitSetAndState
*/
private long insertDuringContentsMove(long bitSetAndState,
long groupIndex, int slotIndexWithinGroup, Object key, Object value) {
// TODO check if specialization of freeAllocIndexClosestTo() makes any difference here
// or JIT compiler scalarizes the SEGMENT_INTERMEDIATE_ALLOC_CAPACITY argument cleanly.
int insertionAllocIndex = freeAllocIndexClosestTo(bitSetAndState,
allocIndexBoundaryForLocalAllocation((int) groupIndex)
, SEGMENT_INTERMEDIATE_ALLOC_CAPACITY
);
if (insertionAllocIndex >= SEGMENT_INTERMEDIATE_ALLOC_CAPACITY) {
// Can happen if entries are inserted into fullCapacitySegment (see the code of
// swapContentsDuringSplit()) concurrently with doSplit(), including concurrently
// with swapContentsDuringSplit() which is called from doSplit().
throw new ConcurrentModificationException();
}
// Write the new data slot into the segment's hash table.
byte dataToInsert = makeDataWithZeroOutboundOverflowBit(insertionAllocIndex);
writeData(this, groupIndex, slotIndexWithinGroup, dataToInsert);
writeKeyAndValueAtIndex(this, insertionAllocIndex, key, value);
bitSetAndState = setAllocBit(bitSetAndState, insertionAllocIndex);
return bitSetAndState;
}
/** Parallel to {@link FullCapacitySegment#copyOutboundOverflowBitsFrom}. */
private void copyOutboundOverflowBitsFrom(long groupIndex, long fromDataGroup) {
long dataGroup = readDataGroup(this, groupIndex);
dataGroup = copyOutboundOverflowBits(fromDataGroup, dataGroup);
writeDataGroup(this, groupIndex, dataGroup);
}
/**
* This method has the same structure as {@link FullCapacitySegment#copyEntriesDuringInflate
* }. These methods should be changed in parallel.
*
* @return an array of {@link SmoothieMap#SEGMENT_INTERMEDIATE_ALLOC_CAPACITY} * 2 of the
* form [k0, v0, k1, v1, ...] where 'kX' and 'vX' means "key (or value) from the alloc index
* X in this segment".
*/
@RarelyCalledAmortizedPerSegment
private Object[] copyEntriesToArrayDuringSegmentSwap() {
// Just iterating all alloc indexes until constant SEGMENT_INTERMEDIATE_ALLOC_CAPACITY
// expecting all keys and values to be non-null (otherwise
// ConcurrentModificationException should be thrown from readKeyAtOffset() or
// readValueAtOffset() calls) because it must be true in swapContentsDuringSplit() from
// where this method is called that this segment is full.
Object[] entries = new Object[SEGMENT_INTERMEDIATE_ALLOC_CAPACITY * 2];
int entriesIndex = 0;
// This loop calls expensive allocOffset(). Alternative is to unroll three separate
// loops for stride 0, strides 1-7 and stride 8 respectively. But the method is
// RarelyCalledAmortizedPerSegment so simplicity is valued more than performance.
for (int allocIndex = 0; allocIndex < SEGMENT_INTERMEDIATE_ALLOC_CAPACITY;
allocIndex++) {
long allocOffset = allocOffset((long) allocIndex);
Object key = readKeyAtOffset(this, allocOffset);
Object value = readValueAtOffset(this, allocOffset);
entries[entriesIndex++] = key;
entries[entriesIndex++] = value;
}
return entries;
}
@RarelyCalledAmortizedPerSegment
private long[] copyHashTableToArrayDuringSegmentSwap() {
long[] hashTable = new long[HASH_TABLE_GROUPS * 2];
int hashTableIndex = 0;
for (long groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
hashTable[hashTableIndex++] = readTagGroup(this, groupIndex);
hashTable[hashTableIndex++] = readDataGroup(this, groupIndex);
}
return hashTable;
}
/** Parallel to {@link FullCapacitySegment#clearHashTableAndAllocArea}. */
@Override
void clearHashTableAndAllocArea() {
// Clear the hash table.
setAllDataGroups(EMPTY_DATA_GROUP);
// Leaving garbage in tag groups.
clearAllocArea();
}
/** Parallel to {@link FullCapacitySegment#setAllDataGroups}. */
@Override
void setAllDataGroups(long dataGroup) {
// [Int-indexed loop to avoid a safepoint poll]
for (int groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
writeDataGroup(this, (long) groupIndex, dataGroup);
}
}
/** Parallel to {@link FullCapacitySegment#clearAllocArea}. */
private void clearAllocArea() {
// Having three separate loops instead of a simple loop for allocIndex from 0 to
// SEGMENT_INTERMEDIATE_ALLOC_CAPACITY because allocOffset(allocIndex) operation is
// expensive.
// ### Clearing partial stride 0.
{
U.putObject(this, ALLOC_INDEX_0_OFFSET, null);
U.putObject(this, valueOffsetFromAllocOffset(ALLOC_INDEX_0_OFFSET), null);
long allocIndex1_offset = ALLOC_INDEX_0_OFFSET + ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocIndex1_offset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocIndex1_offset), null);
}
// ### Iterating full strides 1-7.
// [Int-indexed loop to avoid a safepoint poll]
for (int strideIndex = 1; strideIndex <= NUM_FULL_STRIDES; strideIndex++) {
long strideOffset = STRIDE_0_OFFSET + ((long) strideIndex) * STRIDE_SIZE_IN_BYTES;
// [Int-indexed loop to avoid a safepoint poll]
for (int allocIndexWithinStride = 0;
allocIndexWithinStride < STRIDE_SIZE_IN_ALLOC_INDEXES;
allocIndexWithinStride++) {
long allocOffset = strideOffset +
((long) allocIndexWithinStride) * ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocOffset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocOffset), null);
}
}
// ### Clearing partial stride 8.
{
U.putObject(this, ALLOC_INDEX_30_OFFSET, null);
U.putObject(this, valueOffsetFromAllocOffset(ALLOC_INDEX_30_OFFSET), null);
long allocIndex31_offset = ALLOC_INDEX_30_OFFSET + ALLOCATION_INDEX_SIZE_IN_BYTES;
U.putObject(this, allocIndex31_offset, null);
U.putObject(this, valueOffsetFromAllocOffset(allocIndex31_offset), null);
}
}
@Override
void copyEntriesDuringInflate(
SmoothieMap map, SmoothieMap.InflatedSegment intoSegment) {
throw new UnsupportedOperationException(
"copyEntriesDuringInflate should be called only on full-capacity segments");
}
/** [Method cloned in FullCapacitySegment and IntermediateCapacitySegment] */
@Override
void aggregateStats(SmoothieMap map, OrdinarySegmentStats ordinarySegmentStats) {
ordinarySegmentStats.incrementAggregatedSegments(bitSetAndState);
// [Sticking to [Branchless hash table iteration] in a cold method]
for (long groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
long dataGroup = readDataGroup(this, groupIndex);
int allocIndexBoundaryForGroup =
allocIndexBoundaryForLocalAllocation((int) groupIndex);
for (long bitMask = matchFull(dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
long allocIndex = firstAllocIndex(dataGroup, bitMask);
long allocOffset = allocOffset(allocIndex);
K key = readKeyAtOffset(this, allocOffset);
long hash = map.keyHashCode(key);
long baseGroupIndex = baseGroupIndex(hash);
ordinarySegmentStats.aggregateFullSlot(baseGroupIndex, groupIndex,
map.countCollisionKeyComparisons(this, key, hash),
(int) allocIndex, allocIndexBoundaryForGroup);
}
}
}
/** [Method cloned in FullCapacitySegment and IntermediateCapacitySegment] */
@Override
String debugToString() {
DebugBitSetAndState bitSetAndState = new DebugBitSetAndState(this.bitSetAndState);
StringBuilder sb = new StringBuilder();
sb.append(bitSetAndState).append('\n');
sb.append("Slots:\n");
for (int allocIndex = 0; allocIndex < bitSetAndState.allocCapacity; allocIndex++) {
long allocOffset = allocOffset((long) allocIndex);
Object key = U.getObject(this, allocOffset);
Object value = U.getObject(this, valueOffsetFromAllocOffset(allocOffset));
sb.append(key).append('=').append(value).append('\n');
}
return sb.toString();
}
/** [Method cloned in FullCapacitySegment and IntermediateCapacitySegment] */
@Override
@Nullable DebugHashTableSlot[] debugHashTable(SmoothieMap map) {
@Nullable DebugHashTableSlot[] debugHashTableSlots =
new DebugHashTableSlot[HASH_TABLE_SLOTS];
// [Sticking to [Branchless hash table iteration] in a cold method]
for (long groupIndex = 0; groupIndex < HASH_TABLE_GROUPS; groupIndex++) {
long dataGroup = readDataGroup(this, groupIndex);
long tagGroup = readTagGroup(this, groupIndex);
for (long bitMask = matchFull(dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
int trailingZeros = Long.numberOfTrailingZeros(bitMask);
long allocIndex = extractAllocIndex(dataGroup, trailingZeros);
long valueOffset = valueOffsetFromAllocOffset(allocOffset(allocIndex));
byte tagByte = extractTagByte(tagGroup, trailingZeros);
int dataByte = Byte.toUnsignedInt(
extractDataByte(dataGroup, trailingZeros));
int slotIndexWithinGroup =
lowestMatchingSlotIndexFromTrailingZeros(trailingZeros);
int slotIndex = (int) (groupIndex * GROUP_SLOTS) + slotIndexWithinGroup;
debugHashTableSlots[slotIndex] = new DebugHashTableSlot<>(
map, this, (int) allocIndex, valueOffset, tagByte, dataByte);
}
}
//noinspection unchecked
return debugHashTableSlots;
}
//region specialized bulk methods
/**
* Mirror of {@link SmoothieMap.Segment#hashCode(SmoothieMap)}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
int hashCode(SmoothieMap map) {
int h = 0;
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the hash code computation, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
h += map.keyHashCodeForAggregateHashCodes(key) ^
map.valueHashCodeForAggregateHashCodes(value);
}
return h;
}
/**
* Mirror of {@link SmoothieMap.Segment#keySetHashCode}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
int keySetHashCode(SmoothieMap.KeySet keySet) {
SmoothieMap map = keySet.smoothie;
int h = 0;
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the hash code computation, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
h += map.keyHashCodeForAggregateHashCodes(key);
}
return h;
}
/**
* Mirror of {@link SmoothieMap.Segment#forEach}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void forEach(BiConsumer super K, ? super V> action) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
action.accept(key, value);
}
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachWhile}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean forEachWhile(BiPredicate super K, ? super V> predicate) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the predicate check, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
if (!predicate.test(key, value)) {
return false;
}
}
return true;
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachKey}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void forEachKey(Consumer super K> action) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
action.accept(key);
}
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachKeyWhile}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean forEachKeyWhile(Predicate super K> predicate) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the predicate check, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
if (!predicate.test(key)) {
return false;
}
}
return true;
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachValue}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void forEachValue(Consumer super V> action) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
action.accept(value);
}
}
/**
* Mirror of {@link SmoothieMap.Segment#forEachValueWhile}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean forEachValueWhile(Predicate super V> predicate) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the predicate check, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
if (!predicate.test(value)) {
return false;
}
}
return true;
}
/**
* Mirror of {@link SmoothieMap.Segment#replaceAll}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
K key = readKeyAtOffset(this, iterAllocOffset);
V value = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the action, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
writeValueAtOffset(this, iterAllocOffset, function.apply(key, value));
}
}
/**
* Mirror of {@link SmoothieMap.Segment#containsValue}.
*
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment]
*/
@Override
boolean containsValue(SmoothieMap map, V queriedValue) {
long bitSet = extractBitSetForIteration(bitSetAndState);
// [Iteration in bulk segment methods]
for (int iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1,
iterAllocIndex = Long.SIZE;
(iterAllocIndex -= iterAllocIndexStep) >= 0;) {
long iterAllocOffset = allocOffset((long) iterAllocIndex);
V internalVal = readValueAtOffset(this, iterAllocOffset);
// TODO check what is better - these two statements before or after
// the value objects comparison, or one before and one after, or both after?
bitSet = bitSet << iterAllocIndexStep;
iterAllocIndexStep = Long.numberOfLeadingZeros(bitSet) + 1;
//noinspection ObjectEquality: identity comparision is intended
boolean valuesIdentical = queriedValue == internalVal;
if (valuesIdentical || map.valuesEqual(queriedValue, internalVal)) {
return true;
}
}
return false;
}
/**
* Mirror of {@link SmoothieMap.Segment#removeIf}.
*
* Unlike other similar methods above belonging to the category
* [Method cloned in FullCapacitySegment and IntermediateCapacitySegment], this method is
* very similar but not an exact clone of {@link FullCapacitySegment#removeIf}:
* isFullCapacitySegment local variable is initialized differently and there is a
* difference in comments.
*/
@Override
int removeIf(
SmoothieMap map, BiPredicate super K, ? super V> filter, int modCount) {
long bitSetAndState = this.bitSetAndState;
int initialModCount = modCount;
try {
// [Branchless hash table iteration in removeIf()]
// [Int-indexed loop to avoid a safepoint poll]
for (int iterGroupIndex = 0; iterGroupIndex < HASH_TABLE_GROUPS; iterGroupIndex++) {
long iterDataGroupOffset = dataGroupOffset((long) iterGroupIndex);
long dataGroup = readDataGroupAtOffset(this, iterDataGroupOffset);
groupIteration:
for (long bitMask = matchFull(dataGroup);
bitMask != 0L;
bitMask = clearLowestSetBit(bitMask)) {
// [Inlined lowestMatchingSlotIndex]
int trailingZeros = Long.numberOfTrailingZeros(bitMask);
long allocIndex = extractAllocIndex(dataGroup, trailingZeros);
long allocOffset = allocOffset(allocIndex);
K key = readKeyAtOffset(this, allocOffset);
V value = readValueAtOffset(this, allocOffset);
if (!filter.test(key, value)) {
continue groupIteration;
}
// TODO use hosted overflow mask (when implemented; inspired by
// hostedOverflowCounts in F14) to avoid a potentially expensive
// keyHashCode() call here.
long baseGroupIndex = baseGroupIndex(map.keyHashCode(key));
long outboundOverflowCount_perGroupDecrements =
computeOutboundOverflowCount_perGroupChanges(
baseGroupIndex, (long) iterGroupIndex);
int isFullCapacitySegment = 0;
// TODO research if it's possible to do something better than just call
// removeAtSlotNoShrink() in a loop, which may result in calling expensive
// operations a lot of times if nearly all entries are removed from the
// segment during this loop.
bitSetAndState = map.removeAtSlotNoShrink(bitSetAndState, this,
// [Not specializing removeAtSlotNoShrink()]
isFullCapacitySegment,
outboundOverflowCount_perGroupDecrements, iterDataGroupOffset,
setSlotEmpty(dataGroup, trailingZeros), allocIndex, allocOffset);
// Matches the modCount field increment performed in removeAtSlotNoShrink().
modCount++;
}
}
} finally {
// [Writing bitSetAndState in a finally block]
if (modCount != initialModCount) {
this.bitSetAndState = bitSetAndState;
}
}
return modCount;
}
//endregion
}
static SmoothieMap.Segment allocateNewSegmentWithoutSettingBitSetAndSet(
int allocCapacity) {
@SuppressWarnings("UnnecessaryLocalVariable")
SmoothieMap.Segment segment = allocateSegment(allocCapacity);
// Since HashTable.EMPTY_DATA_GROUP == 0 no additional hash table initialization is needed.
return segment;
}
static SmoothieMap.Segment createNewSegment(int allocCapacity, int segmentOrder) {
SmoothieMap.Segment segment =
allocateNewSegmentWithoutSettingBitSetAndSet(allocCapacity);
segment.bitSetAndState = makeNewBitSetAndState(allocCapacity, segmentOrder);
// Safe segment publication: ensure racy readers always see correctly initialized
// bitSetAndState. It's hard to prove that no races are possible that could lead to
// a JVM crash or memory corruption due to access to an alloc index beyond the alloc
// capacity of the segment, especially considering the possibility of non-atomic
// bitSetAndState access on 32-bit platforms.
U.storeFence();
return segment;
}
private static SmoothieMap.Segment allocateSegment(int allocCapacity) {
if (allocCapacity == SEGMENT_MAX_ALLOC_CAPACITY) {
return new FullCapacitySegment<>();
} else if (allocCapacity == SEGMENT_INTERMEDIATE_ALLOC_CAPACITY) {
return new IntermediateCapacitySegment<>();
} else {
// More capacities than just SEGMENT_MAX_ALLOC_CAPACITY = 48 and
// SEGMENT_INTERMEDIATE_ALLOC_CAPACITY = 32 could be supported: for example, 40 and 24.
// Also, capacities near the multiples of 8 (e. g. +2/-2) could also be supported if
// some asymmetry of alloc indexes around groups is tolerated (see the comment for
// SEGMENT_INTERMEDIATE_ALLOC_CAPACITY).
throw new AssertionError("Interleaved segments cannot have arbitrary capacity");
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy