com.google.common.collect.CompactHashing Maven / Gradle / Ivy
/*
* Copyright (C) 2019 The Guava 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 com.google.common.collect;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Objects;
import com.google.common.primitives.Ints;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Helper classes and static methods for implementing compact hash-based collections.
*
* @author Jon Noack
*/
@GwtIncompatible
final class CompactHashing {
private CompactHashing() {}
/** Indicates blank table entries. */
static final byte UNSET = 0;
/** Number of bits used to store the numbers of hash table bits (max 30). */
private static final int HASH_TABLE_BITS_MAX_BITS = 5;
/** Use high bits of metadata for modification count. */
static final int MODIFICATION_COUNT_INCREMENT = (1 << HASH_TABLE_BITS_MAX_BITS);
/** Bitmask that selects the low bits of metadata to get hashTableBits. */
static final int HASH_TABLE_BITS_MASK = (1 << HASH_TABLE_BITS_MAX_BITS) - 1;
/** Maximum size of a compact hash-based collection (2^30 - 1 because 0 is UNSET). */
static final int MAX_SIZE = Ints.MAX_POWER_OF_TWO - 1;
/** Default size of a compact hash-based collection. */
static final int DEFAULT_SIZE = 3;
/**
* Minimum size of the hash table of a compact hash-based collection. Because small hash tables
* use a byte[], any smaller size uses the same amount of memory due to object padding.
*/
private static final int MIN_HASH_TABLE_SIZE = 4;
private static final int BYTE_MAX_SIZE = 1 << Byte.SIZE; // 2^8 = 256
private static final int BYTE_MASK = (1 << Byte.SIZE) - 1; // 2^8 - 1 = 255
private static final int SHORT_MAX_SIZE = 1 << Short.SIZE; // 2^16 = 65_536
private static final int SHORT_MASK = (1 << Short.SIZE) - 1; // 2^16 - 1 = 65_535
/**
* Returns the power of 2 hashtable size required to hold the expected number of items or the
* minimum hashtable size, whichever is greater.
*/
static int tableSize(int expectedSize) {
// We use entries next == 0 to indicate UNSET, so actual capacity is 1 less than requested.
return Math.max(MIN_HASH_TABLE_SIZE, Hashing.closedTableSize(expectedSize + 1, 1.0f));
}
/** Creates and returns a properly-sized array with the given number of buckets. */
static Object createTable(int buckets) {
if (buckets < 2
|| buckets > Ints.MAX_POWER_OF_TWO
|| Integer.highestOneBit(buckets) != buckets) {
throw new IllegalArgumentException("must be power of 2 between 2^1 and 2^30: " + buckets);
}
if (buckets <= BYTE_MAX_SIZE) {
return new byte[buckets];
} else if (buckets <= SHORT_MAX_SIZE) {
return new short[buckets];
} else {
return new int[buckets];
}
}
static void tableClear(Object table) {
if (table instanceof byte[]) {
Arrays.fill((byte[]) table, (byte) 0);
} else if (table instanceof short[]) {
Arrays.fill((short[]) table, (short) 0);
} else {
Arrays.fill((int[]) table, 0);
}
}
static int tableGet(Object table, int index) {
if (table instanceof byte[]) {
return ((byte[]) table)[index] & BYTE_MASK; // unsigned read
} else if (table instanceof short[]) {
return ((short[]) table)[index] & SHORT_MASK; // unsigned read
} else {
return ((int[]) table)[index];
}
}
static void tableSet(Object table, int index, int entry) {
if (table instanceof byte[]) {
((byte[]) table)[index] = (byte) entry; // unsigned write
} else if (table instanceof short[]) {
((short[]) table)[index] = (short) entry; // unsigned write
} else {
((int[]) table)[index] = entry;
}
}
/**
* Returns a larger power of 2 hashtable size given the current mask.
*
* For hashtable sizes less than or equal to 32, the returned power of 2 is 4x the current
* hashtable size to reduce expensive rehashing. Otherwise the returned power of 2 is 2x the
* current hashtable size.
*/
static int newCapacity(int mask) {
return ((mask < 32) ? 4 : 2) * (mask + 1);
}
/** Returns the hash prefix given the current mask. */
static int getHashPrefix(int value, int mask) {
return value & ~mask;
}
/** Returns the index, or 0 if the entry is "null". */
static int getNext(int entry, int mask) {
return entry & mask;
}
/** Returns a new value combining the prefix and suffix using the given mask. */
static int maskCombine(int prefix, int suffix, int mask) {
return (prefix & ~mask) | (suffix & mask);
}
static int remove(
@Nullable Object key,
@Nullable Object value,
int mask,
Object table,
int[] entries,
Object[] keys,
Object @Nullable [] values) {
int hash = Hashing.smearedHash(key);
int tableIndex = hash & mask;
int next = tableGet(table, tableIndex);
if (next == UNSET) {
return -1;
}
int hashPrefix = getHashPrefix(hash, mask);
int lastEntryIndex = -1;
do {
int entryIndex = next - 1;
int entry = entries[entryIndex];
if (getHashPrefix(entry, mask) == hashPrefix
&& Objects.equal(key, keys[entryIndex])
&& (values == null || Objects.equal(value, values[entryIndex]))) {
int newNext = getNext(entry, mask);
if (lastEntryIndex == -1) {
// we need to update the root link from table[]
tableSet(table, tableIndex, newNext);
} else {
// we need to update the link from the chain
entries[lastEntryIndex] = maskCombine(entries[lastEntryIndex], newNext, mask);
}
return entryIndex;
}
lastEntryIndex = entryIndex;
next = getNext(entry, mask);
} while (next != UNSET);
return -1;
}
}