• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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