io.timeandspace.smoothie.InterleavedSegment_BitSetAndStateArea 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 java.util.ConcurrentModificationException;
import static io.timeandspace.smoothie.BitSetAndState.isBulkOperationPlaceholderBitSetAndState;
import static io.timeandspace.smoothie.BitSetAndState.makeBulkOperationPlaceholderBitSetAndState;
import static io.timeandspace.smoothie.UnsafeUtils.U;
import static io.timeandspace.smoothie.InterleavedSegments.FullCapacitySegment.readDataGroup;
import static io.timeandspace.smoothie.InterleavedSegments.FullCapacitySegment.writeDataGroup;
abstract class InterleavedSegment_BitSetAndStateArea
extends AbstractSegment
{
/** @see BitSetAndState */
long bitSetAndState;
/**
* Each byte of this long value holds an "outbound overflow count" for a {@link HashTable}'s
* group with the same index. Outbound overflow count is the number of entries that would have
* been placed in in a group unless it was full (all group's {@link HashTable#GROUP_SLOTS} are
* full) when the entry was inserted into the table.
*
* In F14, the outbound overflow counts are kept right within hash table's "chunks" (equivalent
* of {@link HashTable}'s groups in SmoothieMap):
* https://github.com/facebook/folly/blob/436efbfed/folly/container/detail/F14Table.h#L360-L364
*
* In SmoothieMap, the outbound overflow counts are kept separately from the groups in
* BitSetAndStateArea for the following reasons:
*
* - It allows to avoid repetitive probing of the hash table's groups during all modification
* operations because in all of them it's unknown whether the entry will be actually inserted,
* or deleted, or no structural modification will happen to the hash table until the end of the
* search. (Note: this is currently implemented only in operations like {@link
* SmoothieMap#removeImpl}, but not {@link SmoothieMap#putImpl}: see [Find empty slot] in the
* latter. But when SmoothieMap is specialized for no-removals case, this can also be
* implemented for insertions.)
*
* - When a modification operation does require a structural modification of the hash table
* (that is, an entry is inserted or deleted), separate outbound overflow counts possibly
* reduce memory write amplification because the cache lines containing the hash table's groups
* are not updated when outbound overflow counts are changed between 1 and 2, 2 and 3, etc.
* The dataGroups are only changed when the count changes between 0 and 1: {@link
* HashTable#DATA__OUTBOUND_OVERFLOW_BIT} should be set or unset in all slots in a group (see
* {@link OutboundOverflowCounts#incrementOutboundOverflowCountsPerGroup} and {@link
* OutboundOverflowCounts#decrementOutboundOverflowCountsPerGroup}). On the other hand, this field
* (outboundOverflowCountsPerGroup) is likely on the same cache line as {@link #bitSetAndState}
* which needs to be updated on an insertion or deletion to the segment anyway, so changing
* outboundOverflowCountsPerGroup likely won't contribute to memory write amplification. Note,
* however, that in practice, write amplification reduction might be small if transitions
* between 0 and 1 counts are in fact the most common (TODO quantify). Also, there is a 12.5%
* chance that outboundOverflowCountsPerGroup will be on a different cache line than {@link
* #bitSetAndState} (given that we have no control over the layout of segment objects; in an
* implementation of SmoothieMap in an environment that allows to control layout of data
* structures they can be ensured to lay in the same cache line).
*
* - It allows to make {@link HashTable}'s tagGroups' and dataGroups' slots of 8 bits each (
* they have to be the same to allow efficient bitMask-based extraction in {@link
* HashTable#extractAllocIndex}) that in turn allows the tags to hold 8 rather than only 7 bits
* of keys' hash codes, thus reducing collisions during the search for a key. (Another
* alternative is incrementing a "scattered" outbound overflow count within a data group which
* is comprised of dataGroup's bits #6, #14, ... #62 instead of just {@link
* HashTable#DATA_GROUP__OUTBOUND_OVERFLOW_MASK} but it doesn't seem possible to increment and
* decrement such a scattered value efficiently, at least without parallel bit deposit and
* extract:
* en.wikipedia.org/wiki/Bit_Manipulation_Instruction_Sets#Parallel_bit_deposit_and_extract
* TODO experiment with that in native impl, or with Panama?
*/
long outboundOverflowCountsPerGroup;
private static final long BIT_SET_AND_STATE_OFFSET;
private static final long OUTBOUND_OVERFLOW_COUNTS_PER_GROUP_OFFSET;
static {
BIT_SET_AND_STATE_OFFSET = UnsafeUtils.getFieldOffset(
InterleavedSegment_BitSetAndStateArea.class, "bitSetAndState");
OUTBOUND_OVERFLOW_COUNTS_PER_GROUP_OFFSET = UnsafeUtils.getFieldOffset(
InterleavedSegment_BitSetAndStateArea.class, "outboundOverflowCountsPerGroup");
}
/**
* This method must not be called on objects which already have type {@link
* io.timeandspace.smoothie.SmoothieMap.Segment}.
*/
static long getBitSetAndState(Object segment) {
return U.getLong(segment, BIT_SET_AND_STATE_OFFSET);
}
/**
* This method must not be called on objects which already have type {@link
* io.timeandspace.smoothie.SmoothieMap.Segment}.
*/
static void setBitSetAndState(Object segment, long bitSetAndState) {
U.putLong(segment, BIT_SET_AND_STATE_OFFSET, bitSetAndState);
}
/**
* This method must not be called on objects which already have type {@link
* io.timeandspace.smoothie.SmoothieMap.Segment}.
*/
static long getOutboundOverflowCountsPerGroup(Object segment) {
return U.getLong(segment, OUTBOUND_OVERFLOW_COUNTS_PER_GROUP_OFFSET);
}
/**
* This method must not be called on objects which already have type {@link
* io.timeandspace.smoothie.SmoothieMap.Segment}.
*/
static void setOutboundOverflowCountsPerGroup(
Object segment, long outboundOverflowCountsPerGroup) {
U.putLong(
segment, OUTBOUND_OVERFLOW_COUNTS_PER_GROUP_OFFSET, outboundOverflowCountsPerGroup);
}
void clearOutboundOverflowCountsPerGroup() {
outboundOverflowCountsPerGroup = 0;
}
long replaceBitSetAndStateWithBulkOperationPlaceholderOrThrowCme() {
long bitSetAndState = this.bitSetAndState;
if (isBulkOperationPlaceholderBitSetAndState(bitSetAndState)) {
throw new ConcurrentModificationException();
}
this.bitSetAndState = makeBulkOperationPlaceholderBitSetAndState(bitSetAndState);
return bitSetAndState;
}
void setBitSetAndStateAfterBulkOperation(long newBitSetAndState) {
// This extra check doesn't guarantee anything because the update is not atomic, but it
// raises the chances of catching concurrent modification.
if (!isBulkOperationPlaceholderBitSetAndState(this.bitSetAndState)) {
throw new ConcurrentModificationException();
}
this.bitSetAndState = newBitSetAndState;
}
/** @deprecated in order not to forget to remove calls from production code */
@Deprecated
final BitSetAndState.DebugBitSetAndState debugBitSetAndState() {
return new BitSetAndState.DebugBitSetAndState(bitSetAndState);
}
}