io.grpc.PersistentHashArrayMappedTrie Maven / Gradle / Ivy
/*
* Copyright 2017 The gRPC 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.grpc;
import java.util.Arrays;
/**
* A persistent (copy-on-write) hash tree/trie. Collisions are handled
* linearly. Delete is not supported, but replacement is. The implementation
* favors simplicity and low memory allocation during insertion. Although the
* asymptotics are good, it is optimized for small sizes like less than 20;
* "unbelievably large" would be 100.
*
* Inspired by popcnt-based compression seen in Ideal Hash Trees, Phil
* Bagwell (2000). The rest of the implementation is ignorant of/ignores the
* paper.
*/
final class PersistentHashArrayMappedTrie {
private PersistentHashArrayMappedTrie() {}
/**
* Returns the value with the specified key, or {@code null} if it does not exist.
*/
static V get(Node root, K key) {
if (root == null) {
return null;
}
return root.get(key, key.hashCode(), 0);
}
/**
* Returns a new trie where the key is set to the specified value.
*/
static Node put(Node root, K key, V value) {
if (root == null) {
return new Leaf<>(key, value);
}
return root.put(key, value, key.hashCode(), 0);
}
// Not actually annotated to avoid depending on guava
// @VisibleForTesting
static final class Leaf implements Node {
private final K key;
private final V value;
public Leaf(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public int size() {
return 1;
}
@Override
public V get(K key, int hash, int bitsConsumed) {
if (this.key == key) {
return value;
} else {
return null;
}
}
@Override
public Node put(K key, V value, int hash, int bitsConsumed) {
int thisHash = this.key.hashCode();
if (thisHash != hash) {
// Insert
return CompressedIndex.combine(
new Leaf<>(key, value), hash, this, thisHash, bitsConsumed);
} else if (this.key == key) {
// Replace
return new Leaf<>(key, value);
} else {
// Hash collision
return new CollisionLeaf<>(this.key, this.value, key, value);
}
}
@Override
public String toString() {
return String.format("Leaf(key=%s value=%s)", key, value);
}
}
// Not actually annotated to avoid depending on guava
// @VisibleForTesting
static final class CollisionLeaf implements Node {
// All keys must have same hash, but not have the same reference
private final K[] keys;
private final V[] values;
// Not actually annotated to avoid depending on guava
// @VisibleForTesting
@SuppressWarnings("unchecked")
CollisionLeaf(K key1, V value1, K key2, V value2) {
this((K[]) new Object[] {key1, key2}, (V[]) new Object[] {value1, value2});
assert key1 != key2;
assert key1.hashCode() == key2.hashCode();
}
private CollisionLeaf(K[] keys, V[] values) {
this.keys = keys;
this.values = values;
}
@Override
public int size() {
return values.length;
}
@Override
public V get(K key, int hash, int bitsConsumed) {
for (int i = 0; i < keys.length; i++) {
if (keys[i] == key) {
return values[i];
}
}
return null;
}
@Override
public Node put(K key, V value, int hash, int bitsConsumed) {
int thisHash = keys[0].hashCode();
int keyIndex;
if (thisHash != hash) {
// Insert
return CompressedIndex.combine(
new Leaf<>(key, value), hash, this, thisHash, bitsConsumed);
} else if ((keyIndex = indexOfKey(key)) != -1) {
// Replace
K[] newKeys = Arrays.copyOf(keys, keys.length);
V[] newValues = Arrays.copyOf(values, keys.length);
newKeys[keyIndex] = key;
newValues[keyIndex] = value;
return new CollisionLeaf<>(newKeys, newValues);
} else {
// Yet another hash collision
K[] newKeys = Arrays.copyOf(keys, keys.length + 1);
V[] newValues = Arrays.copyOf(values, keys.length + 1);
newKeys[keys.length] = key;
newValues[keys.length] = value;
return new CollisionLeaf<>(newKeys, newValues);
}
}
// -1 if not found
private int indexOfKey(K key) {
for (int i = 0; i < keys.length; i++) {
if (keys[i] == key) {
return i;
}
}
return -1;
}
@Override
public String toString() {
StringBuilder valuesSb = new StringBuilder();
valuesSb.append("CollisionLeaf(");
for (int i = 0; i < values.length; i++) {
valuesSb.append("(key=").append(keys[i]).append(" value=").append(values[i]).append(") ");
}
return valuesSb.append(")").toString();
}
}
// Not actually annotated to avoid depending on guava
// @VisibleForTesting
static final class CompressedIndex implements Node {
private static final int BITS = 5;
private static final int BITS_MASK = 0x1F;
final int bitmap;
final Node[] values;
private final int size;
private CompressedIndex(int bitmap, Node[] values, int size) {
this.bitmap = bitmap;
this.values = values;
this.size = size;
}
@Override
public int size() {
return size;
}
@Override
public V get(K key, int hash, int bitsConsumed) {
int indexBit = indexBit(hash, bitsConsumed);
if ((bitmap & indexBit) == 0) {
return null;
}
int compressedIndex = compressedIndex(indexBit);
return values[compressedIndex].get(key, hash, bitsConsumed + BITS);
}
@Override
public Node put(K key, V value, int hash, int bitsConsumed) {
int indexBit = indexBit(hash, bitsConsumed);
int compressedIndex = compressedIndex(indexBit);
if ((bitmap & indexBit) == 0) {
// Insert
int newBitmap = bitmap | indexBit;
@SuppressWarnings("unchecked")
Node[] newValues = (Node[]) new Node,?>[values.length + 1];
System.arraycopy(values, 0, newValues, 0, compressedIndex);
newValues[compressedIndex] = new Leaf<>(key, value);
System.arraycopy(
values,
compressedIndex,
newValues,
compressedIndex + 1,
values.length - compressedIndex);
return new CompressedIndex<>(newBitmap, newValues, size() + 1);
} else {
// Replace
Node[] newValues = Arrays.copyOf(values, values.length);
newValues[compressedIndex] =
values[compressedIndex].put(key, value, hash, bitsConsumed + BITS);
int newSize = size();
newSize += newValues[compressedIndex].size();
newSize -= values[compressedIndex].size();
return new CompressedIndex<>(bitmap, newValues, newSize);
}
}
static Node combine(
Node node1, int hash1, Node node2, int hash2, int bitsConsumed) {
assert hash1 != hash2;
int indexBit1 = indexBit(hash1, bitsConsumed);
int indexBit2 = indexBit(hash2, bitsConsumed);
if (indexBit1 == indexBit2) {
Node node = combine(node1, hash1, node2, hash2, bitsConsumed + BITS);
@SuppressWarnings("unchecked")
Node[] values = (Node[]) new Node,?>[] {node};
return new CompressedIndex<>(indexBit1, values, node.size());
} else {
// Make node1 the smallest
if (uncompressedIndex(hash1, bitsConsumed) > uncompressedIndex(hash2, bitsConsumed)) {
Node nodeCopy = node1;
node1 = node2;
node2 = nodeCopy;
}
@SuppressWarnings("unchecked")
Node[] values = (Node[]) new Node,?>[] {node1, node2};
return new CompressedIndex<>(indexBit1 | indexBit2, values, node1.size() + node2.size());
}
}
@Override
public String toString() {
StringBuilder valuesSb = new StringBuilder();
valuesSb.append("CompressedIndex(")
.append(String.format("bitmap=%s ", Integer.toBinaryString(bitmap)));
for (Node value : values) {
valuesSb.append(value).append(" ");
}
return valuesSb.append(")").toString();
}
private int compressedIndex(int indexBit) {
return Integer.bitCount(bitmap & (indexBit - 1));
}
private static int uncompressedIndex(int hash, int bitsConsumed) {
return (hash >>> bitsConsumed) & BITS_MASK;
}
private static int indexBit(int hash, int bitsConsumed) {
int uncompressedIndex = uncompressedIndex(hash, bitsConsumed);
return 1 << uncompressedIndex;
}
}
interface Node {
V get(K key, int hash, int bitsConsumed);
Node put(K key, V value, int hash, int bitsConsumed);
int size();
}
}