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:
Jesse Wilson 2012-09-17 00:19:44 +00:00
parent a0493b9732
commit aceadaecf1
2 changed files with 73 additions and 21 deletions

View File

@ -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;
}

View File

@ -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,