Optimizing KObjectHashMap hashIndex()

Motivation:

The IntObjectHashMap benchmarks show the Agrona collections to be faster on put, lookup, and remove. One major difference is that we're using 2 modulus operations each time we increment the position index while iterating.  Agrona uses a mask instead.

Modifications:

Modified the KObjectHashMap to use masking rather than modulus when wrapping the position index. This requires that the capacity be a power of 2.

Result:

Improved performance of IntObjectHashMap.
This commit is contained in:
nmittler 2015-04-13 15:17:51 -07:00
parent 4e70523edd
commit 7aac50a79a
2 changed files with 76 additions and 48 deletions

View File

@ -0,0 +1,38 @@
/*
* Copyright 2015 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.util.internal;
/**
* Math utility methods.
*/
public final class MathUtil {
private MathUtil() {
}
/**
* Fast method of finding the next power of 2 greater than or equal to the supplied value.
*
* If the value is {@code <= 0} then 1 will be returned.
*
* This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30.*
* @param value from which to search for next power of 2
* @return The next power of 2 or the value itself if it is a power of 2
*/
public static int findNextPositivePowerOfTwo(final int value) {
assert value > Integer.MIN_VALUE && value < 0x40000000;
return 1 << (32 - Integer.numberOfLeadingZeros(value - 1));
}
}

View File

@ -15,6 +15,8 @@
package io.netty.util.collection;
import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo;
import java.lang.reflect.Array;
import java.util.AbstractCollection;
import java.util.Arrays;
@ -33,7 +35,7 @@ import java.util.NoSuchElementException;
public class @K@ObjectHashMap<V> implements @K@ObjectMap<V>, Iterable<@K@ObjectMap.Entry<V>> {
/** Default initial capacity. Used if not specified in the constructor */
public static final int DEFAULT_CAPACITY = 11;
public static final int DEFAULT_CAPACITY = 8;
/** Default load factor. Used if not specified in the constructor */
public static final float DEFAULT_LOAD_FACTOR = 0.5f;
@ -52,8 +54,8 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V>, Iterable<@K@ObjectM
private @k@[] keys;
private V[] values;
private Collection<V> valueCollection;
private int size;
private int mask;
public @K@ObjectHashMap() {
this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
@ -76,7 +78,8 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V>, Iterable<@K@ObjectM
this.loadFactor = loadFactor;
// Adjust the initial capacity if necessary.
int capacity = adjustCapacity(initialCapacity);
int capacity = findNextPositivePowerOfTwo(initialCapacity);
mask = capacity - 1;
// Allocate the arrays.
keys = new @k@[capacity];
@ -238,38 +241,33 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V>, Iterable<@K@ObjectM
@Override
public Collection<V> values() {
Collection<V> valueCollection = this.valueCollection;
if (valueCollection == null) {
this.valueCollection = valueCollection = new AbstractCollection<V>() {
@Override
public Iterator<V> iterator() {
return new Iterator<V>() {
final Iterator<Entry<V>> iter = @K@ObjectHashMap.this.iterator();
@Override
public boolean hasNext() {
return iter.hasNext();
}
return new AbstractCollection<V>() {
@Override
public Iterator<V> iterator() {
return new Iterator<V>() {
final Iterator<Entry<V>> iter = @K@ObjectHashMap.this.iterator();
@Override
public boolean hasNext() {
return iter.hasNext();
}
@Override
public V next() {
return iter.next().value();
}
@Override
public V next() {
return iter.next().value();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public int size() {
return size;
}
};
}
return valueCollection;
@Override
public int size() {
return size;
}
};
}
@Override
@ -351,8 +349,7 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V>, Iterable<@K@ObjectM
* Returns the hashed index for the given key.
*/
private int hashIndex(@k@ key) {
// Allowing for negative keys by adding the length after the first mod operation.
return (hashCode(key) % keys.length + keys.length) % keys.length;
return hashCode(key) & mask;
}
/**
@ -369,21 +366,13 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V>, Iterable<@K@ObjectM
size++;
if (size > maxSize) {
// Need to grow the arrays. We take care to detect integer overflow,
// also limit array size to ArrayList.MAX_ARRAY_SIZE.
rehash(adjustCapacity((int) Math.min(keys.length * 2.0, Integer.MAX_VALUE - 8)));
} else if (size == keys.length) {
// Open addressing requires that we have at least 1 slot available. Need to refresh
// the arrays to clear any removed elements.
rehash(keys.length);
}
}
if(keys.length == Integer.MAX_VALUE) {
throw new IllegalStateException("Max capacity reached at size=" + size);
}
/**
* Adjusts the given capacity value to ensure that it's odd. Even capacities can break probing.
*/
private static int adjustCapacity(int capacity) {
return capacity | 1;
// Double the capacity.
rehash(keys.length << 1);
}
}
/**
@ -444,6 +433,7 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V>, Iterable<@K@ObjectM
values = temp;
maxSize = calcMaxSize(newCapacity);
mask = newCapacity - 1;
// Insert to the new arrays.
for (int i = 0; i < oldVals.length; ++i) {