CraftBukkit/src/main/java/org/bukkit/craftbukkit/util/LongObjectHashMap.java
Travis Watkins 74b3be57b7 Replace LongObjectHashMap with a more efficient implementation
After further testing it appears that while the original LongHashtable
has issues with object creation churn and is severly slower than even
java.util.HashMap in general case benchmarks it is in fact very efficient
for our use case.

With this in mind I wrote a replacement LongObjectHashMap modeled after
LongHashtable. Unlike the original implementation this one does not use
Entry objects for storage so does not have the same object creation churn.
It also uses a 2D array instead of a 3D one and does not use a cache as
benchmarking shows this is more efficient. The "bucket size" was chosen
based on benchmarking performance of the HashMap with contents that would
be plausible for a 200+ player server. This means it uses a little extra
memory for smaller servers but almost always uses less than the normal
java.util.HashMap.

To make up for the original LongHashtable being a poor choice for generic
datasets I added a mixer to the new implementation based on code from
MurmurHash. While this has no noticable effect positive or negative with
our normal use of chunk coordinates it makes the HashMap perform just as
well with nearly any kind of dataset.

After these changes ChunkProviderServer.isChunkLoaded() goes from using
20% CPU time while sampling to not even showing up after 45 minutes of
sampling due to the CPU usage being too low to be noticed.
2012-09-21 11:20:10 -05:00

425 lines
11 KiB
Java

