29 import java.util.Collection; |
29 import java.util.Collection; |
30 import java.util.Collections; |
30 import java.util.Collections; |
31 import java.util.HashSet; |
31 import java.util.HashSet; |
32 import java.util.Map; |
32 import java.util.Map; |
33 import java.util.Set; |
33 import java.util.Set; |
|
34 import jdk.nashorn.internal.runtime.options.Options; |
34 |
35 |
35 /** |
36 /** |
36 * Immutable hash map implementation for properties. Properties are keyed on strings. |
37 * Immutable hash map implementation for properties. Properties are keyed on strings |
37 * Copying and cloning is avoided by relying on immutability. |
38 * or symbols (ES6). Copying and cloning is avoided by relying on immutability. |
38 * <p> |
39 * <p> |
39 * When adding an element to a hash table, only the head of a bin list is updated, thus |
40 * When adding an element to a hash table, only the head of a bin list is updated, thus |
40 * an add only requires the cloning of the bins array and adding an element to the head |
41 * an add only requires the cloning of the bins array and adding an element to the head |
41 * of the bin list. Similarly for removal, only a portion of a bin list is updated. |
42 * of the bin list. Similarly for removal, only a portion of a bin list is updated. |
42 * <p> |
43 * <p> |
|
44 * For large tables with hundreds or thousands of elements, even just cloning the bins |
|
45 * array when adding properties is an expensive operation. For this case, we put new |
|
46 * elements in a separate list called {@link ElementQueue}. |
|
47 * The list component is merged into the hash table at regular intervals during element |
|
48 * insertion to keep it from growing too long. Also, when a map with a queue component |
|
49 * is queried repeatedly, the map will replace itself with a pure hash table version |
|
50 * of itself to optimize lookup performance. |
|
51 * <p> |
43 * A separate chronological list is kept for quick generation of keys and values, and, |
52 * A separate chronological list is kept for quick generation of keys and values, and, |
44 * for rehashing. |
53 * for rehashing. For very small maps where the overhead of the hash table would |
|
54 * outweigh its benefits we deliberately avoid creating a hash structure and use the |
|
55 * chronological list alone for element storage. |
45 * <p> |
56 * <p> |
46 * Details: |
57 * Details: |
47 * <p> |
58 * <p> |
48 * The main goal is to be able to retrieve properties from a map quickly, keying on |
59 * The main goal is to be able to retrieve properties from a map quickly, keying on |
49 * the property name (String.) A secondary, but important goal, is to keep maps |
60 * the property name (String or Symbol). A secondary, but important goal, is to keep |
50 * immutable, so that a map can be shared by multiple objects in a context. |
61 * maps immutable, so that a map can be shared by multiple objects in a context. |
51 * Sharing maps allows objects to be categorized as having similar properties, a |
62 * Sharing maps allows objects to be categorized as having similar properties, a |
52 * fact that call site guards rely on. In this discussion, immutability allows us |
63 * fact that call site guards rely on. In this discussion, immutability allows us |
53 * to significantly reduce the amount of duplication we have in our maps. |
64 * to significantly reduce the amount of duplication we have in our maps. |
54 * <p> |
65 * <p> |
55 * The simplest of immutable maps is a basic singly linked list. New properties |
66 * The simplest of immutable maps is a basic singly linked list. New properties |
132 */ |
149 */ |
133 private PropertyHashMap() { |
150 private PropertyHashMap() { |
134 this.size = 0; |
151 this.size = 0; |
135 this.threshold = 0; |
152 this.threshold = 0; |
136 this.bins = null; |
153 this.bins = null; |
|
154 this.queue = null; |
137 this.list = null; |
155 this.list = null; |
138 } |
156 } |
139 |
157 |
140 /** |
158 /** |
141 * Clone Constructor |
159 * Constructor used internally to create new maps |
142 * |
160 * |
143 * @param map Original {@link PropertyHashMap}. |
161 * @param map the new map |
144 */ |
162 */ |
145 private PropertyHashMap(final PropertyHashMap map) { |
163 private PropertyHashMap(final MapBuilder map) { |
146 this.size = map.size; |
164 this.size = map.size; |
147 this.threshold = map.threshold; |
165 if (map.qhead == null) { |
148 this.bins = map.bins; |
166 this.bins = map.bins; |
|
167 this.queue = null; |
|
168 } else { |
|
169 this.bins = null; |
|
170 this.queue = new ElementQueue(map.qhead, map.bins); |
|
171 } |
149 this.list = map.list; |
172 this.list = map.list; |
150 } |
173 this.threshold = map.bins != null ? threeQuarters(map.bins.length) : 0; |
151 |
|
152 /** |
|
153 * Constructor used internally to extend a map |
|
154 * |
|
155 * @param size Size of the new {@link PropertyHashMap}. |
|
156 * @param bins The hash bins. |
|
157 * @param list The {@link Property} list. |
|
158 */ |
|
159 private PropertyHashMap(final int size, final Element[] bins, final Element list) { |
|
160 this.size = size; |
|
161 this.threshold = bins != null ? threeQuarters(bins.length) : 0; |
|
162 this.bins = bins; |
|
163 this.list = list; |
|
164 } |
174 } |
165 |
175 |
166 /** |
176 /** |
167 * Clone a property map, replacing a property with a new one in the same place, |
177 * Clone a property map, replacing a property with a new one in the same place, |
168 * which is important for property iterations if a property changes types |
178 * which is important for property iterations if a property changes types |
214 * @return New {@link PropertyHashMap}. |
226 * @return New {@link PropertyHashMap}. |
215 */ |
227 */ |
216 public PropertyHashMap immutableAdd(final Collection<Property> newProperties) { |
228 public PropertyHashMap immutableAdd(final Collection<Property> newProperties) { |
217 if (newProperties != null) { |
229 if (newProperties != null) { |
218 final int newSize = size + newProperties.size(); |
230 final int newSize = size + newProperties.size(); |
219 PropertyHashMap newMap = cloneMap(newSize); |
231 MapBuilder builder = newMapBuilder(newSize); |
220 for (final Property property : newProperties) { |
232 for (final Property property : newProperties) { |
221 newMap = newMap.addNoClone(property); |
233 builder.addProperty(property); |
222 } |
234 } |
223 return newMap; |
235 return new PropertyHashMap(builder); |
224 } |
236 } |
225 return this; |
237 return this; |
226 } |
238 } |
227 |
239 |
228 /** |
240 /** |
229 * Clone a {@link PropertyHashMap} and remove a {@link Property}. |
241 * Clone a {@link PropertyHashMap} and remove a {@link Property} based on its key. |
230 * |
242 * |
231 * @param property {@link Property} to remove. |
243 * @param key Key of {@link Property} to remove. |
232 * |
244 * |
233 * @return New {@link PropertyHashMap}. |
245 * @return New {@link PropertyHashMap}. |
234 */ |
246 */ |
235 public PropertyHashMap immutableRemove(final Property property) { |
|
236 return immutableRemove(property.getKey()); |
|
237 } |
|
238 |
|
239 /** |
|
240 * Clone a {@link PropertyHashMap} and remove a {@link Property} based on its key. |
|
241 * |
|
242 * @param key Key of {@link Property} to remove. |
|
243 * |
|
244 * @return New {@link PropertyHashMap}. |
|
245 */ |
|
246 public PropertyHashMap immutableRemove(final Object key) { |
247 public PropertyHashMap immutableRemove(final Object key) { |
247 if (bins != null) { |
248 MapBuilder builder = newMapBuilder(size); |
248 final int binIndex = binIndex(bins, key); |
249 builder.removeProperty(key); |
249 final Element bin = bins[binIndex]; |
250 if (builder.size < size) { |
250 if (findElement(bin, key) != null) { |
251 return builder.size != 0 ? new PropertyHashMap(builder) : EMPTY_HASHMAP; |
251 final int newSize = size - 1; |
|
252 Element[] newBins = null; |
|
253 if (newSize >= LIST_THRESHOLD) { |
|
254 newBins = bins.clone(); |
|
255 newBins[binIndex] = removeFromList(bin, key); |
|
256 } |
|
257 final Element newList = removeFromList(list, key); |
|
258 return new PropertyHashMap(newSize, newBins, newList); |
|
259 } |
|
260 } else if (findElement(list, key) != null) { |
|
261 final int newSize = size - 1; |
|
262 return newSize != 0 ? new PropertyHashMap(newSize, null, removeFromList(list, key)) : EMPTY_HASHMAP; |
|
263 } |
252 } |
264 return this; |
253 return this; |
265 } |
254 } |
266 |
255 |
267 /** |
256 /** |
378 } |
369 } |
379 } |
370 } |
380 return null; |
371 return null; |
381 } |
372 } |
382 |
373 |
383 |
374 /** |
384 private PropertyHashMap cloneMap() { |
375 * Create a {@code MapBuilder} to add new elements to. |
385 return new PropertyHashMap(size, bins == null ? null : bins.clone(), list); |
|
386 } |
|
387 |
|
388 /** |
|
389 * Clone {@link PropertyHashMap} to accommodate new size. |
|
390 * |
376 * |
391 * @param newSize New size of {@link PropertyHashMap}. |
377 * @param newSize New size of {@link PropertyHashMap}. |
392 * |
378 * |
393 * @return Cloned {@link PropertyHashMap} with new size. |
379 * @return {@link MapBuilder} for the new size. |
394 */ |
380 */ |
395 private PropertyHashMap cloneMap(final int newSize) { |
381 private MapBuilder newMapBuilder(final int newSize) { |
396 Element[] newBins; |
382 if (bins == null && newSize < LIST_THRESHOLD) { |
397 if (bins == null && newSize <= LIST_THRESHOLD) { |
383 return new MapBuilder(bins, list, size, false); |
398 newBins = null; |
|
399 } else if (newSize > threshold) { |
384 } else if (newSize > threshold) { |
400 newBins = rehash(list, binsNeeded(newSize)); |
385 return new MapBuilder(rehash(list, binsNeeded(newSize)), list, size, true); |
|
386 } else if (shouldCloneBins(size, newSize)) { |
|
387 return new MapBuilder(cloneBins(), list, size, true); |
|
388 } else if (queue == null) { |
|
389 return new MapBuilder(bins, list, size, false); |
401 } else { |
390 } else { |
402 newBins = bins.clone(); |
391 return new MapBuilder(queue, list, size, false); |
403 } |
392 } |
404 return new PropertyHashMap(newSize, newBins, list); |
393 } |
405 } |
394 |
406 |
395 /** |
407 |
396 * Create a cloned or new bins array and merge the elements in the queue into it if there are any. |
408 |
397 * |
409 /** |
398 * @return the cloned bins array |
410 * Add a {@link Property} to a temporary {@link PropertyHashMap}, that has |
399 */ |
411 * been already cloned. Removes duplicates if necessary. |
400 private Element[] cloneBins() { |
412 * |
401 if (queue != null) { |
413 * @param property {@link Property} to add. |
402 return queue.cloneAndMergeBins(); |
414 * |
403 } |
415 * @return New {@link PropertyHashMap}. |
404 |
416 */ |
405 return bins.clone(); |
417 private PropertyHashMap addNoClone(final Property property) { |
406 } |
418 int newSize = size; |
407 |
419 final Object key = property.getKey(); |
408 /** |
420 Element newList = list; |
409 * Used on insertion to determine whether the bins array should be cloned, or we should keep |
421 if (bins != null) { |
410 * using the ancestor's bins array and put new elements into the queue. |
422 final int binIndex = binIndex(bins, key); |
411 * |
423 Element bin = bins[binIndex]; |
412 * @param oldSize the old map size |
424 if (findElement(bin, key) != null) { |
413 * @param newSize the new map size |
425 newSize--; |
414 * @return whether to clone the bins array |
426 bin = removeFromList(bin, key); |
415 */ |
427 newList = removeFromList(list, key); |
416 private boolean shouldCloneBins(final int oldSize, final int newSize) { |
428 } |
417 // For maps with less than QUEUE_THRESHOLD elements we clone the bins array on every insertion. |
429 bins[binIndex] = new Element(bin, property); |
418 // Above that threshold we put new elements into the queue and only merge every 512 elements. |
430 } else { |
419 return newSize < QUEUE_THRESHOLD || (newSize >>> 9) > (oldSize >>> 9); |
431 if (findElement(list, key) != null) { |
|
432 newSize--; |
|
433 newList = removeFromList(list, key); |
|
434 } |
|
435 } |
|
436 newList = new Element(newList, property); |
|
437 return new PropertyHashMap(newSize, bins, newList); |
|
438 } |
|
439 |
|
440 private PropertyHashMap replaceNoClone(final Object key, final Property property) { |
|
441 if (bins != null) { |
|
442 final int binIndex = binIndex(bins, key); |
|
443 Element bin = bins[binIndex]; |
|
444 bin = replaceInList(bin, key, property); |
|
445 bins[binIndex] = bin; |
|
446 } |
|
447 Element newList = list; |
|
448 newList = replaceInList(newList, key, property); |
|
449 return new PropertyHashMap(size, bins, newList); |
|
450 } |
420 } |
451 |
421 |
452 /** |
422 /** |
453 * Removes an {@link Element} from a specific list, avoiding duplication. |
423 * Removes an {@link Element} from a specific list, avoiding duplication. |
454 * |
424 * |
679 Property getProperty() { |
649 Property getProperty() { |
680 return property; |
650 return property; |
681 } |
651 } |
682 } |
652 } |
683 |
653 |
|
654 /** |
|
655 * A hybrid map/list structure to add elements to the map without the need to clone and rehash the main table. |
|
656 * This is used for large maps to reduce the overhead of adding elements. Instances of this class can replace |
|
657 * themselves with a pure hash map version of themselves to optimize query performance. |
|
658 */ |
|
659 private class ElementQueue { |
|
660 |
|
661 /** List of elements not merged into bins */ |
|
662 private final Element qhead; |
|
663 |
|
664 /** Our own bins array. Differs from original PropertyHashMap bins when queue is merged. */ |
|
665 private final Element[] qbins; |
|
666 |
|
667 /** Count searches to trigger merging of queue into bins. */ |
|
668 int searchCount = 0; |
|
669 |
|
670 ElementQueue(final Element qhead, final Element[] qbins) { |
|
671 this.qhead = qhead; |
|
672 this.qbins = qbins; |
|
673 } |
|
674 |
|
675 Element find(final Object key) { |
|
676 final int binIndex = binIndex(qbins, key); |
|
677 final Element element = findElement(qbins[binIndex], key); |
|
678 if (element != null) { |
|
679 return element; |
|
680 } |
|
681 if (qhead != null) { |
|
682 if (++searchCount > 2) { |
|
683 // Merge the queue into the hash bins if this map is queried more than a few times |
|
684 final Element[] newBins = cloneAndMergeBins(); |
|
685 assert newBins != qbins; |
|
686 PropertyHashMap.this.queue = new ElementQueue(null, newBins); |
|
687 return PropertyHashMap.this.queue.find(key); |
|
688 } |
|
689 return findElement(qhead, key); |
|
690 } |
|
691 return null; |
|
692 } |
|
693 |
|
694 /** |
|
695 * Create a cloned or new bins array and merge the elements in the queue into it if there are any. |
|
696 * |
|
697 * @return the cloned bins array |
|
698 */ |
|
699 private Element[] cloneAndMergeBins() { |
|
700 if (qhead == null) { |
|
701 return qbins; |
|
702 } |
|
703 |
|
704 final Element[] newBins = qbins.clone(); |
|
705 |
|
706 for (Element element = qhead; element != null; element = element.getLink()) { |
|
707 final Property property = element.getProperty(); |
|
708 final Object key = property.getKey(); |
|
709 final int binIndex = binIndex(newBins, key); |
|
710 |
|
711 newBins[binIndex] = new Element(newBins[binIndex], property); |
|
712 } |
|
713 |
|
714 return newBins; |
|
715 } |
|
716 |
|
717 } |
|
718 |
|
719 /** |
|
720 * A builder class used for adding, replacing, or removing elements. |
|
721 */ |
|
722 private static class MapBuilder { |
|
723 |
|
724 /** Bins array - may be shared with map that created us. */ |
|
725 private Element[] bins; |
|
726 /** Whether our bins are shared */ |
|
727 private boolean hasOwnBins; |
|
728 |
|
729 /** Queue of unmerged elements */ |
|
730 private Element qhead; |
|
731 |
|
732 /** Full property list. */ |
|
733 private Element list; |
|
734 |
|
735 /** Number of properties. */ |
|
736 private int size; |
|
737 |
|
738 MapBuilder(final Element[] bins, final Element list, final int size, final boolean hasOwnBins) { |
|
739 this.bins = bins; |
|
740 this.hasOwnBins = hasOwnBins; |
|
741 this.list = list; |
|
742 this.qhead = null; |
|
743 this.size = size; |
|
744 } |
|
745 |
|
746 MapBuilder(final ElementQueue queue, final Element list, final int size, final boolean hasOwnBins) { |
|
747 this.bins = queue.qbins; |
|
748 this.hasOwnBins = hasOwnBins; |
|
749 this.list = list; |
|
750 this.qhead = queue.qhead; |
|
751 this.size = size; |
|
752 } |
|
753 |
|
754 /** |
|
755 * Add a {@link Property}. Removes duplicates if necessary. |
|
756 * |
|
757 * @param property {@link Property} to add. |
|
758 */ |
|
759 private void addProperty(final Property property) { |
|
760 final Object key = property.getKey(); |
|
761 |
|
762 if (bins != null) { |
|
763 final int binIndex = binIndex(bins, key); |
|
764 if (findElement(bins[binIndex], key) != null) { |
|
765 ensureOwnBins(); |
|
766 bins[binIndex] = removeExistingElement(bins[binIndex], key); |
|
767 } else if (findElement(qhead, key) != null) { |
|
768 qhead = removeExistingElement(qhead, key); |
|
769 } |
|
770 if (hasOwnBins) { |
|
771 bins[binIndex] = new Element(bins[binIndex], property); |
|
772 } else { |
|
773 qhead = new Element(qhead, property); |
|
774 } |
|
775 } else { |
|
776 if (findElement(list, key) != null) { |
|
777 list = removeFromList(list, key); |
|
778 size--; |
|
779 } |
|
780 } |
|
781 |
|
782 list = new Element(list, property); |
|
783 size++; |
|
784 } |
|
785 |
|
786 /** |
|
787 * Replace an existing {@link Property} with a new one with the same key. |
|
788 * |
|
789 * @param key the property key |
|
790 * @param property the property to replace the old one with |
|
791 */ |
|
792 private void replaceProperty(final Object key, final Property property) { |
|
793 |
|
794 if (bins != null) { |
|
795 final int binIndex = binIndex(bins, key); |
|
796 Element bin = bins[binIndex]; |
|
797 if (findElement(bin, key) != null) { |
|
798 ensureOwnBins(); |
|
799 bins[binIndex] = replaceInList(bin, key, property); |
|
800 } else if (qhead != null) { |
|
801 qhead = replaceInList(qhead, key, property); |
|
802 } |
|
803 } |
|
804 |
|
805 list = replaceInList(list, key, property); |
|
806 } |
|
807 |
|
808 /** |
|
809 * Remove a {@link Property} based on its key. |
|
810 * |
|
811 * @param key Key of {@link Property} to remove. |
|
812 */ |
|
813 void removeProperty(final Object key) { |
|
814 |
|
815 if (bins != null) { |
|
816 final int binIndex = binIndex(bins, key); |
|
817 final Element bin = bins[binIndex]; |
|
818 if (findElement(bin, key) != null) { ; |
|
819 if (size >= LIST_THRESHOLD) { |
|
820 ensureOwnBins(); |
|
821 bins[binIndex] = removeFromList(bin, key); |
|
822 } else { |
|
823 // Go back to list-only representation for small maps |
|
824 bins = null; |
|
825 qhead = null; |
|
826 } |
|
827 } else if (findElement(qhead, key) != null) { |
|
828 qhead = removeFromList(qhead, key); |
|
829 } |
|
830 } |
|
831 |
|
832 list = removeFromList(list, key); |
|
833 size--; |
|
834 } |
|
835 |
|
836 /** |
|
837 * Removes an element known to exist from an element list and the main {@code list} and decreases {@code size}. |
|
838 * |
|
839 * @param element the element list |
|
840 * @param key the key to remove |
|
841 * @return the new element list |
|
842 */ |
|
843 private Element removeExistingElement(Element element, Object key) { |
|
844 size--; |
|
845 list = removeFromList(list, key); |
|
846 return removeFromList(element, key); |
|
847 } |
|
848 |
|
849 /** |
|
850 * Make sure we own the bins we have, cloning them if necessary. |
|
851 */ |
|
852 private void ensureOwnBins() { |
|
853 if (!hasOwnBins) { |
|
854 bins = bins.clone(); |
|
855 } |
|
856 hasOwnBins = true; |
|
857 } |
|
858 } |
|
859 |
684 } |
860 } |