src/jdk/nashorn/internal/runtime/PropertyHashMap.java

Tue, 30 Apr 2013 10:05:42 -0300

author
jlaskey
date
Tue, 30 Apr 2013 10:05:42 -0300
changeset 242
b754fb89367d
parent 156
a094fc010120
child 952
6d5471a497fb
child 963
e2497b11a021
permissions
-rw-r--r--

8006220: Simplify PropertyMaps
Reviewed-by: hannesw, lagergren
Contributed-by: james.laskey@oracle.com

     1 /*
     2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
     4  *
     5  * This code is free software; you can redistribute it and/or modify it
     6  * under the terms of the GNU General Public License version 2 only, as
     7  * published by the Free Software Foundation.  Oracle designates this
     8  * particular file as subject to the "Classpath" exception as provided
     9  * by Oracle in the LICENSE file that accompanied this code.
    10  *
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    14  * version 2 for more details (a copy is included in the LICENSE file that
    15  * accompanied this code).
    16  *
    17  * You should have received a copy of the GNU General Public License version
    18  * 2 along with this work; if not, write to the Free Software Foundation,
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
    20  *
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
    22  * or visit www.oracle.com if you need additional information or have any
    23  * questions.
    24  */
    26 package jdk.nashorn.internal.runtime;
    28 import java.util.Arrays;
    29 import java.util.Collection;
    30 import java.util.Collections;
    31 import java.util.HashSet;
    32 import java.util.Map;
    33 import java.util.Set;
    35 /**
    36  * Immutable hash map implementation for properties.  Properties are keyed on strings.
    37  * Copying and cloning is avoided by relying on immutability.
    38  * <p>
    39  * 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  * of the bin list.  Similarly for removal, only a portion of a bin list is updated.
    42  * <p>
    43  * A separate chronological list is kept for quick generation of keys and values, and,
    44  * for rehashing.
    45  * <p>
    46  * Details:
    47  * <p>
    48  * 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
    50  * 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
    52  * 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.
    54  * <p>
    55  * The simplest of immutable maps is a basic singly linked list.  New properties
    56  * are simply added to the head of the list.  Ancestor maps are not affected by the
    57  * addition, since they continue to refer to their own head.  Searching is done by
    58  * walking linearly though the elements until a match is found, O(N).
    59  * <p>
    60  * A hash map can be thought of as an optimization of a linked list map, where the
    61  * linked list is broken into fragments based on hashCode(key) .  An array is use
    62  * to quickly reference these fragments, indexing on hashCode(key) mod tableSize
    63  * (tableSize is typically a power of 2 so that the mod is a fast masking
    64  * operation.)  If the size of the table is sufficient large, then search time
    65  * approaches O(1).  In fact, most bins in a hash table are typically empty or
    66  * contain a one element list.
    67  * <p>
    68  * For immutable hash maps, we can think of the hash map as an array of the shorter
    69  * linked list maps.  If we add an element to the head of one of those lists,  it
    70  * doesn't affect any ancestor maps.  Thus adding an element to an immutable hash
    71  * map only requires cloning the array and inserting an element at the head of one
    72  * of the bins.
    73  * <p>
    74  * Using Java HashMaps we don't have enough control over the entries to allow us to
    75  * implement this technique, so we are forced to clone the entire hash map.
    76  * <p>
    77  * Removing elements is done similarly.  We clone the array and then only modify
    78  * the bin containing the removed element.  More often than not, the list contains
    79  * only one element (or is very short), so this is not very costly.  When the list
    80  * has several items, we need to clone the list portion prior to the removed item.
    81  * <p>
    82  * Another requirement of property maps is that we need to be able to gather all
    83  * properties in chronological (add) order.  We have been using LinkedHashMap to
    84  * provide this.  For the implementation of immutable hash map, we use a singly
    85  * linked list that is linked in reverse chronological order.  This means we simply
    86  * add new entries to the head of the list.  If we need to work with the list in
    87  * forward order, it's simply a matter of allocating an array (size is known) and
    88  * back filling in reverse order.  Removal of elements from the chronological list
    89  * is trickier.  LinkedHashMap uses a doubly linked list to give constant time
    90  * removal. Immutable hash maps can't do that and maintain immutability.  So we
    91  * manage the chronological list the same way we manage the bins, cloning up to the
    92  * point of removal.  Don't panic.  This cost is more than offset by the cost of
    93  * cloning an entire LinkedHashMap.  Plus removal is far more rare than addition.
    94  * <p>
    95  * One more optimization.  Maps with a small number of entries don't use the hash
    96  * map at all, the chronological list is used instead.
    97  * <p>
    98  * So the benefits from immutable arrays are; fewer objects and less copying.  For
    99  * immutable hash map, when no removal is involved, the number of elements per
   100  * property is two (bin + chronological elements).  For LinkedHashMap it is one
   101  * (larger element) times the number of maps that refer to the property.  For
   102  * immutable hash map, addition is constant time.  For LinkedHashMap it's O(N+C)
   103  * since we have to clone the older map.
   104  */
   105 public final class PropertyHashMap implements Map <String, Property> {
   106     /** Number of initial bins. Power of 2. */
   107     private static final int INITIAL_BINS = 32;
   109     /** Threshold before using bins. */
   110     private static final int LIST_THRESHOLD = 8;
   112     /** Initial map. */
   113     public static final PropertyHashMap EMPTY_HASHMAP = new PropertyHashMap();
   115     /** Number of properties in the map. */
   116     private final int size;
   118     /** Threshold before growing the bins. */
   119     private final int threshold;
   121     /** Reverse list of all properties. */
   122     private final Element list;
   124     /** Hash map bins. */
   125     private final Element[] bins;
   127     /** All properties as an array (lazy). */
   128     private Property[] properties;
   130     /**
   131      * Empty map constructor.
   132      */
   133     private PropertyHashMap() {
   134         this.size      = 0;
   135         this.threshold = 0;
   136         this.bins      = null;
   137         this.list      = null;
   138     }
   140     /**
   141      * Clone Constructor
   142      *
   143      * @param map Original {@link PropertyHashMap}.
   144      */
   145     private PropertyHashMap(final PropertyHashMap map) {
   146         this.size      = map.size;
   147         this.threshold = map.threshold;
   148         this.bins      = map.bins;
   149         this.list      = map.list;
   150     }
   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     }
   166     /**
   167      * Clone a {@link PropertyHashMap} and add a {@link Property}.
   168      *
   169      * @param property {@link Property} to add.
   170      *
   171      * @return New {@link PropertyHashMap}.
   172      */
   173     public PropertyHashMap immutableAdd(final Property property) {
   174         final int newSize = size + 1;
   175         PropertyHashMap newMap = cloneMap(newSize);
   176         newMap = newMap.addNoClone(property);
   177         return newMap;
   178     }
   180     /**
   181      * Clone a {@link PropertyHashMap} and add an array of properties.
   182      *
   183      * @param newProperties Properties to add.
   184      *
   185      * @return New {@link PropertyHashMap}.
   186      */
   187     public PropertyHashMap immutableAdd(final Property... newProperties) {
   188         final int newSize = size + newProperties.length;
   189         PropertyHashMap newMap = cloneMap(newSize);
   190         for (final Property property : newProperties) {
   191             newMap = newMap.addNoClone(property);
   192         }
   193         return newMap;
   194     }
   196     /**
   197      * Clone a {@link PropertyHashMap} and add a collection of properties.
   198      *
   199      * @param newProperties Properties to add.
   200      *
   201      * @return New {@link PropertyHashMap}.
   202      */
   203     public PropertyHashMap immutableAdd(final Collection<Property> newProperties) {
   204         if (newProperties != null) {
   205             final int newSize = size + newProperties.size();
   206             PropertyHashMap newMap = cloneMap(newSize);
   207             for (final Property property : newProperties) {
   208                 newMap = newMap.addNoClone(property);
   209             }
   210             return newMap;
   211         }
   212         return this;
   213     }
   215     /**
   216      * Clone a {@link PropertyHashMap} and remove a {@link Property}.
   217      *
   218      * @param property {@link Property} to remove.
   219      *
   220      * @return New {@link PropertyHashMap}.
   221      */
   222     public PropertyHashMap immutableRemove(final Property property) {
   223         return immutableRemove(property.getKey());
   224     }
   226     /**
   227      * Clone a {@link PropertyHashMap} and remove a {@link Property} based on its key.
   228      *
   229      * @param key Key of {@link Property} to remove.
   230      *
   231      * @return New {@link PropertyHashMap}.
   232      */
   233     public PropertyHashMap immutableRemove(final String key) {
   234         if (bins != null) {
   235             final int binIndex = binIndex(bins, key);
   236             final Element bin = bins[binIndex];
   237             if (findElement(bin, key) != null) {
   238                 final int newSize = size - 1;
   239                 Element[] newBins = null;
   240                 if (newSize >= LIST_THRESHOLD) {
   241                     newBins = bins.clone();
   242                     newBins[binIndex] = removeFromList(bin, key);
   243                 }
   244                 final Element newList = removeFromList(list, key);
   245                 return new PropertyHashMap(newSize, newBins, newList);
   246             }
   247         } else if (findElement(list, key) != null) {
   248             final int newSize = size - 1;
   249             return newSize != 0 ? new PropertyHashMap(newSize, null, removeFromList(list, key)) : EMPTY_HASHMAP;
   250         }
   251         return this;
   252     }
   254     /**
   255      * Find a {@link Property} in the {@link PropertyHashMap}.
   256      *
   257      * @param key Key of {@link Property} to find.
   258      *
   259      * @return {@link Property} matching key or {@code null} if not found.
   260      */
   261     public Property find(final String key) {
   262         final Element element = findElement(key);
   263         return element != null ? element.getProperty() : null;
   264     }
   266     /**
   267      * Return an array of properties in chronological order of adding.
   268      *
   269      * @return Array of all properties.
   270      */
   271     Property[] getProperties() {
   272         if (properties == null) {
   273             final Property[] array = new Property[size];
   274             int i = size;
   275             for (Element element = list; element != null; element = element.getLink()) {
   276                 array[--i] = element.getProperty();
   277             }
   278             properties = array;
   279         }
   280         return properties;
   281     }
   283     /**
   284      * Returns the bin index from the key.
   285      *
   286      * @param bins     The bins array.
   287      * @param key      {@link Property} key.
   288      *
   289      * @return The bin index.
   290      */
   291     private static int binIndex(final Element[] bins, final String key) {
   292         return  key.hashCode() & (bins.length - 1);
   293     }
   295     /**
   296      * Calculate the number of bins needed to contain n properties.
   297      *
   298      * @param n Number of elements.
   299      *
   300      * @return Number of bins required.
   301      */
   302     private static int binsNeeded(final int n) {
   303         // 50% padding
   304         return 1 << (32 - Integer.numberOfLeadingZeros((n + (n >>> 1)) | (INITIAL_BINS - 1)));
   305     }
   307     /**
   308      * Used to calculate the current capacity of the bins.
   309      *
   310      * @param n Number of bin slots.
   311      *
   312      * @return 75% of n.
   313      */
   314     private static int threeQuarters(final int n) {
   315         return (n >>> 1) + (n >>> 2);
   316     }
   318     /**
   319      * Regenerate the bin table after changing the number of bins.
   320      *
   321      * @param list    // List of all properties.
   322      * @param binSize // New size of bins.
   323      *
   324      * @return Populated bins.
   325      */
   326     private static Element[] rehash(final Element list, final int binSize) {
   327         final Element[] newBins = new Element[binSize];
   328         for (Element element = list; element != null; element = element.getLink()) {
   329             final Property property = element.getProperty();
   330             final String key = property.getKey();
   331             final int binIndex = binIndex(newBins, key);
   332             newBins[binIndex] = new Element(newBins[binIndex], property);
   333         }
   334         return newBins;
   335     }
   337     /**
   338      * Locate an element based on key.
   339      *
   340      * @param key {@link Element} key.
   341      *
   342      * @return {@link Element} matching key or {@code null} if not found.
   343      */
   344     private Element findElement(final String key) {
   345         if (bins != null) {
   346             final int binIndex = binIndex(bins, key);
   347             return findElement(bins[binIndex], key);
   348         }
   349         return findElement(list, key);
   350     }
   352     /**
   353      * Locate an {@link Element} based on key from a specific list.
   354      *
   355      * @param elementList Head of {@link Element} list
   356      * @param key         {@link Element} key.
   357      * @return {@link Element} matching key or {@code null} if not found.
   358      */
   359     private static Element findElement(final Element elementList, final String key) {
   360         final int hashCode = key.hashCode();
   361         for (Element element = elementList; element != null; element = element.getLink()) {
   362             if (element.match(key, hashCode)) {
   363                 return element;
   364             }
   365         }
   366         return null;
   367     }
   369     /**
   370      * Clone {@link PropertyHashMap} to accommodate new size.
   371      *
   372      * @param newSize New size of {@link PropertyHashMap}.
   373      *
   374      * @return Cloned {@link PropertyHashMap} with new size.
   375      */
   376     private PropertyHashMap cloneMap(final int newSize) {
   377         Element[] newBins;
   378         if (bins == null && newSize <= LIST_THRESHOLD) {
   379             newBins = null;
   380         } else if (newSize > threshold) {
   381             newBins = rehash(list, binsNeeded(newSize));
   382         } else {
   383             newBins = bins.clone();
   384         }
   385         return new PropertyHashMap(newSize, newBins, list);
   386     }
   388     /**
   389      * Add a {@link Property} to a temporary {@link PropertyHashMap}, that has
   390      * been already cloned.  Removes duplicates if necessary.
   391      *
   392      * @param property {@link Property} to add.
   393      *
   394      * @return New {@link PropertyHashMap}.
   395      */
   396     private PropertyHashMap addNoClone(final Property property) {
   397         int newSize = size;
   398         final String key = property.getKey();
   399         Element newList = list;
   400         if (bins != null) {
   401             final int binIndex = binIndex(bins, key);
   402             Element bin = bins[binIndex];
   403             if (findElement(bin, key) != null) {
   404                 newSize--;
   405                 bin = removeFromList(bin, key);
   406                 newList = removeFromList(list, key);
   407             }
   408             bins[binIndex] = new Element(bin, property);
   409         } else {
   410             if (findElement(list, key) != null) {
   411                 newSize--;
   412                 newList = removeFromList(list, key);
   413             }
   414         }
   415         newList = new Element(newList, property);
   416         return new PropertyHashMap(newSize, bins, newList);
   417     }
   419     /**
   420      * Removes an {@link Element} from a specific list, avoiding duplication.
   421      *
   422      * @param list List to remove from.
   423      * @param key  Key of {@link Element} to remove.
   424      *
   425      * @return New list with {@link Element} removed.
   426      */
   427     private static Element removeFromList(final Element list, final String key) {
   428         if (list == null) {
   429             return null;
   430         }
   431         final int hashCode = key.hashCode();
   432         if (list.match(key, hashCode)) {
   433             return list.getLink();
   434         }
   435         final Element head = new Element(null, list.getProperty());
   436         Element previous = head;
   437         for (Element element = list.getLink(); element != null; element = element.getLink()) {
   438             if (element.match(key, hashCode)) {
   439                 previous.setLink(element.getLink());
   440                 return head;
   441             }
   442             final Element next = new Element(null, element.getProperty());
   443             previous.setLink(next);
   444             previous = next;
   445         }
   446         return list;
   447     }
   449     /*
   450      * Map implementation
   451      */
   453     @Override
   454     public int size() {
   455         return size;
   456     }
   458     @Override
   459     public boolean isEmpty() {
   460         return size == 0;
   461     }
   463     @Override
   464     public boolean containsKey(final Object key) {
   465         if (key instanceof String) {
   466             return findElement((String)key) != null;
   467         }
   468         assert key instanceof String;
   469         return false;
   470     }
   472     /**
   473      * Check if the map contains a key.
   474      *
   475      * @param key {@link Property} key.
   476      *
   477      * @return {@code true} of key is in {@link PropertyHashMap}.
   478      */
   479     public boolean containsKey(final String key) {
   480         return findElement(key) != null;
   481     }
   483     @Override
   484     public boolean containsValue(final Object value) {
   485         if (value instanceof Property) {
   486             final Property property = (Property) value;
   487             final Element element = findElement(property.getKey());
   488             return element != null && element.getProperty().equals(value);
   489         }
   490         return false;
   491     }
   493     @Override
   494     public Property get(final Object key) {
   495         if (key instanceof String) {
   496             final Element element = findElement((String)key);
   497             return element != null ? element.getProperty() : null;
   498         }
   499         assert key instanceof String;
   500         return null;
   501     }
   503     /**
   504      * Get the {@link Property} given a key that is an explicit {@link String}.
   505      * See also {@link PropertyHashMap#get(Object)}
   506      *
   507      * @param key {@link Property} key.
   508      *
   509      * @return {@link Property}, or {@code null} if no property with that key was found.
   510      */
   511     public Property get(final String key) {
   512         final Element element = findElement(key);
   513         return element != null ? element.getProperty() : null;
   514     }
   516     @Override
   517     public Property put(final String key, final Property value) {
   518         throw new UnsupportedOperationException("Immutable map.");
   519     }
   521     @Override
   522     public Property remove(final Object key) {
   523         throw new UnsupportedOperationException("Immutable map.");
   524     }
   526     @Override
   527     public void putAll(final Map<? extends String, ? extends Property> m) {
   528         throw new UnsupportedOperationException("Immutable map.");
   529     }
   531     @Override
   532     public void clear() {
   533         throw new UnsupportedOperationException("Immutable map.");
   534     }
   536     @Override
   537     public Set<String> keySet() {
   538         final HashSet<String> set = new HashSet<>();
   539         for (Element element = list; element != null; element = element.getLink()) {
   540             set.add(element.getKey());
   541         }
   542         return Collections.unmodifiableSet(set);
   543     }
   545     @Override
   546     public Collection<Property> values() {
   547         return Collections.unmodifiableList(Arrays.asList(getProperties()));
   548     }
   550     @Override
   551     public Set<Entry<String, Property>> entrySet() {
   552         final HashSet<Entry<String, Property>> set = new HashSet<>();
   553         for (Element element = list; element != null; element = element.getLink()) {
   554             set.add(element);
   555         }
   556         return Collections.unmodifiableSet(set);
   557     }
   559     /**
   560      * List map element.
   561      */
   562     static final class Element implements Entry<String, Property> {
   563         /** Link for list construction. */
   564         private Element link;
   566         /** Element property. */
   567         private final Property property;
   569         /** Element key. Kept separate for performance.) */
   570         private final String key;
   572         /** Element key hash code. */
   573         private final int hashCode;
   575         /*
   576          * Constructors
   577          */
   579         Element(final Element link, final Property property) {
   580             this.link     = link;
   581             this.property = property;
   582             this.key      = property.getKey();
   583             this.hashCode = this.key.hashCode();
   584         }
   586         boolean match(final String otherKey, final int otherHashCode) {
   587             return this.hashCode == otherHashCode && this.key.equals(otherKey);
   588         }
   590         /*
   591          * Entry implmentation.
   592          */
   594         @Override
   595         public boolean equals(final Object other) {
   596             assert property != null && other != null;
   597             return other instanceof Element && property.equals(((Element)other).property);
   598         }
   600         @Override
   601         public String getKey() {
   602             return key;
   603         }
   605         @Override
   606         public Property getValue() {
   607             return property;
   608         }
   610         @Override
   611         public int hashCode() {
   612             return hashCode;
   613         }
   615         @Override
   616         public Property setValue(final Property value) {
   617             throw new UnsupportedOperationException("Immutable map.");
   618         }
   620         /*
   621          * Accessors
   622          */
   624         Element getLink() {
   625             return link;
   626         }
   628         void setLink(final Element link) {
   629             this.link = link;
   630         }
   632         Property getProperty() {
   633             return property;
   634         }
   635     }
   637 }

mercurial