Add a hashing layer to LinkedTreeMap. Instead of having 1 root node, the class now has several root nodes, one for each hash bucket in a hash table.
Compared to LinkedTreeMap, this is slower for small (size=5) maps: 124% slower to get() and 33% slower to create and populate. It's a win for large (size=500) maps: 46% faster to get() but 8% slower to create and populate. And it's a big win for very large (size=50,000) maps: 81% faster to get() and 46% faster to create and populate. http://microbenchmarks.appspot.com/run/limpbizkit@gmail.com/com.google.common.collect.MapBenchmark I'm going to follow this up with some simple optimizations: caching local fields and simplifying access. That should narrow the performance gap.
This commit is contained in:
parent
a0493b9732
commit
aceadaecf1
|
@ -22,11 +22,11 @@ import java.io.Serializable;
|
|||
import java.util.AbstractMap;
|
||||
import java.util.AbstractSet;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.ConcurrentModificationException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -47,10 +47,11 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
};
|
||||
|
||||
Comparator<? super K> comparator;
|
||||
Node<K, V> root;
|
||||
Node<K, V>[] table;
|
||||
final Node<K, V> header;
|
||||
int size = 0;
|
||||
int modCount = 0;
|
||||
int threshold;
|
||||
|
||||
/**
|
||||
* Create a natural order, empty tree map whose keys must be mutually
|
||||
|
@ -74,6 +75,8 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
? comparator
|
||||
: (Comparator) NATURAL_ORDER;
|
||||
this.header = new Node<K, V>();
|
||||
this.table = new Node[16]; // TODO: sizing/resizing policies
|
||||
this.threshold = (table.length / 2) + (table.length / 4); // 3/4 capacity
|
||||
}
|
||||
|
||||
@Override public int size() {
|
||||
|
@ -100,9 +103,19 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
}
|
||||
|
||||
@Override public void clear() {
|
||||
root = null;
|
||||
Arrays.fill(table, null);
|
||||
size = 0;
|
||||
modCount++;
|
||||
|
||||
// Clear all links to help GC
|
||||
Node<K, V> header = this.header;
|
||||
for (Node<K, V> e = header.next; e != header; ) {
|
||||
Node<K, V> next = e.next;
|
||||
e.next = e.prev = null;
|
||||
e = next;
|
||||
}
|
||||
|
||||
header.next = header.prev = header;
|
||||
}
|
||||
|
||||
@Override public V remove(Object key) {
|
||||
|
@ -117,21 +130,28 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
* mutually comparable.
|
||||
*/
|
||||
Node<K, V> find(K key, boolean create) {
|
||||
if (root == null) {
|
||||
int hash = secondaryHash(key.hashCode());
|
||||
int index = hash & (table.length - 1);
|
||||
|
||||
if (table[index] == null) {
|
||||
if (comparator == NATURAL_ORDER && !(key instanceof Comparable)) {
|
||||
throw new ClassCastException(key.getClass().getName() + " is not Comparable");
|
||||
}
|
||||
if (create) {
|
||||
root = new Node<K, V>(null, key, header, header);
|
||||
size = 1;
|
||||
Node<K, V> created = new Node<K, V>(null, key, hash, header, header.prev);
|
||||
size++;
|
||||
table[index] = created;
|
||||
modCount++;
|
||||
return root;
|
||||
if (size > threshold) {
|
||||
doubleCapacity();
|
||||
}
|
||||
return created;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Node<K, V> nearest = root;
|
||||
Node<K, V> nearest = table[index];
|
||||
while (true) {
|
||||
int comparison = comparator.compare(key, nearest.key);
|
||||
|
||||
|
@ -149,7 +169,7 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
|
||||
// The key doesn't exist in this tree. Create it here, then rebalance.
|
||||
if (create) {
|
||||
Node<K, V> created = new Node<K, V>(nearest, key, header, header.prev);
|
||||
Node<K, V> created = new Node<K, V>(nearest, key, hash, header, header.prev);
|
||||
if (comparison < 0) { // nearest.key is higher
|
||||
nearest.left = created;
|
||||
} else { // comparison > 0, nearest.key is lower
|
||||
|
@ -158,6 +178,9 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
size++;
|
||||
modCount++;
|
||||
rebalance(nearest, true);
|
||||
if (size > threshold) {
|
||||
doubleCapacity();
|
||||
}
|
||||
return created;
|
||||
} else {
|
||||
return null;
|
||||
|
@ -189,6 +212,18 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
return a == b || (a != null && a.equals(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a supplemental hash function to a given hashCode, which defends
|
||||
* against poor quality hash functions. This is critical because HashMap
|
||||
* uses power-of-two length hash tables, that otherwise encounter collisions
|
||||
* for hashCodes that do not differ in lower or upper bits.
|
||||
*/
|
||||
private static int secondaryHash(int h) {
|
||||
// Doug Lea's supplemental hash function
|
||||
h ^= (h >>> 20) ^ (h >>> 12);
|
||||
return h ^ (h >>> 7) ^ (h >>> 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes {@code node} from this tree, rearranging the tree's structure as
|
||||
* necessary.
|
||||
|
@ -276,7 +311,8 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
parent.right = replacement;
|
||||
}
|
||||
} else {
|
||||
root = replacement;
|
||||
int index = node.hash & (table.length - 1);
|
||||
table[index] = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -416,27 +452,29 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
return result != null ? result : (keySet = new KeySet());
|
||||
}
|
||||
|
||||
static class Node<K, V> implements Map.Entry<K, V> {
|
||||
static class Node<K, V> implements Entry<K, V> {
|
||||
Node<K, V> parent;
|
||||
Node<K, V> left;
|
||||
Node<K, V> right;
|
||||
Node<K, V> next;
|
||||
Node<K, V> prev;
|
||||
final K key;
|
||||
final int hash;
|
||||
V value;
|
||||
int height;
|
||||
int hash;
|
||||
|
||||
/** Create the header entry */
|
||||
Node() {
|
||||
key = null;
|
||||
hash = -1;
|
||||
next = prev = this;
|
||||
}
|
||||
|
||||
/** Create a regular entry */
|
||||
Node(Node<K, V> parent, K key, Node<K, V> next, Node<K, V> prev) {
|
||||
Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
|
||||
this.parent = parent;
|
||||
this.key = key;
|
||||
this.hash = hash;
|
||||
this.height = 1;
|
||||
this.next = next;
|
||||
this.prev = prev;
|
||||
|
@ -459,8 +497,8 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if (o instanceof Map.Entry) {
|
||||
Map.Entry other = (Map.Entry) o;
|
||||
if (o instanceof Entry) {
|
||||
Entry other = (Entry) o;
|
||||
return (key == null ? other.getKey() == null : key.equals(other.getKey()))
|
||||
&& (value == null ? other.getValue() == null : value.equals(other.getValue()));
|
||||
}
|
||||
|
@ -503,6 +541,11 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
}
|
||||
}
|
||||
|
||||
private void doubleCapacity() {
|
||||
table = doubleCapacity(table);
|
||||
threshold = (table.length / 2) + (table.length / 4); // 3/4 capacity
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array containing the same nodes as {@code oldTable}, but with
|
||||
* twice as many trees, each of (approximately) half the previous size.
|
||||
|
@ -726,13 +769,13 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
|
|||
if (lastReturned == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
LinkedTreeMap.this.removeInternal(lastReturned, true);
|
||||
removeInternal(lastReturned, true);
|
||||
lastReturned = null;
|
||||
expectedModCount = modCount;
|
||||
}
|
||||
}
|
||||
|
||||
class EntrySet extends AbstractSet<Map.Entry<K, V>> {
|
||||
class EntrySet extends AbstractSet<Entry<K, V>> {
|
||||
@Override public int size() {
|
||||
return size;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package com.google.gson.internal;
|
||||
|
||||
import com.google.gson.internal.LinkedTreeMap.AvlBuilder;
|
||||
import com.google.gson.internal.LinkedTreeMap.AvlIterator;
|
||||
import com.google.gson.internal.LinkedTreeMap.Node;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -72,6 +73,16 @@ public final class LinkedTreeMapTest extends TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
public void testClear() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<String, String>();
|
||||
map.put("a", "android");
|
||||
map.put("c", "cola");
|
||||
map.put("b", "bbq");
|
||||
map.clear();
|
||||
assertIterationOrder(map.keySet());
|
||||
assertEquals(0, map.size());
|
||||
}
|
||||
|
||||
public void testAvlWalker() {
|
||||
assertAvlWalker(node(node("a"), "b", node("c")),
|
||||
"a", "b", "c");
|
||||
|
@ -86,7 +97,7 @@ public final class LinkedTreeMapTest extends TestCase {
|
|||
}
|
||||
|
||||
private void assertAvlWalker(Node<String, String> root, String... values) {
|
||||
LinkedTreeMap.AvlIterator<String, String> iterator = new LinkedTreeMap.AvlIterator<String, String>();
|
||||
AvlIterator<String, String> iterator = new AvlIterator<String, String>();
|
||||
iterator.reset(root);
|
||||
for (String value : values) {
|
||||
assertTrue(iterator.hasNext());
|
||||
|
@ -141,9 +152,7 @@ public final class LinkedTreeMapTest extends TestCase {
|
|||
private static final Node<String, String> head = new Node<String, String>();
|
||||
|
||||
private Node<String, String> node(String value) {
|
||||
Node<String, String> result = new Node<String, String>(null, value, head, head);
|
||||
result.hash = value.hashCode();
|
||||
return result;
|
||||
return new Node<String, String>(null, value, value.hashCode(), head, head);
|
||||
}
|
||||
|
||||
private Node<String, String> node(Node<String, String> left, String value,
|
||||
|
|
Loading…
Reference in New Issue