
io.questdb.cairo.map.FastMap Maven / Gradle / Ivy
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2023 QuestDB
*
* 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 io.questdb.cairo.map;
import io.questdb.cairo.*;
import io.questdb.cairo.sql.Record;
import io.questdb.cairo.sql.RecordCursor;
import io.questdb.griffin.engine.LimitOverflowException;
import io.questdb.std.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* FastMap is a general purpose off-heap hash table used to store intermediate data of join,
* group by, sample by queries, but not only. It provides {@link MapKey} and {@link MapValue},
* as well as {@link RecordCursor} interfaces for data access and modification.
* The preferred way to create a FastMap is {@link MapFactory}.
*
* Important!
* Key and value structures must match the ones provided via lists of columns ({@link ColumnTypes})
* to the map constructor. Later put* calls made on {@link MapKey} and {@link MapValue} must match
* the declared column types to guarantee memory access safety.
*
* Keys may be var-size, i.e. a key may contain string or binary columns, while values are expected
* to be fixed-size. Only insertions and updates operations are supported meaning that a key can't
* be removed from the map once it was inserted.
*
* Map iteration provided by {@link RecordCursor} preserves the key insertion order.
*
* The hash table is organized into two main parts:
*
* - 1. Off-heap list for heap offsets
* - 2. Off-heap memory for key-value pairs a.k.a. "key memory"
*
* The offset list contains [compressed_offset, hash_code] pairs. An offset value contains an offset to
* the address of a key-value pair in the key memory compressed to an int. Key-value pair addresses are
* 8 byte aligned, so a FastMap is capable of holding up to 32GB of data.
*
* The offset list is used as a hash table with linear probing. So, a table resize allocates a new
* offset list and copies offsets there while the key memory stays as is.
*
* Key-value pairs stored in the key memory may have the following layout:
*
* | length | Value columns 0..V | Key columns 0..K |
* +----------------------+--------------------+------------------+
* | 4 bytes | - | - |
* +----------------------+--------------------+------------------+
*
* Length field is present for var-size keys only. It stores full key-value pair length in bytes.
*/
public class FastMap implements Map, Reopenable {
private static final long MAX_HEAP_SIZE = (Integer.toUnsignedLong(-1) - 1) << 3;
private static final int MIN_INITIAL_CAPACITY = 128;
private final FastMapCursor cursor;
private final int initialKeyCapacity;
private final int initialPageSize;
private final BaseKey key;
private final int keyOffset;
// Set to -1 when key is var-size.
private final int keySize;
private final int listMemoryTag;
private final double loadFactor;
private final int mapMemoryTag;
private final int maxResizes;
private final FastMapRecord record;
private final FastMapValue value;
private final FastMapValue value2;
private final FastMapValue value3;
private final int valueColumnCount;
private final int valueSize;
private long capacity;
private int free;
private long kLimit; // Key memory limit pointer.
private long kPos; // Current key memory pointer.
private long kStart; // Key memory start pointer.
private int keyCapacity;
private int mask;
private int nResizes;
// Offsets are shifted by +1 (0 -> 1, 1 -> 2, etc.), so that we fill the memory with 0.
private DirectLongList offsets;
private int size = 0;
public FastMap(
int pageSize,
@Transient @NotNull ColumnTypes keyTypes,
int keyCapacity,
double loadFactor,
int maxResizes
) {
this(pageSize, keyTypes, null, keyCapacity, loadFactor, maxResizes);
}
public FastMap(
int pageSize,
@Transient @NotNull ColumnTypes keyTypes,
@Transient @Nullable ColumnTypes valueTypes,
int keyCapacity,
double loadFactor,
int maxResizes,
int memoryTag
) {
this(pageSize, keyTypes, valueTypes, keyCapacity, loadFactor, maxResizes, memoryTag, memoryTag);
}
public FastMap(
int pageSize,
@Transient @NotNull ColumnTypes keyTypes,
@Transient @Nullable ColumnTypes valueTypes,
int keyCapacity,
double loadFactor,
int maxResizes
) {
this(pageSize, keyTypes, valueTypes, keyCapacity, loadFactor, maxResizes, MemoryTag.NATIVE_FAST_MAP, MemoryTag.NATIVE_FAST_MAP_LONG_LIST);
}
FastMap(
int pageSize,
@NotNull @Transient ColumnTypes keyTypes,
@Nullable @Transient ColumnTypes valueTypes,
int keyCapacity,
double loadFactor,
int maxResizes,
int mapMemoryTag,
int listMemoryTag
) {
assert pageSize > 3;
assert loadFactor > 0 && loadFactor < 1d;
this.mapMemoryTag = mapMemoryTag;
this.listMemoryTag = listMemoryTag;
initialKeyCapacity = keyCapacity;
initialPageSize = pageSize;
this.loadFactor = loadFactor;
kStart = kPos = Unsafe.malloc(capacity = pageSize, mapMemoryTag);
kLimit = kStart + pageSize;
this.keyCapacity = (int) (keyCapacity / loadFactor);
this.keyCapacity = this.keyCapacity < MIN_INITIAL_CAPACITY ? MIN_INITIAL_CAPACITY : Numbers.ceilPow2(this.keyCapacity);
mask = this.keyCapacity - 1;
free = (int) (this.keyCapacity * loadFactor);
offsets = new DirectLongList(this.keyCapacity, listMemoryTag);
offsets.setPos(this.keyCapacity);
offsets.zero(0);
nResizes = 0;
this.maxResizes = maxResizes;
final int keyColumnCount = keyTypes.getColumnCount();
int keySize = 0;
for (int i = 0; i < keyColumnCount; i++) {
final int columnType = keyTypes.getColumnType(i);
final int size = ColumnType.sizeOf(columnType);
if (size > 0) {
keySize += size;
} else {
keySize = -1;
break;
}
}
this.keySize = keySize;
// Reserve 4 bytes for key length in case of var-size keys.
int offset = keySize != -1 ? 0 : 4;
int[] valueOffsets = null;
int valueSize = 0;
if (valueTypes != null) {
valueColumnCount = valueTypes.getColumnCount();
valueOffsets = new int[valueColumnCount];
for (int i = 0; i < valueColumnCount; i++) {
valueOffsets[i] = offset;
final int columnType = valueTypes.getColumnType(i);
final int size = ColumnType.sizeOf(columnType);
if (size <= 0) {
close();
throw CairoException.nonCritical().put("value type is not supported: ").put(ColumnType.nameOf(columnType));
}
offset += size;
valueSize += size;
}
} else {
valueColumnCount = 0;
}
this.valueSize = valueSize;
keyOffset = offset;
value = new FastMapValue(valueOffsets);
value2 = new FastMapValue(valueOffsets);
value3 = new FastMapValue(valueOffsets);
record = new FastMapRecord(valueOffsets, keyOffset, value, keyTypes, valueTypes);
assert keySize + valueSize < kLimit - kStart : "page size is too small to fit a single key";
cursor = new FastMapCursor(record, this);
key = keySize == -1 ? new VarSizeKey() : new FixedSizeKey();
}
@Override
public void clear() {
kPos = kStart;
free = (int) (keyCapacity * loadFactor);
size = 0;
offsets.zero(0);
}
@Override
public final void close() {
Misc.free(offsets);
if (kStart != 0) {
Unsafe.free(kStart, capacity, mapMemoryTag);
kLimit = kStart = kPos = 0;
free = 0;
size = 0;
capacity = 0;
}
}
public long getAppendOffset() {
return kPos;
}
public long getAreaSize() {
return kLimit - kStart;
}
@Override
public RecordCursor getCursor() {
return cursor.init(kStart, kLimit, size);
}
public int getKeyCapacity() {
return keyCapacity;
}
@Override
public MapRecord getRecord() {
return record;
}
public int getValueColumnCount() {
return valueColumnCount;
}
public void reopen() {
if (kStart == 0) {
// handles both mem and offsets
restoreInitialCapacity();
}
}
@Override
public void restoreInitialCapacity() {
kStart = kPos = Unsafe.realloc(kStart, kLimit - kStart, capacity = initialPageSize, mapMemoryTag);
kLimit = kStart + initialPageSize;
keyCapacity = (int) (initialKeyCapacity / loadFactor);
keyCapacity = keyCapacity < MIN_INITIAL_CAPACITY ? MIN_INITIAL_CAPACITY : Numbers.ceilPow2(keyCapacity);
mask = keyCapacity - 1;
free = (int) (keyCapacity * loadFactor);
offsets.resetCapacity();
offsets.setCapacity(keyCapacity);
offsets.setPos(keyCapacity);
offsets.zero(0);
nResizes = 0;
}
@Override
public long size() {
return size;
}
@Override
public MapValue valueAt(long address) {
return valueOf(address, false, value);
}
@Override
public MapKey withKey() {
return key.init();
}
private static long getPackedOffset(DirectLongList offsets, int index) {
return offsets.get(index);
}
private static void setPackedOffset(DirectLongList offsets, int index, long offset, int hashCode) {
offsets.set(index, Numbers.encodeLowHighInts((int) ((offset >> 3) + 1), hashCode));
}
private static void setPackedOffset(DirectLongList offsets, int index, long packedOffset) {
offsets.set(index, packedOffset);
}
private static int unpackHashCode(long packedOffset) {
return Numbers.decodeHighInt(packedOffset);
}
private static long unpackOffset(long packedOffset) {
return (Integer.toUnsignedLong(Numbers.decodeLowInt(packedOffset)) - 1) << 3;
}
private FastMapValue asNew(BaseKey keyWriter, int index, int hashCode, FastMapValue value) {
kPos = keyWriter.appendAddress;
// Align current pointer to 8 bytes, so that we can store compressed offsets.
if ((kPos & 0x7) != 0) {
kPos |= 0x7;
kPos++;
}
setPackedOffset(offsets, index, keyWriter.startAddress - kStart, hashCode);
if (--free == 0) {
rehash();
}
size++;
return valueOf(keyWriter.startAddress, true, value);
}
private FastMapValue probe0(BaseKey keyWriter, int index, int hashCode, FastMapValue value) {
long packedOffset;
long offset;
while ((offset = unpackOffset(packedOffset = getPackedOffset(offsets, index = (++index & mask)))) > -1) {
if (hashCode == unpackHashCode(packedOffset) && keyWriter.eq(offset)) {
return valueOf(kStart + offset, false, value);
}
}
return asNew(keyWriter, index, hashCode, value);
}
private FastMapValue probeReadOnly(BaseKey keyWriter, int index, long hashCode, FastMapValue value) {
long packedOffset;
long offset;
while ((offset = unpackOffset(packedOffset = getPackedOffset(offsets, index = (++index & mask)))) > -1) {
if (hashCode == unpackHashCode(packedOffset) && keyWriter.eq(offset)) {
return valueOf(kStart + offset, false, value);
}
}
return null;
}
private void rehash() {
int capacity = keyCapacity << 1;
mask = capacity - 1;
DirectLongList newOffsets = new DirectLongList(capacity, listMemoryTag);
newOffsets.setPos(capacity);
newOffsets.zero(0);
for (int i = 0, k = (int) offsets.size(); i < k; i++) {
long packedOffset = getPackedOffset(offsets, i);
long offset = unpackOffset(packedOffset);
if (offset < 0) {
continue;
}
int hashCode = unpackHashCode(packedOffset);
int index = hashCode & mask;
while (unpackOffset(getPackedOffset(newOffsets, index)) > -1) {
index = (index + 1) & mask;
}
setPackedOffset(newOffsets, index, packedOffset);
}
offsets.close();
offsets = newOffsets;
free += (int) ((capacity - keyCapacity) * loadFactor);
keyCapacity = capacity;
}
private void resize(int size) {
if (nResizes < maxResizes) {
nResizes++;
long kCapacity = (kLimit - kStart) << 1;
long target = key.appendAddress + size - kStart;
if (kCapacity < target) {
kCapacity = Numbers.ceilPow2(target);
}
if (kCapacity > MAX_HEAP_SIZE) {
throw LimitOverflowException.instance().put("limit of ").put(MAX_HEAP_SIZE).put(" memory exceeded in FastMap");
}
long kAddress = Unsafe.realloc(this.kStart, this.capacity, kCapacity, mapMemoryTag);
this.capacity = kCapacity;
long d = kAddress - this.kStart;
kPos += d;
key.startAddress += d;
key.appendAddress += d;
assert kPos > 0;
assert key.startAddress > 0;
assert key.appendAddress > 0;
this.kStart = kAddress;
this.kLimit = kAddress + kCapacity;
} else {
throw LimitOverflowException.instance().put("limit of ").put(maxResizes).put(" resizes exceeded in FastMap");
}
}
private FastMapValue valueOf(long address, boolean newValue, FastMapValue value) {
return value.of(address, kLimit, newValue);
}
int keySize() {
return keySize;
}
int valueSize() {
return valueSize;
}
private abstract class BaseKey implements MapKey {
protected long appendAddress;
protected long startAddress;
@Override
public MapValue createValue() {
return createValue(value);
}
@Override
public MapValue createValue2() {
return createValue(value2);
}
@Override
public MapValue createValue3() {
return createValue(value3);
}
@Override
public MapValue findValue() {
return findValue(value);
}
@Override
public MapValue findValue2() {
return findValue(value2);
}
@Override
public MapValue findValue3() {
return findValue(value3);
}
public BaseKey init() {
startAddress = kPos;
appendAddress = kPos + keyOffset;
return this;
}
@Override
public void put(Record record, RecordSink sink) {
sink.copy(record, this);
}
@Override
public void putRecord(Record value) {
// no-op
}
private MapValue createValue(FastMapValue value) {
commit();
// calculate hash remembering "key" structure
// [ len | value block | key offset block | key data block ]
int hashCode = hash();
int index = hashCode & mask;
long packedOffset = getPackedOffset(offsets, index);
long offset = unpackOffset(packedOffset);
if (offset < 0) {
return asNew(this, index, hashCode, value);
} else if (hashCode == unpackHashCode(packedOffset) && eq(offset)) {
return valueOf(kStart + offset, false, value);
} else {
return probe0(this, index, hashCode, value);
}
}
private MapValue findValue(FastMapValue value) {
commit();
int hashCode = hash();
int index = hashCode & mask;
long packedOffset = getPackedOffset(offsets, index);
long offset = unpackOffset(packedOffset);
if (offset < 0) {
return null;
} else if (hashCode == unpackHashCode(packedOffset) && eq(offset)) {
return valueOf(kStart + offset, false, value);
} else {
return probeReadOnly(this, index, hashCode, value);
}
}
protected void checkSize(int size) {
if (appendAddress + size > kLimit) {
resize(size);
}
}
protected void commit() {
// no-op
}
protected abstract boolean eq(long offset);
protected abstract int hash();
}
private class FixedSizeKey extends BaseKey {
public FixedSizeKey init() {
super.init();
checkSize(keySize + valueSize);
return this;
}
@Override
public void putBin(BinarySequence value) {
throw new UnsupportedOperationException();
}
@Override
public void putBool(boolean value) {
assert appendAddress + Byte.BYTES <= kLimit;
Unsafe.getUnsafe().putByte(appendAddress, (byte) (value ? 1 : 0));
appendAddress += Byte.BYTES;
}
@Override
public void putByte(byte value) {
assert appendAddress + Byte.BYTES <= kLimit;
Unsafe.getUnsafe().putByte(appendAddress, value);
appendAddress += Byte.BYTES;
}
@Override
public void putChar(char value) {
assert appendAddress + Character.BYTES <= kLimit;
Unsafe.getUnsafe().putChar(appendAddress, value);
appendAddress += Character.BYTES;
}
@Override
public void putDate(long value) {
putLong(value);
}
@Override
public void putDouble(double value) {
assert appendAddress + Double.BYTES <= kLimit;
Unsafe.getUnsafe().putDouble(appendAddress, value);
appendAddress += Double.BYTES;
}
@Override
public void putFloat(float value) {
assert appendAddress + Float.BYTES <= kLimit;
Unsafe.getUnsafe().putFloat(appendAddress, value);
appendAddress += Float.BYTES;
}
@Override
public void putInt(int value) {
assert appendAddress + Integer.BYTES <= kLimit;
Unsafe.getUnsafe().putInt(appendAddress, value);
appendAddress += Integer.BYTES;
}
@Override
public void putLong(long value) {
assert appendAddress + Long.BYTES <= kLimit;
Unsafe.getUnsafe().putLong(appendAddress, value);
appendAddress += Long.BYTES;
}
@Override
public void putLong128(long lo, long hi) {
assert appendAddress + 16 <= kLimit;
Unsafe.getUnsafe().putLong(appendAddress, lo);
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES, hi);
appendAddress += 16;
}
@Override
public void putLong256(Long256 value) {
assert appendAddress + Long256.BYTES <= kLimit;
Unsafe.getUnsafe().putLong(appendAddress, value.getLong0());
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES, value.getLong1());
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES * 2, value.getLong2());
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES * 3, value.getLong3());
appendAddress += Long256.BYTES;
}
@Override
public void putShort(short value) {
assert appendAddress + Short.BYTES <= kLimit;
Unsafe.getUnsafe().putShort(appendAddress, value);
appendAddress += Short.BYTES;
}
@Override
public void putStr(CharSequence value) {
throw new UnsupportedOperationException();
}
@Override
public void putStr(CharSequence value, int lo, int hi) {
throw new UnsupportedOperationException();
}
@Override
public void putTimestamp(long value) {
putLong(value);
}
@Override
public void skip(int bytes) {
appendAddress += bytes;
}
@Override
protected boolean eq(long offset) {
return Vect.memeq(kStart + offset + keyOffset, startAddress + keyOffset, keySize);
}
@Override
protected int hash() {
return Hash.hashMem32(startAddress + keyOffset, keySize);
}
}
private class VarSizeKey extends BaseKey {
private int len;
@Override
public void putBin(BinarySequence value) {
if (value == null) {
putNull();
} else {
long len = value.length() + 4;
if (len > Integer.MAX_VALUE) {
throw CairoException.nonCritical().put("binary column is too large");
}
checkSize((int) len);
int l = (int) (len - 4);
Unsafe.getUnsafe().putInt(appendAddress, l);
value.copyTo(appendAddress + 4L, 0L, l);
appendAddress += len;
}
}
@Override
public void putBool(boolean value) {
checkSize(1);
Unsafe.getUnsafe().putByte(appendAddress, (byte) (value ? 1 : 0));
appendAddress += 1;
}
@Override
public void putByte(byte value) {
checkSize(1);
Unsafe.getUnsafe().putByte(appendAddress, value);
appendAddress += 1;
}
@Override
public void putChar(char value) {
checkSize(Character.BYTES);
Unsafe.getUnsafe().putChar(appendAddress, value);
appendAddress += Character.BYTES;
}
@Override
public void putDate(long value) {
putLong(value);
}
@Override
public void putDouble(double value) {
checkSize(Double.BYTES);
Unsafe.getUnsafe().putDouble(appendAddress, value);
appendAddress += Double.BYTES;
}
@Override
public void putFloat(float value) {
checkSize(Float.BYTES);
Unsafe.getUnsafe().putFloat(appendAddress, value);
appendAddress += Float.BYTES;
}
@Override
public void putInt(int value) {
checkSize(Integer.BYTES);
Unsafe.getUnsafe().putInt(appendAddress, value);
appendAddress += Integer.BYTES;
}
@Override
public void putLong(long value) {
checkSize(Long.BYTES);
Unsafe.getUnsafe().putLong(appendAddress, value);
appendAddress += Long.BYTES;
}
@Override
public void putLong128(long lo, long hi) {
checkSize(16);
Unsafe.getUnsafe().putLong(appendAddress, lo);
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES, hi);
appendAddress += 16;
}
@Override
public void putLong256(Long256 value) {
checkSize(Long256.BYTES);
Unsafe.getUnsafe().putLong(appendAddress, value.getLong0());
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES, value.getLong1());
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES * 2, value.getLong2());
Unsafe.getUnsafe().putLong(appendAddress + Long.BYTES * 3, value.getLong3());
appendAddress += Long256.BYTES;
}
@Override
public void putShort(short value) {
checkSize(2);
Unsafe.getUnsafe().putShort(appendAddress, value);
appendAddress += 2;
}
@Override
public void putStr(CharSequence value) {
if (value == null) {
putNull();
return;
}
int len = value.length();
checkSize((len << 1) + 4);
Unsafe.getUnsafe().putInt(appendAddress, len);
appendAddress += 4;
for (int i = 0; i < len; i++) {
Unsafe.getUnsafe().putChar(appendAddress + ((long) i << 1), value.charAt(i));
}
appendAddress += (long) len << 1;
}
@Override
public void putStr(CharSequence value, int lo, int hi) {
int len = hi - lo;
checkSize((len << 1) + 4);
Unsafe.getUnsafe().putInt(appendAddress, len);
appendAddress += 4;
for (int i = lo; i < hi; i++) {
Unsafe.getUnsafe().putChar(appendAddress + ((long) (i - lo) << 1), value.charAt(i));
}
appendAddress += (long) len << 1;
}
@Override
public void putStrLowerCase(CharSequence value) {
if (value == null) {
putNull();
return;
}
int len = value.length();
checkSize((len << 1) + 4);
Unsafe.getUnsafe().putInt(appendAddress, len);
appendAddress += 4;
for (int i = 0; i < len; i++) {
Unsafe.getUnsafe().putChar(appendAddress + ((long) i << 1), Character.toLowerCase(value.charAt(i)));
}
appendAddress += (long) len << 1;
}
@Override
public void putStrLowerCase(CharSequence value, int lo, int hi) {
int len = hi - lo;
checkSize((len << 1) + 4);
Unsafe.getUnsafe().putInt(appendAddress, len);
appendAddress += 4;
for (int i = lo; i < hi; i++) {
Unsafe.getUnsafe().putChar(appendAddress + ((long) (i - lo) << 1), Character.toLowerCase(value.charAt(i)));
}
appendAddress += (long) len << 1;
}
@Override
public void putTimestamp(long value) {
putLong(value);
}
@Override
public void skip(int bytes) {
checkSize(bytes);
appendAddress += bytes;
}
private void putNull() {
checkSize(4);
Unsafe.getUnsafe().putInt(appendAddress, TableUtils.NULL_LEN);
appendAddress += 4;
}
@Override
protected void commit() {
Unsafe.getUnsafe().putInt(startAddress, len = (int) (appendAddress - startAddress));
}
@Override
protected boolean eq(long offset) {
long a = kStart + offset;
long b = startAddress;
// Check the length first.
if (Unsafe.getUnsafe().getInt(a) != Unsafe.getUnsafe().getInt(b)) {
return false;
}
return Vect.memeq(a + keyOffset, b + keyOffset, this.len - keyOffset);
}
@Override
protected int hash() {
return Hash.hashMem32(startAddress + keyOffset, len - keyOffset);
}
}
}