1 /* 2 * Copyright 2017 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc; 18 19 import java.util.Arrays; 20 21 /** 22 * A persistent (copy-on-write) hash tree/trie. Collisions are handled 23 * linearly. Delete is not supported, but replacement is. The implementation 24 * favors simplicity and low memory allocation during insertion. Although the 25 * asymptotics are good, it is optimized for small sizes like less than 20; 26 * "unbelievably large" would be 100. 27 * 28 * <p>Inspired by popcnt-based compression seen in Ideal Hash Trees, Phil 29 * Bagwell (2000). The rest of the implementation is ignorant of/ignores the 30 * paper. 31 */ 32 final class PersistentHashArrayMappedTrie { 33 PersistentHashArrayMappedTrie()34 private PersistentHashArrayMappedTrie() {} 35 36 /** 37 * Returns the value with the specified key, or {@code null} if it does not exist. 38 */ get(Node<K,V> root, K key)39 static <K,V> V get(Node<K,V> root, K key) { 40 if (root == null) { 41 return null; 42 } 43 return root.get(key, key.hashCode(), 0); 44 } 45 46 /** 47 * Returns a new trie where the key is set to the specified value. 48 */ put(Node<K,V> root, K key, V value)49 static <K,V> Node<K,V> put(Node<K,V> root, K key, V value) { 50 if (root == null) { 51 return new Leaf<>(key, value); 52 } 53 return root.put(key, value, key.hashCode(), 0); 54 } 55 56 // Not actually annotated to avoid depending on guava 57 // @VisibleForTesting 58 static final class Leaf<K,V> implements Node<K,V> { 59 private final K key; 60 private final V value; 61 Leaf(K key, V value)62 public Leaf(K key, V value) { 63 this.key = key; 64 this.value = value; 65 } 66 67 @Override size()68 public int size() { 69 return 1; 70 } 71 72 @Override get(K key, int hash, int bitsConsumed)73 public V get(K key, int hash, int bitsConsumed) { 74 if (this.key == key) { 75 return value; 76 } else { 77 return null; 78 } 79 } 80 81 @Override put(K key, V value, int hash, int bitsConsumed)82 public Node<K,V> put(K key, V value, int hash, int bitsConsumed) { 83 int thisHash = this.key.hashCode(); 84 if (thisHash != hash) { 85 // Insert 86 return CompressedIndex.combine( 87 new Leaf<>(key, value), hash, this, thisHash, bitsConsumed); 88 } else if (this.key == key) { 89 // Replace 90 return new Leaf<>(key, value); 91 } else { 92 // Hash collision 93 return new CollisionLeaf<>(this.key, this.value, key, value); 94 } 95 } 96 97 @Override toString()98 public String toString() { 99 return String.format("Leaf(key=%s value=%s)", key, value); 100 } 101 } 102 103 // Not actually annotated to avoid depending on guava 104 // @VisibleForTesting 105 static final class CollisionLeaf<K,V> implements Node<K,V> { 106 // All keys must have same hash, but not have the same reference 107 private final K[] keys; 108 private final V[] values; 109 110 // Not actually annotated to avoid depending on guava 111 // @VisibleForTesting 112 @SuppressWarnings("unchecked") CollisionLeaf(K key1, V value1, K key2, V value2)113 CollisionLeaf(K key1, V value1, K key2, V value2) { 114 this((K[]) new Object[] {key1, key2}, (V[]) new Object[] {value1, value2}); 115 assert key1 != key2; 116 assert key1.hashCode() == key2.hashCode(); 117 } 118 CollisionLeaf(K[] keys, V[] values)119 private CollisionLeaf(K[] keys, V[] values) { 120 this.keys = keys; 121 this.values = values; 122 } 123 124 @Override size()125 public int size() { 126 return values.length; 127 } 128 129 @Override get(K key, int hash, int bitsConsumed)130 public V get(K key, int hash, int bitsConsumed) { 131 for (int i = 0; i < keys.length; i++) { 132 if (keys[i] == key) { 133 return values[i]; 134 } 135 } 136 return null; 137 } 138 139 @Override put(K key, V value, int hash, int bitsConsumed)140 public Node<K,V> put(K key, V value, int hash, int bitsConsumed) { 141 int thisHash = keys[0].hashCode(); 142 int keyIndex; 143 if (thisHash != hash) { 144 // Insert 145 return CompressedIndex.combine( 146 new Leaf<>(key, value), hash, this, thisHash, bitsConsumed); 147 } else if ((keyIndex = indexOfKey(key)) != -1) { 148 // Replace 149 K[] newKeys = Arrays.copyOf(keys, keys.length); 150 V[] newValues = Arrays.copyOf(values, keys.length); 151 newKeys[keyIndex] = key; 152 newValues[keyIndex] = value; 153 return new CollisionLeaf<>(newKeys, newValues); 154 } else { 155 // Yet another hash collision 156 K[] newKeys = Arrays.copyOf(keys, keys.length + 1); 157 V[] newValues = Arrays.copyOf(values, keys.length + 1); 158 newKeys[keys.length] = key; 159 newValues[keys.length] = value; 160 return new CollisionLeaf<>(newKeys, newValues); 161 } 162 } 163 164 // -1 if not found indexOfKey(K key)165 private int indexOfKey(K key) { 166 for (int i = 0; i < keys.length; i++) { 167 if (keys[i] == key) { 168 return i; 169 } 170 } 171 return -1; 172 } 173 174 @Override toString()175 public String toString() { 176 StringBuilder valuesSb = new StringBuilder(); 177 valuesSb.append("CollisionLeaf("); 178 for (int i = 0; i < values.length; i++) { 179 valuesSb.append("(key=").append(keys[i]).append(" value=").append(values[i]).append(") "); 180 } 181 return valuesSb.append(")").toString(); 182 } 183 } 184 185 // Not actually annotated to avoid depending on guava 186 // @VisibleForTesting 187 static final class CompressedIndex<K,V> implements Node<K,V> { 188 private static final int BITS = 5; 189 private static final int BITS_MASK = 0x1F; 190 191 final int bitmap; 192 final Node<K,V>[] values; 193 private final int size; 194 CompressedIndex(int bitmap, Node<K,V>[] values, int size)195 private CompressedIndex(int bitmap, Node<K,V>[] values, int size) { 196 this.bitmap = bitmap; 197 this.values = values; 198 this.size = size; 199 } 200 201 @Override size()202 public int size() { 203 return size; 204 } 205 206 @Override get(K key, int hash, int bitsConsumed)207 public V get(K key, int hash, int bitsConsumed) { 208 int indexBit = indexBit(hash, bitsConsumed); 209 if ((bitmap & indexBit) == 0) { 210 return null; 211 } 212 int compressedIndex = compressedIndex(indexBit); 213 return values[compressedIndex].get(key, hash, bitsConsumed + BITS); 214 } 215 216 @Override put(K key, V value, int hash, int bitsConsumed)217 public Node<K,V> put(K key, V value, int hash, int bitsConsumed) { 218 int indexBit = indexBit(hash, bitsConsumed); 219 int compressedIndex = compressedIndex(indexBit); 220 if ((bitmap & indexBit) == 0) { 221 // Insert 222 int newBitmap = bitmap | indexBit; 223 @SuppressWarnings("unchecked") 224 Node<K,V>[] newValues = (Node<K,V>[]) new Node<?,?>[values.length + 1]; 225 System.arraycopy(values, 0, newValues, 0, compressedIndex); 226 newValues[compressedIndex] = new Leaf<>(key, value); 227 System.arraycopy( 228 values, 229 compressedIndex, 230 newValues, 231 compressedIndex + 1, 232 values.length - compressedIndex); 233 return new CompressedIndex<>(newBitmap, newValues, size() + 1); 234 } else { 235 // Replace 236 Node<K,V>[] newValues = Arrays.copyOf(values, values.length); 237 newValues[compressedIndex] = 238 values[compressedIndex].put(key, value, hash, bitsConsumed + BITS); 239 int newSize = size(); 240 newSize += newValues[compressedIndex].size(); 241 newSize -= values[compressedIndex].size(); 242 return new CompressedIndex<>(bitmap, newValues, newSize); 243 } 244 } 245 combine( Node<K,V> node1, int hash1, Node<K,V> node2, int hash2, int bitsConsumed)246 static <K,V> Node<K,V> combine( 247 Node<K,V> node1, int hash1, Node<K,V> node2, int hash2, int bitsConsumed) { 248 assert hash1 != hash2; 249 int indexBit1 = indexBit(hash1, bitsConsumed); 250 int indexBit2 = indexBit(hash2, bitsConsumed); 251 if (indexBit1 == indexBit2) { 252 Node<K,V> node = combine(node1, hash1, node2, hash2, bitsConsumed + BITS); 253 @SuppressWarnings("unchecked") 254 Node<K,V>[] values = (Node<K,V>[]) new Node<?,?>[] {node}; 255 return new CompressedIndex<>(indexBit1, values, node.size()); 256 } else { 257 // Make node1 the smallest 258 if (uncompressedIndex(hash1, bitsConsumed) > uncompressedIndex(hash2, bitsConsumed)) { 259 Node<K,V> nodeCopy = node1; 260 node1 = node2; 261 node2 = nodeCopy; 262 } 263 @SuppressWarnings("unchecked") 264 Node<K,V>[] values = (Node<K,V>[]) new Node<?,?>[] {node1, node2}; 265 return new CompressedIndex<>(indexBit1 | indexBit2, values, node1.size() + node2.size()); 266 } 267 } 268 269 @Override toString()270 public String toString() { 271 StringBuilder valuesSb = new StringBuilder(); 272 valuesSb.append("CompressedIndex(") 273 .append(String.format("bitmap=%s ", Integer.toBinaryString(bitmap))); 274 for (Node<K, V> value : values) { 275 valuesSb.append(value).append(" "); 276 } 277 return valuesSb.append(")").toString(); 278 } 279 compressedIndex(int indexBit)280 private int compressedIndex(int indexBit) { 281 return Integer.bitCount(bitmap & (indexBit - 1)); 282 } 283 uncompressedIndex(int hash, int bitsConsumed)284 private static int uncompressedIndex(int hash, int bitsConsumed) { 285 return (hash >>> bitsConsumed) & BITS_MASK; 286 } 287 indexBit(int hash, int bitsConsumed)288 private static int indexBit(int hash, int bitsConsumed) { 289 int uncompressedIndex = uncompressedIndex(hash, bitsConsumed); 290 return 1 << uncompressedIndex; 291 } 292 } 293 294 interface Node<K,V> { get(K key, int hash, int bitsConsumed)295 V get(K key, int hash, int bitsConsumed); 296 put(K key, V value, int hash, int bitsConsumed)297 Node<K,V> put(K key, V value, int hash, int bitsConsumed); 298 size()299 int size(); 300 } 301 } 302