package org.bukkit.craftbukkit.util;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import static org.bukkit.craftbukkit.util.Java15Compat.Arrays_copyOf;
@SuppressWarnings("unchecked")
public class LongObjectHashMap<V> implements Cloneable, Serializable {
static final long serialVersionUID = 2841537710170573815L;
private static final long EMPTY_KEY = Long.MIN_VALUE;
private static final int BUCKET_SIZE = 4096;
private transient long[][] keys;
private transient V[][] values;
private transient int modCount;
private transient int size;
public LongObjectHashMap() {
initialize();
}
public LongObjectHashMap(Map<? extends Long, ? extends V> map) {
this();
putAll(map);
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean containsKey(long key) {
return get(key) != null;
}
public boolean containsValue(V value) {
for (V val : values()) {
if (val == value || val.equals(value)) {
return true;
}
}
return false;
}
public V get(long key) {
int index = (int) (keyIndex(key) & (BUCKET_SIZE - 1));
long[] inner = keys[index];
if (inner == null) return null;
for (int i = 0; i < inner.length; i++) {
long innerKey = inner[i];
if (innerKey == EMPTY_KEY) {
return null;
} else if (innerKey == key) {
return values[index][i];
}
}
return null;
}
public V put(long key, V value) {
int index = (int) (keyIndex(key) & (BUCKET_SIZE - 1));
long[] innerKeys = keys[index];
V[] innerValues = values[index];
modCount++;
if (innerKeys == null) {
// need to make a new chain
keys[index] = innerKeys = new long[8];
Arrays.fill(innerKeys, EMPTY_KEY);
values[index] = innerValues = (V[]) new Object[8];
innerKeys[0] = key;
innerValues[0] = value;
size++;
} else {
int i;
for (i = 0; i < innerKeys.length; i++) {
// found an empty spot in the chain to put this
if (innerKeys[i] == EMPTY_KEY) {
size++;
innerKeys[i] = key;
innerValues[i] = value;
return null;
}
// found an existing entry in the chain with this key, replace it
if (innerKeys[i] == key) {
V oldValue = innerValues[i];
innerKeys[i] = key;
innerValues[i] = value;
return oldValue;
}
}
// chain is full, resize it and add our new entry
keys[index] = innerKeys = Arrays_copyOf(innerKeys, i << 1);
Arrays.fill(innerKeys, i, innerKeys.length, EMPTY_KEY);
values[index] = innerValues = Arrays_copyOf(innerValues, i << 1);
innerKeys[i] = key;
innerValues[i] = value;
size++;
}
return null;
}
public V remove(long key) {
int index = (int) (keyIndex(key) & (BUCKET_SIZE - 1));
long[] inner = keys[index];
if (inner == null) {
return null;
}
for (int i = 0; i < inner.length; i++) {
// hit the end of the chain, didn't find this entry
if (inner[i] == EMPTY_KEY) {
break;
}
if (inner[i] == key) {
V value = values[index][i];
for (i++; i < inner.length; i++) {
if (inner[i] == EMPTY_KEY) {
break;
}
inner[i - 1] = inner[i];
values[index][i - 1] = values[index][i];
}
inner[i - 1] = EMPTY_KEY;
values[index][i - 1] = null;
size--;
modCount++;
return value;
}
}
return null;
}
public void putAll(Map<? extends Long, ? extends V> map) {
for (Map.Entry entry : map.entrySet()) {
put((Long) entry.getKey(), (V) entry.getValue());
}
}
public void clear() {
if (size == 0) {
return;
}
modCount++;
size = 0;
Arrays.fill(keys, null);
Arrays.fill(values, null);
}
public Set<Long> keySet() {
return new KeySet();
}
public Collection<V> values() {
return new ValueCollection();
}
/**
* Returns a Set of Entry objects for the HashMap. This is not how the internal
* implementation is laid out so this constructs the entire Set when called. For
* this reason it should be avoided if at all possible.
*
* @return Set of Entry objects
* @deprecated
*/
@Deprecated
public Set<Map.Entry<Long, V>> entrySet() {
HashSet<Map.Entry<Long, V>> set = new HashSet<Map.Entry<Long, V>>();
for (long key : keySet()) {
set.add(new Entry(key, get(key)));
}
return set;
}
public Object clone() throws CloneNotSupportedException {
LongObjectHashMap clone = (LongObjectHashMap) super.clone();
// Make sure we clear any existing information from the clone
clone.clear();
// Make sure the clone is properly setup for new entries
clone.initialize();
// Iterate through the data normally to do a safe clone
for (long key : keySet()) {
final V value = get(key);
clone.put(key, value);
}
return clone;
}
private void initialize() {
keys = new long[BUCKET_SIZE][];
values = (V[][]) new Object[BUCKET_SIZE][];
}
private long keyIndex(long key) {
key ^= key >>> 33;
key *= 0xff51afd7ed558ccdL;
key ^= key >>> 33;
key *= 0xc4ceb9fe1a85ec53L;
key ^= key >>> 33;
return key;
}
private void writeObject(ObjectOutputStream outputStream) throws IOException {
outputStream.defaultWriteObject();
for (long key : keySet()) {
V value = get(key);
outputStream.writeLong(key);
outputStream.writeObject(value);
}
outputStream.writeLong(EMPTY_KEY);
outputStream.writeObject(null);
}
private void readObject(ObjectInputStream inputStream) throws ClassNotFoundException, IOException {
inputStream.defaultReadObject();
initialize();
while (true) {
long key = inputStream.readLong();
V value = (V) inputStream.readObject();
if (key == EMPTY_KEY && value == null) {
break;
}
put(key, value);
}
}
private class ValueIterator implements Iterator<V> {
private int count;
private int index;
private int innerIndex;
private int expectedModCount;
private long lastReturned = EMPTY_KEY;
long prevKey = EMPTY_KEY;
V prevValue;
ValueIterator() {
expectedModCount = LongObjectHashMap.this.modCount;
}
public boolean hasNext() {
return count < LongObjectHashMap.this.size;
}
public void remove() {
if (LongObjectHashMap.this.modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (lastReturned == EMPTY_KEY) {
throw new IllegalStateException();
}
count--;
LongObjectHashMap.this.remove(lastReturned);
lastReturned = EMPTY_KEY;
expectedModCount = LongObjectHashMap.this.modCount;
}
public V next() {
if (LongObjectHashMap.this.modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (!hasNext()) {
throw new NoSuchElementException();
}
long[][] keys = LongObjectHashMap.this.keys;
count++;
if (prevKey != EMPTY_KEY) {
innerIndex++;
}
for (; index < keys.length; index++) {
if (keys[index] != null) {
for (; innerIndex < keys[index].length; innerIndex++) {
long key = keys[index][innerIndex];
V value = values[index][innerIndex];
if (key == EMPTY_KEY) {
break;
}
lastReturned = key;
prevKey = key;
prevValue = value;
return prevValue;
}
innerIndex = 0;
}
}
throw new NoSuchElementException();
}
}
private class KeyIterator implements Iterator<Long> {
final ValueIterator iterator;
public KeyIterator() {
iterator = new ValueIterator();
}
public void remove() {
iterator.remove();
}
public boolean hasNext() {
return iterator.hasNext();
}
public Long next() {
iterator.next();
return iterator.prevKey;
}
}
private class KeySet extends AbstractSet<Long> {
public void clear() {
LongObjectHashMap.this.clear();
}
public int size() {
return LongObjectHashMap.this.size();
}
public boolean contains(Object key) {
return key instanceof Long && LongObjectHashMap.this.containsKey((Long) key);
}
public boolean remove(Object key) {
return LongObjectHashMap.this.remove((Long) key) != null;
}
public Iterator<Long> iterator() {
return new KeyIterator();
}
}
private class ValueCollection extends AbstractCollection<V> {
public void clear() {
LongObjectHashMap.this.clear();
}
public int size() {
return LongObjectHashMap.this.size();
}
public boolean contains(Object value) {
return LongObjectHashMap.this.containsValue((V) value);
}
public Iterator<V> iterator() {
return new ValueIterator();
}
}
private class Entry implements Map.Entry<Long, V> {
private final Long key;
private V value;
Entry(long k, V v) {
key = k;
value = v;
}
public Long getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V v) {
V old = value;
value = v;
put(key, v);
return old;
}
}
}