| /* |
| * 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. |
| * |
| * <p>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 <K,V> V get(Node<K,V> 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 <K,V> Node<K,V> put(Node<K,V> 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<K,V> implements Node<K,V> { |
| 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<K,V> 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<K,V> implements Node<K,V> { |
| // 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<K,V> 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<K,V> implements Node<K,V> { |
| private static final int BITS = 5; |
| private static final int BITS_MASK = 0x1F; |
| |
| final int bitmap; |
| final Node<K,V>[] values; |
| private final int size; |
| |
| private CompressedIndex(int bitmap, Node<K,V>[] 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<K,V> 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<K,V>[] newValues = (Node<K,V>[]) 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<K,V>[] 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 <K,V> Node<K,V> combine( |
| Node<K,V> node1, int hash1, Node<K,V> node2, int hash2, int bitsConsumed) { |
| assert hash1 != hash2; |
| int indexBit1 = indexBit(hash1, bitsConsumed); |
| int indexBit2 = indexBit(hash2, bitsConsumed); |
| if (indexBit1 == indexBit2) { |
| Node<K,V> node = combine(node1, hash1, node2, hash2, bitsConsumed + BITS); |
| @SuppressWarnings("unchecked") |
| Node<K,V>[] values = (Node<K,V>[]) new Node<?,?>[] {node}; |
| return new CompressedIndex<>(indexBit1, values, node.size()); |
| } else { |
| // Make node1 the smallest |
| if (uncompressedIndex(hash1, bitsConsumed) > uncompressedIndex(hash2, bitsConsumed)) { |
| Node<K,V> nodeCopy = node1; |
| node1 = node2; |
| node2 = nodeCopy; |
| } |
| @SuppressWarnings("unchecked") |
| Node<K,V>[] values = (Node<K,V>[]) 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<K, V> 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<K,V> { |
| V get(K key, int hash, int bitsConsumed); |
| |
| Node<K,V> put(K key, V value, int hash, int bitsConsumed); |
| |
| int size(); |
| } |
| } |