OpenJDK / jdk8 / jdk8 / jdk
changeset 6849:0cccdb9a9a4c
7143928: Optimize empty HashMap and ArrayList
Reviewed-by: mduigou
Contributed-by: Sergey Linetskiy <sergey.linetskiy@oracle.com>, John Rose <john.rose@oracle.com>, Mike Duigou <mike.duigou@oracle.com>
author | mduigou |
---|---|
date | Mon, 01 Apr 2013 20:15:48 -0700 |
parents | b590bd37a6d0 |
children | 5ee837ba093a |
files | src/share/classes/java/util/ArrayList.java src/share/classes/java/util/HashMap.java test/java/util/Map/BasicSerialization.java |
diffstat | 3 files changed, 337 insertions(+), 52 deletions(-) [+] |
line wrap: on
line diff
--- a/src/share/classes/java/util/ArrayList.java Mon Apr 01 12:02:19 2013 -0700 +++ b/src/share/classes/java/util/ArrayList.java Mon Apr 01 20:15:48 2013 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -105,6 +105,11 @@ private static final long serialVersionUID = 8683452581122892189L; /** + * Shared empty array instance used for empty instances. + */ + private static final Object EMPTY_ELEMENTDATA[] = new Object[0]; + + /** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. */ @@ -136,7 +141,8 @@ * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { - this(10); + super(); + this.elementData = EMPTY_ELEMENTDATA; } /** @@ -162,8 +168,7 @@ */ public void trimToSize() { modCount++; - int oldCapacity = elementData.length; - if (size < oldCapacity) { + if (size < elementData.length) { elementData = Arrays.copyOf(elementData, size); } } @@ -177,11 +182,20 @@ */ public void ensureCapacity(int minCapacity) { if (minCapacity > 0) - ensureCapacityInternal(minCapacity); + ensureExplicitCapacity(minCapacity); } private void ensureCapacityInternal(int minCapacity) { + if(elementData == EMPTY_ELEMENTDATA) { + minCapacity = Math.max(10, minCapacity); + } + + ensureExplicitCapacity(minCapacity); + } + + private void ensureExplicitCapacity(int minCapacity) { modCount++; + // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); @@ -506,8 +520,7 @@ modCount++; // Let gc do its work - for (int i = 0; i < size; i++) - elementData[i] = null; + Arrays.fill(elementData, null); size = 0; } @@ -588,8 +601,8 @@ // Let gc do its work int newSize = size - (toIndex-fromIndex); - while (size != newSize) - elementData[--size] = null; + Arrays.fill(elementData, newSize, size, null); + size = newSize; } /** @@ -677,8 +690,8 @@ w += size - r; } if (w != size) { - for (int i = w; i < size; i++) - elementData[i] = null; + // Let gc do its work + Arrays.fill(elementData, w, size, null); modCount += size - w; size = w; modified = true; @@ -702,7 +715,7 @@ s.defaultWriteObject(); // Write out array length - s.writeInt(elementData.length); + s.writeInt((elementData == EMPTY_ELEMENTDATA) ? 10 : elementData.length); // Write out all elements in the proper order. for (int i=0; i<size; i++) @@ -711,7 +724,6 @@ if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } - } /** @@ -723,10 +735,16 @@ // Read in size, and any hidden stuff s.defaultReadObject(); - // Read in array length and allocate array - int arrayLength = s.readInt(); - Object[] a = elementData = new Object[arrayLength]; + // Read in array length + int initialCapacity = s.readInt(); + elementData = EMPTY_ELEMENTDATA; + if((size > 0) || (initialCapacity != 10)) { + // allocate array based upon size. + ensureCapacityInternal(size); + } + + Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) a[i] = s.readObject();
--- a/src/share/classes/java/util/HashMap.java Mon Apr 01 12:02:19 2013 -0700 +++ b/src/share/classes/java/util/HashMap.java Mon Apr 01 20:15:48 2013 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -144,9 +144,14 @@ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** + * An empty table instance to share when the table is not inflated. + */ + static final Entry<?,?>[] EMPTY_TABLE = {}; + + /** * The table, resized as necessary. Length MUST Always be a power of two. */ - transient Entry<?,?>[] table; + transient Entry<?,?>[] table = EMPTY_TABLE; /** * The number of key-value mappings contained in this map. @@ -223,14 +228,8 @@ throw new IllegalArgumentException("Illegal load factor: " + loadFactor); - // Find a power of 2 >= initialCapacity - int capacity = 1; - while (capacity < initialCapacity) - capacity <<= 1; - this.loadFactor = loadFactor; - threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); - table = new Entry<?,?>[capacity]; + threshold = initialCapacity; init(); } @@ -265,9 +264,30 @@ public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); + inflateTable(threshold); + putAllForCreate(m); } + private static int roundUpToPowerOf2(int number) { + int rounded = (rounded = Integer.highestOneBit(number)) != 0 + ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded + : 1; + + return rounded; + } + + /** + * Inflate the table + */ + final void inflateTable(int toSize) { + // Find a power of 2 >= initialCapacity + int capacity = roundUpToPowerOf2(toSize); + + threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); + table = new Entry[capacity]; + } + // internal utilities /** @@ -305,6 +325,7 @@ * Returns index for hash code h. */ static int indexFor(int h, int length) { + if (Integer.bitCount(length) != 1) { throw new Error("Ya dun messed up good"); } return h & (length-1); } @@ -369,6 +390,10 @@ */ @SuppressWarnings("unchecked") final Entry<K,V> getEntry(Object key) { + if (isEmpty()) { + return null; + } + int hash = (key == null) ? 0 : hash(key); for (Entry<?,?> e = table[indexFor(hash, table.length)]; e != null; @@ -381,7 +406,6 @@ return null; } - /** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old @@ -395,6 +419,9 @@ * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { + if (table == EMPTY_TABLE) { + inflateTable(threshold); + } if (key == null) return putForNullKey(value); int hash = hash(key); @@ -529,6 +556,10 @@ if (numKeysToBeAdded == 0) return; + if (table == EMPTY_TABLE) { + inflateTable(Math.max((int) (numKeysToBeAdded * loadFactor), threshold)); + } + /* * Expand the map if the map if the number of mappings to be added * is greater than or equal to threshold. This is conservative; the @@ -573,6 +604,9 @@ * for this key. */ final Entry<K,V> removeEntryForKey(Object key) { + if(isEmpty()) { + return null; + } int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); @SuppressWarnings("unchecked") @@ -605,7 +639,7 @@ * for matching. */ final Entry<K,V> removeMapping(Object o) { - if (!(o instanceof Map.Entry)) + if (isEmpty() || !(o instanceof Map.Entry)) return null; Map.Entry<?,?> entry = (Map.Entry<?,?>) o; @@ -641,9 +675,7 @@ */ public void clear() { modCount++; - Entry<?,?>[] tab = table; - for (int i = 0; i < tab.length; i++) - tab[i] = null; + Arrays.fill(table, null); size = 0; } @@ -656,6 +688,10 @@ * specified value */ public boolean containsValue(Object value) { + if(isEmpty()) { + return false; + } + if (value == null) return containsNullValue(); @@ -693,7 +729,9 @@ } catch (CloneNotSupportedException e) { // assert false; } - result.table = new Entry<?,?>[table.length]; + result.table = (table == EMPTY_TABLE) + ? EMPTY_TABLE + : new Entry<?,?>[table.length]; result.entrySet = null; result.modCount = 0; result.size = 0; @@ -749,8 +787,7 @@ } public final int hashCode() { - return (key==null ? 0 : key.hashCode()) ^ - (value==null ? 0 : value.hashCode()); + return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { @@ -1008,7 +1045,7 @@ * serialize it). * * @serialData The <i>capacity</i> of the HashMap (the length of the - * bucket array) is emitted (int), followed by the + * bucket array, a power of 2) is emitted (int), followed by the * <i>size</i> (an int, the number of key-value * mappings), followed by the key (Object) and value (Object) * for each key-value mapping. The key-value mappings are @@ -1017,14 +1054,14 @@ private void writeObject(java.io.ObjectOutputStream s) throws IOException { - Iterator<Map.Entry<K,V>> i = - (size > 0) ? entrySet0().iterator() : null; - // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); // Write out number of buckets - s.writeInt(table.length); + if (table==EMPTY_TABLE) + s.writeInt(roundUpToPowerOf2(threshold)); + else + s.writeInt(table.length); // Write out size (number of Mappings) s.writeInt(size); @@ -1058,7 +1095,15 @@ sun.misc.Hashing.randomHashSeed(this)); // Read in number of buckets and allocate the bucket array; - s.readInt(); // ignored + table = EMPTY_TABLE; + + int buckets = s.readInt(); + + if ((buckets < 0) || // negative + (buckets > HashMap.MAXIMUM_CAPACITY) || // fits in array + (Integer.bitCount(buckets) > 1)) /* not power of 2 or zero */ { + throw new InvalidObjectException("Illegal capacity: " + buckets); + } // Read number of mappings int mappings = s.readInt(); @@ -1066,23 +1111,24 @@ throw new InvalidObjectException("Illegal mappings count: " + mappings); - int initialCapacity = (int) Math.min( - // capacity chosen by number of mappings - // and desired load (if >= 0.25) - mappings * Math.min(1 / loadFactor, 4.0f), - // we have limits... - HashMap.MAXIMUM_CAPACITY); - int capacity = 1; - // find smallest power of two which holds all mappings - while (capacity < initialCapacity) { - capacity <<= 1; + int mappingsCapacity = Math.max( + (int) Math.min( + // capacity chosen by number of mappings + // and desired load (if >= 0.25) + mappings * Math.min(1 / loadFactor, 4.0f), + // we have limits... + HashMap.MAXIMUM_CAPACITY), + // maybe they want extra buckets. + buckets); + + if(mappings > 0) { + inflateTable(mappingsCapacity); + } else { + threshold = mappingsCapacity; } - table = new Entry<?,?>[capacity]; - threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); init(); // Give subclass a chance to do its thing. - // Read the keys and values, and put the mappings in the HashMap for (int i=0; i<mappings; i++) { @SuppressWarnings("unchecked")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/java/util/Map/BasicSerialization.java Mon Apr 01 20:15:48 2013 -0700 @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 7143928 + * @run testng BasicSerialization + * @summary Ensure Maps can be serialized and deserialized. + * @author Mike Duigou + */ +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; + +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; +import static org.testng.Assert.fail; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertSame; + +public class BasicSerialization { + + enum IntegerEnum { + + e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, + e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, + e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, + e30, e31, e32, e33, e34, e35, e36, e37, e38, e39, + e40, e41, e42, e43, e44, e45, e46, e47, e48, e49, + e50, e51, e52, e53, e54, e55, e56, e57, e58, e59, + e60, e61, e62, e63, e64, e65, e66, e67, e68, e69, + e70, e71, e72, e73, e74, e75, e76, e77, e78, e79, + e80, e81, e82, e83, e84, e85, e86, e87, e88, e89, + e90, e91, e92, e93, e94, e95, e96, e97, e98, e99, + EXTRA_KEY; + public static final int SIZE = values().length; + }; + private static final int TEST_SIZE = IntegerEnum.SIZE - 1; + /** + * Realized keys ensure that there is always a hard ref to all test objects. + */ + private static final IntegerEnum[] KEYS = new IntegerEnum[TEST_SIZE]; + /** + * Realized values ensure that there is always a hard ref to all test + * objects. + */ + private static final String[] VALUES = new String[TEST_SIZE]; + + static { + IntegerEnum[] keys = IntegerEnum.values(); + for (int each = 0; each < TEST_SIZE; each++) { + KEYS[each] = keys[each]; + VALUES[each] = keys[each].name(); + } + } + private static final IntegerEnum EXTRA_KEY = IntegerEnum.EXTRA_KEY; + private static final String EXTRA_VALUE = IntegerEnum.EXTRA_KEY.name(); + + public static <K, V> Map<K, V> mapClone(Map<K, V> map) { + Method cloneMethod; + + try { + cloneMethod = map.getClass().getMethod("clone", new Class[]{}); + } catch (NoSuchMethodException | SecurityException all) { + cloneMethod = null; + } + + if (null != cloneMethod) { + try { + Map<K, V> result = (Map<K, V>)cloneMethod.invoke(map, new Object[]{}); + return result; + } catch (Exception all) { + fail("clone() failed " + map.getClass().getSimpleName(), all); + return null; + } + } else { + Constructor<? extends Map> copyConstructor; + try { + copyConstructor = (Constructor<? extends Map>)map.getClass().getConstructor(new Class[]{Map.class}); + + Map<K, V> result = (Map<K, V>)copyConstructor.newInstance(new Object[]{map}); + + return result; + } catch (Exception all) { + return serialClone(map); + } + } + } + + @Test(dataProvider = "Map<IntegerEnum,String>") + public void testSerialization(String description, Map<IntegerEnum, String> map) { + Object foo = new Object(); + + Map<IntegerEnum, String> clone = mapClone(map); + Map<IntegerEnum, String> serialClone = serialClone(map); + + assertEquals(map, map, description + ":should equal self"); + assertEquals(clone, map, description + ":should equal clone"); + assertEquals(map, clone, description + ": should equal orginal map"); + assertEquals(serialClone, map, description + ": should equal deserialized clone"); + assertEquals(map, serialClone, description + ": should equal original map"); + assertEquals(serialClone, clone, description + ": deserialized clone should equal clone"); + assertEquals(clone, serialClone, description + ": clone should equal deserialized clone"); + + assertFalse(map.containsKey(EXTRA_KEY), description + ":unexpected key"); + assertFalse(clone.containsKey(EXTRA_KEY), description + ":unexpected key"); + assertFalse(serialClone.containsKey(EXTRA_KEY), description + ":unexpected key"); + map.put(EXTRA_KEY, EXTRA_VALUE); + clone.put(EXTRA_KEY, EXTRA_VALUE); + serialClone.put(EXTRA_KEY, EXTRA_VALUE); + assertTrue(map.containsKey(EXTRA_KEY), description + ":missing key"); + assertTrue(clone.containsKey(EXTRA_KEY), description + ":missing key"); + assertTrue(serialClone.containsKey(EXTRA_KEY), description + ":missing key"); + assertSame(map.get(EXTRA_KEY), EXTRA_VALUE, description + ":wrong value"); + assertSame(clone.get(EXTRA_KEY), EXTRA_VALUE, description + ":wrong value"); + assertSame(serialClone.get(EXTRA_KEY), EXTRA_VALUE, description + ":wrong value"); + + assertEquals(map, map, description + ":should equal self"); + assertEquals(clone, map, description + ":should equal clone"); + assertEquals(map, clone, description + ": should equal orginal map"); + assertEquals(serialClone, map, description + ": should equal deserialized clone"); + assertEquals(map, serialClone, description + ": should equal original map"); + assertEquals(serialClone, clone, description + ": deserialized clone should equal clone"); + assertEquals(clone, serialClone, description + ": clone should equal deserialized clone"); + } + + static byte[] serializedForm(Object obj) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ObjectOutputStream(baos).writeObject(obj); + return baos.toByteArray(); + } catch (IOException e) { + fail("Unexpected Exception", e); + return null; + } + } + + static Object readObject(byte[] bytes) throws IOException, ClassNotFoundException { + InputStream is = new ByteArrayInputStream(bytes); + return new ObjectInputStream(is).readObject(); + } + + @SuppressWarnings("unchecked") + static <T> T serialClone(T obj) { + try { + return (T)readObject(serializedForm(obj)); + } catch (IOException | ClassNotFoundException e) { + fail("Unexpected Exception", e); + return null; + } + } + + @DataProvider(name = "Map<IntegerEnum,String>", parallel = true) + private static Iterator<Object[]> makeMaps() { + return Arrays.asList( + // empty + new Object[]{"HashMap", new HashMap()}, + new Object[]{"LinkedHashMap", new LinkedHashMap()}, + new Object[]{"Collections.checkedMap(HashMap)", Collections.checkedMap(new HashMap(), IntegerEnum.class, String.class)}, + new Object[]{"Collections.synchronizedMap(HashMap)", Collections.synchronizedMap(new HashMap())}, + // null hostile + new Object[]{"EnumMap", new EnumMap(IntegerEnum.class)}, + new Object[]{"Hashtable", new Hashtable()}, + new Object[]{"TreeMap", new TreeMap()}, + new Object[]{"ConcurrentHashMap", new ConcurrentHashMap()}, + new Object[]{"ConcurrentSkipListMap", new ConcurrentSkipListMap()}, + new Object[]{"Collections.checkedMap(ConcurrentHashMap)", Collections.checkedMap(new ConcurrentHashMap(), IntegerEnum.class, String.class)}, + new Object[]{"Collections.synchronizedMap(EnumMap)", Collections.synchronizedMap(new EnumMap(IntegerEnum.class))}, + // filled + new Object[]{"HashMap", fillMap(new HashMap())}, + new Object[]{"LinkedHashMap", fillMap(new LinkedHashMap())}, + new Object[]{"Collections.checkedMap(HashMap)", Collections.checkedMap(fillMap(new HashMap()), IntegerEnum.class, String.class)}, + new Object[]{"Collections.synchronizedMap(HashMap)", Collections.synchronizedMap(fillMap(new HashMap()))}, + // null hostile + new Object[]{"EnumMap", fillMap(new EnumMap(IntegerEnum.class))}, + new Object[]{"Hashtable", fillMap(new Hashtable())}, + new Object[]{"TreeMap", fillMap(new TreeMap())}, + new Object[]{"ConcurrentHashMap", fillMap(new ConcurrentHashMap())}, + new Object[]{"ConcurrentSkipListMap", fillMap(new ConcurrentSkipListMap())}, + new Object[]{"Collections.checkedMap(ConcurrentHashMap)", Collections.checkedMap(fillMap(new ConcurrentHashMap()), IntegerEnum.class, String.class)}, + new Object[]{"Collections.synchronizedMap(EnumMap)", Collections.synchronizedMap(fillMap(new EnumMap(IntegerEnum.class)))}).iterator(); + } + + private static Map<IntegerEnum, String> fillMap(Map<IntegerEnum, String> result) { + for (int each = 0; each < TEST_SIZE; each++) { + result.put(KEYS[each], VALUES[each]); + } + + return result; + } +}