mirror of
https://github.com/OpenHFT/Chronicle-Map.git
synced 2021-09-13 01:26:12 +00:00
323 lines
11 KiB
Java
323 lines
11 KiB
Java
/*
|
|
* Copyright 2014 Higher Frequency Trading http://www.higherfrequencytrading.com
|
|
*
|
|
* Licensed 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 net.openhft.chronicle.map;
|
|
|
|
import net.openhft.lang.Maths;
|
|
import net.openhft.lang.collection.ATSDirectBitSet;
|
|
import net.openhft.lang.collection.DirectBitSet;
|
|
import net.openhft.lang.io.Bytes;
|
|
import net.openhft.lang.io.DirectStore;
|
|
|
|
/**
|
|
* Supports a simple interface for int -> int[] off heap.
|
|
*/
|
|
class VanillaIntIntMultiMap implements IntIntMultiMap {
|
|
|
|
static int multiMapCapacity(int minCapacity) {
|
|
if (minCapacity < 0)
|
|
throw new IllegalArgumentException();
|
|
int capacity = Maths.nextPower2(minCapacity, 16);
|
|
if (((double) minCapacity) / capacity > 2./3.) {
|
|
// multi map shouldn't be too dense
|
|
capacity <<= 1;
|
|
}
|
|
return capacity;
|
|
}
|
|
|
|
private static final int ENTRY_SIZE = 8;
|
|
private static final int ENTRY_SIZE_SHIFT = 3;
|
|
private static final int UNSET_KEY = 0;
|
|
private static final int HASH_INSTEAD_OF_UNSET_KEY = -1;
|
|
private static final int UNSET_VALUE = Integer.MIN_VALUE;
|
|
/**
|
|
* hash is in 32 higher order bits, because in Intel's little-endian they are written first
|
|
* in memory, and in memory we have keys and values in natural order: 4 bytes of k1, 4 bytes
|
|
* of v1, 4 bytes of k2, ... and this is somehow compatible with previous version of this class,
|
|
* where keys were written before values explicitly. However, this layout increases latency
|
|
* of map operations by 1 clock cycle, because we always need to perform shift to obtain the key
|
|
* between memory read and comparison with UNSET_KEY.
|
|
*/
|
|
private static final long UNSET_ENTRY = Integer.MIN_VALUE & 0xFFFFFFFFL;
|
|
final DirectBitSet positions;
|
|
private final int capacity;
|
|
private final int capacityMask;
|
|
private final long capacityMask2;
|
|
private final Bytes bytes;
|
|
private int searchHash = -1;
|
|
private long searchPos = -1L;
|
|
|
|
public VanillaIntIntMultiMap(int minCapacity) {
|
|
capacity = multiMapCapacity(minCapacity);
|
|
capacityMask = capacity - 1;
|
|
capacityMask2 = indexToPos(capacity - 1);
|
|
bytes = DirectStore.allocateLazy(indexToPos(capacity)).bytes();
|
|
positions = newPositions(capacity);
|
|
clear();
|
|
}
|
|
|
|
public VanillaIntIntMultiMap(Bytes multiMapBytes, Bytes multiMapBitSetBytes) {
|
|
capacity = (int) (multiMapBytes.capacity() / ENTRY_SIZE);
|
|
assert capacity == Maths.nextPower2(capacity, 16);
|
|
capacityMask = capacity - 1;
|
|
capacityMask2 = indexToPos(capacity - 1);
|
|
this.bytes = multiMapBytes;
|
|
positions = new ATSDirectBitSet(multiMapBitSetBytes);
|
|
}
|
|
|
|
/**
|
|
* @param minCapacity as in {@link #VanillaIntIntMultiMap(int)} constructor
|
|
* @return size of {@link Bytes} to provide to {@link #VanillaIntIntMultiMap(Bytes, Bytes)}
|
|
* constructor as the first argument
|
|
*/
|
|
public static long sizeInBytes(int minCapacity) {
|
|
return indexToPos(multiMapCapacity(minCapacity));
|
|
}
|
|
|
|
/**
|
|
* @param minCapacity as in {@link #VanillaIntIntMultiMap(int)} constructor
|
|
* @return size of {@link Bytes} to provide to {@link #VanillaIntIntMultiMap(Bytes, Bytes)}
|
|
* constructor as the second argument
|
|
*/
|
|
public static long sizeOfBitSetInBytes(int minCapacity) {
|
|
return Math.max(multiMapCapacity(minCapacity), 64L) / 8L;
|
|
}
|
|
|
|
public static ATSDirectBitSet newPositions(int capacity) {
|
|
// bit set size should be at least 1 native long (in bits)
|
|
capacity = Math.max(capacity, 64);
|
|
// no round-up, because capacity is already a power of 2
|
|
int bitSetSizeInBytes = capacity / 8;
|
|
return new ATSDirectBitSet(DirectStore.allocateLazy(bitSetSizeInBytes).bytes());
|
|
}
|
|
|
|
/**
|
|
* Separate method because it is too easy to forget to cast to long before shifting.
|
|
*/
|
|
private static long indexToPos(int index) {
|
|
return ((long) index) << ENTRY_SIZE_SHIFT;
|
|
}
|
|
|
|
private static int maskUnsetKey(int key) {
|
|
return key != UNSET_KEY ? key : HASH_INSTEAD_OF_UNSET_KEY;
|
|
}
|
|
|
|
@Override
|
|
public void put(int key, int value) {
|
|
key = maskUnsetKey(key);
|
|
long pos = indexToPos(key & capacityMask);
|
|
for (int i = 0; i <= capacityMask; i++) {
|
|
long entry = bytes.readLong(pos);
|
|
int hash2 = (int) (entry >> 32);
|
|
if (hash2 == UNSET_KEY) {
|
|
bytes.writeLong(pos, (((long) key) << 32) | (value & 0xFFFFFFFFL));
|
|
positions.set(value);
|
|
return;
|
|
}
|
|
if (hash2 == key) {
|
|
int value2 = (int) entry;
|
|
if (value2 == value)
|
|
return;
|
|
}
|
|
pos = (pos + ENTRY_SIZE) & capacityMask2;
|
|
}
|
|
throw new IllegalStateException(getClass().getSimpleName() + " is full");
|
|
}
|
|
|
|
@Override
|
|
public boolean remove(int key, int value) {
|
|
key = maskUnsetKey(key);
|
|
long pos = indexToPos(key & capacityMask);
|
|
long posToRemove = -1L;
|
|
for (int i = 0; i <= capacityMask; i++) {
|
|
long entry = bytes.readLong(pos);
|
|
int hash2 = (int) (entry >> 32);
|
|
if (hash2 == key) {
|
|
int value2 = (int) entry;
|
|
if (value2 == value) {
|
|
posToRemove = pos;
|
|
break;
|
|
}
|
|
} else if (hash2 == UNSET_KEY) {
|
|
break;
|
|
}
|
|
pos = (pos + ENTRY_SIZE) & capacityMask2;
|
|
}
|
|
if (posToRemove < 0)
|
|
return false;
|
|
positions.clear(value);
|
|
removePos(posToRemove);
|
|
return true;
|
|
}
|
|
|
|
/////////////////////
|
|
// Stateful methods
|
|
|
|
@Override
|
|
public boolean replace(int key, int oldValue, int newValue) {
|
|
key = maskUnsetKey(key);
|
|
long pos = indexToPos(key & capacityMask);
|
|
for (int i = 0; i <= capacityMask; i++) {
|
|
long entry = bytes.readLong(pos);
|
|
int hash2 = (int) (entry >> 32);
|
|
if (hash2 == key) {
|
|
int value2 = (int) entry;
|
|
if (value2 == oldValue) {
|
|
positions.clear(oldValue);
|
|
positions.set(newValue);
|
|
bytes.writeLong(pos, (((long) key) << 32) | (newValue & 0xFFFFFFFFL));
|
|
return true;
|
|
}
|
|
} else if (hash2 == UNSET_KEY) {
|
|
break;
|
|
}
|
|
pos = (pos + ENTRY_SIZE) & capacityMask2;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void removePos(long posToRemove) {
|
|
long posToShift = posToRemove;
|
|
for (int i = 0; i <= capacityMask; i++) {
|
|
posToShift = (posToShift + ENTRY_SIZE) & capacityMask2;
|
|
long entryToShift = bytes.readLong(posToShift);
|
|
int hash = (int) (entryToShift >> 32);
|
|
if (hash == UNSET_KEY)
|
|
break;
|
|
long insertPos = indexToPos(hash & capacityMask);
|
|
// the following condition essentially means circular permutations
|
|
// of three (r = posToRemove, s = posToShift, i = insertPos)
|
|
// positions are accepted:
|
|
// [...i..r...s.] or
|
|
// [...r..s...i.] or
|
|
// [...s..i...r.]
|
|
boolean cond1 = insertPos <= posToRemove;
|
|
boolean cond2 = posToRemove <= posToShift;
|
|
if ((cond1 && cond2) ||
|
|
// chain wrapped around capacity
|
|
(posToShift < insertPos && (cond1 || cond2))) {
|
|
bytes.writeLong(posToRemove, entryToShift);
|
|
posToRemove = posToShift;
|
|
}
|
|
}
|
|
bytes.writeLong(posToRemove, UNSET_ENTRY);
|
|
}
|
|
|
|
@Override
|
|
public int startSearch(int key) {
|
|
key = maskUnsetKey(key);
|
|
searchPos = indexToPos(key & capacityMask);
|
|
return searchHash = key;
|
|
}
|
|
|
|
@Override
|
|
public int nextPos() {
|
|
long pos = searchPos;
|
|
for (int i = 0; i < capacity; i++) {
|
|
long entry = bytes.readLong(pos);
|
|
int hash2 = (int) (entry >> 32);
|
|
if (hash2 == UNSET_KEY) {
|
|
searchPos = pos;
|
|
return UNSET_VALUE;
|
|
}
|
|
pos = (pos + ENTRY_SIZE) & capacityMask2;
|
|
if (hash2 == searchHash) {
|
|
searchPos = pos;
|
|
return (int) entry;
|
|
}
|
|
}
|
|
// if return UNSET_VALUE, we have 2 cases in putAfterFailedSearch()
|
|
throw new IllegalStateException(getClass().getSimpleName() + " is full");
|
|
}
|
|
|
|
@Override
|
|
public void removePrevPos() {
|
|
long prevPos = (searchPos - ENTRY_SIZE) & capacityMask2;
|
|
long entry = bytes.readLong(prevPos);
|
|
int value = (int) entry;
|
|
positions.clear(value);
|
|
removePos(prevPos);
|
|
}
|
|
|
|
@Override
|
|
public void replacePrevPos(int newValue) {
|
|
long prevPos = ((searchPos - ENTRY_SIZE) & capacityMask2);
|
|
long oldEntry = bytes.readLong(prevPos);
|
|
int oldValue = (int) oldEntry;
|
|
positions.clear(oldValue);
|
|
positions.set(newValue);
|
|
// Don't need to overwrite searchHash, but we don't know our bytes
|
|
// byte order, and can't determine offset of the value within entry.
|
|
long newEntry = (((long) searchHash) << 32) | (newValue & 0xFFFFFFFFL);
|
|
bytes.writeLong(prevPos, newEntry);
|
|
}
|
|
|
|
|
|
@Override
|
|
public void putAfterFailedSearch(int value) {
|
|
positions.set(value);
|
|
long entry = (((long) searchHash) << 32) | (value & 0xFFFFFFFFL);
|
|
bytes.writeLong(searchPos, entry);
|
|
}
|
|
|
|
public int getSearchHash() {
|
|
return searchHash;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("{ ");
|
|
for (int i = 0, pos = 0; i < capacity; i++, pos += ENTRY_SIZE) {
|
|
long entry = bytes.readLong(pos);
|
|
int key = (int) (entry >> 32);
|
|
int value = (int) entry;
|
|
if (key != UNSET_KEY)
|
|
sb.append(key).append('=').append(value).append(", ");
|
|
}
|
|
if (sb.length() > 2) {
|
|
sb.setLength(sb.length() - 2);
|
|
return sb.append(" }").toString();
|
|
}
|
|
return "{ }";
|
|
}
|
|
|
|
@Override
|
|
public void forEach(EntryConsumer action) {
|
|
for (int i = 0, pos = 0; i < capacity; i++, pos += ENTRY_SIZE) {
|
|
long entry = bytes.readLong(pos);
|
|
int key = (int) (entry >> 32);
|
|
int value = (int) entry;
|
|
if (key != UNSET_KEY) {
|
|
action.accept(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public DirectBitSet getPositions() {
|
|
return positions;
|
|
}
|
|
|
|
@Override
|
|
public void clear() {
|
|
positions.clear();
|
|
for (int pos = 0; pos < bytes.capacity(); pos += ENTRY_SIZE) {
|
|
bytes.writeLong(pos, UNSET_ENTRY);
|
|
}
|
|
}
|
|
}
|