cache disk blocks in an LRU cache

Improves read access by factor 4 for small trees.
This commit is contained in:
2018-11-24 15:07:37 +01:00
parent 9889252205
commit d67e452a91
7 changed files with 146 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
package org.lucares.pdb.blockstorage; package org.lucares.pdb.blockstorage;
import java.nio.MappedByteBuffer; import java.nio.ByteBuffer;
import org.lucares.collections.LongList; import org.lucares.collections.LongList;
import org.lucares.pdb.diskstorage.DiskBlock; import org.lucares.pdb.diskstorage.DiskBlock;
@@ -28,7 +28,7 @@ public class BSFileDiskBlock {
public byte[] getBuffer() { public byte[] getBuffer() {
if (buffer == null) { if (buffer == null) {
final MappedByteBuffer byteBuffer = diskBlock.getByteBuffer(); final ByteBuffer byteBuffer = diskBlock.getByteBuffer();
this.buffer = new byte[byteBuffer.capacity() - INT_SEQUENCE_OFFSET]; this.buffer = new byte[byteBuffer.capacity() - INT_SEQUENCE_OFFSET];
byteBuffer.position(INT_SEQUENCE_OFFSET); byteBuffer.position(INT_SEQUENCE_OFFSET);
byteBuffer.get(buffer); byteBuffer.get(buffer);
@@ -86,7 +86,7 @@ public class BSFileDiskBlock {
} }
public void force() { public void force() {
diskBlock.getByteBuffer().force(); diskBlock.force();
} }
@Override @Override

View File

@@ -1,5 +1,6 @@
package org.lucares.pdb.diskstorage; package org.lucares.pdb.diskstorage;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer; import java.nio.MappedByteBuffer;
public class DiskBlock { public class DiskBlock {
@@ -7,9 +8,9 @@ public class DiskBlock {
private byte[] buffer = null; private byte[] buffer = null;
private final long blockOffset; private final long blockOffset;
private final MappedByteBuffer byteBuffer; private final ByteBuffer byteBuffer;
public DiskBlock(final long blockOffset, final MappedByteBuffer byteBuffer) { public DiskBlock(final long blockOffset, final ByteBuffer byteBuffer) {
this.blockOffset = blockOffset; this.blockOffset = blockOffset;
this.byteBuffer = byteBuffer; this.byteBuffer = byteBuffer;
} }
@@ -24,7 +25,7 @@ public class DiskBlock {
return buffer; return buffer;
} }
public MappedByteBuffer getByteBuffer() { public ByteBuffer getByteBuffer() {
return byteBuffer; return byteBuffer;
} }
@@ -42,7 +43,10 @@ public class DiskBlock {
} }
public void force() { public void force() {
byteBuffer.force(); // some tests use HeapByteBuffer and don't support force
if (byteBuffer instanceof MappedByteBuffer) {
((MappedByteBuffer) byteBuffer).force();
}
} }
@Override @Override

View File

@@ -16,6 +16,7 @@ import org.lucares.pdb.diskstorage.DiskBlock;
import org.lucares.pdb.diskstorage.DiskStorage; import org.lucares.pdb.diskstorage.DiskStorage;
import org.lucares.utils.Preconditions; import org.lucares.utils.Preconditions;
import org.lucares.utils.byteencoder.VariableByteEncoder; import org.lucares.utils.byteencoder.VariableByteEncoder;
import org.lucares.utils.cache.LRUCache;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -101,6 +102,8 @@ public class PersistentMap<K, V> implements AutoCloseable {
private final EncoderDecoder<V> valueEncoder; private final EncoderDecoder<V> valueEncoder;
private final LRUCache<Long, PersistentMapDiskNode> nodeCache = new LRUCache<>(10_000);
public PersistentMap(final Path path, final EncoderDecoder<K> keyEncoder, final EncoderDecoder<V> valueEncoder) public PersistentMap(final Path path, final EncoderDecoder<K> keyEncoder, final EncoderDecoder<V> valueEncoder)
throws IOException { throws IOException {
this.diskStore = new DiskStorage(path); this.diskStore = new DiskStorage(path);
@@ -294,16 +297,27 @@ public class PersistentMap<K, V> implements AutoCloseable {
} }
private PersistentMapDiskNode getNode(final long nodeOffset) throws IOException { private PersistentMapDiskNode getNode(final long nodeOffset) throws IOException {
final DiskBlock diskBlock = diskStore.getDiskBlock(nodeOffset, BLOCK_SIZE);
final byte[] buffer = diskBlock.getBuffer(); PersistentMapDiskNode node = nodeCache.get(nodeOffset);
final PersistentMapDiskNode node = PersistentMapDiskNode.parse(nodeOffset, buffer); if (node == null) {
final DiskBlock diskBlock = diskStore.getDiskBlock(nodeOffset, BLOCK_SIZE);
node = PersistentMapDiskNode.parse(nodeOffset, diskBlock);
nodeCache.put(nodeOffset, node);
}
return node; return node;
} }
private void writeNode(final PersistentMapDiskNode node) throws IOException { private void writeNode(final PersistentMapDiskNode node) throws IOException {
LOGGER.trace("writing node {}", node); LOGGER.trace("writing node {}", node);
final long nodeOffest = node.getNodeOffset(); final long nodeOffest = node.getNodeOffset();
final DiskBlock diskBlock = diskStore.getDiskBlock(nodeOffest, BLOCK_SIZE); // final DiskBlock diskBlock = diskStore.getDiskBlock(nodeOffest, BLOCK_SIZE);
DiskBlock diskBlock = node.getDiskBlock();
if (diskBlock == null) {
diskBlock = diskStore.getDiskBlock(nodeOffest, BLOCK_SIZE);
}
final byte[] buffer = diskBlock.getBuffer(); final byte[] buffer = diskBlock.getBuffer();
final byte[] newBuffer = node.serialize(); final byte[] newBuffer = node.serialize();
System.arraycopy(newBuffer, 0, buffer, 0, buffer.length); System.arraycopy(newBuffer, 0, buffer, 0, buffer.length);

View File

@@ -7,6 +7,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.lucares.collections.LongList; import org.lucares.collections.LongList;
import org.lucares.pdb.diskstorage.DiskBlock;
import org.lucares.pdb.map.NodeEntry.ValueType; import org.lucares.pdb.map.NodeEntry.ValueType;
import org.lucares.utils.Preconditions; import org.lucares.utils.Preconditions;
import org.lucares.utils.byteencoder.VariableByteEncoder; import org.lucares.utils.byteencoder.VariableByteEncoder;
@@ -32,19 +33,23 @@ import org.lucares.utils.byteencoder.VariableByteEncoder;
*/ */
public class PersistentMapDiskNode { public class PersistentMapDiskNode {
// TODO use map instead of list
private final List<NodeEntry> entries; private final List<NodeEntry> entries;
private final long nodeOffset; private final long nodeOffset;
private final DiskBlock diskBlock;
public PersistentMapDiskNode(final long nodeOffset, final List<NodeEntry> entries) { public PersistentMapDiskNode(final long nodeOffset, final List<NodeEntry> entries, final DiskBlock diskBlock) {
this.nodeOffset = nodeOffset; this.nodeOffset = nodeOffset;
this.diskBlock = diskBlock;
this.entries = new ArrayList<>(entries); this.entries = new ArrayList<>(entries);
} }
public static PersistentMapDiskNode emptyRootNode(final long nodeOffset) { public static PersistentMapDiskNode emptyRootNode(final long nodeOffset) {
return new PersistentMapDiskNode(nodeOffset, Collections.emptyList()); return new PersistentMapDiskNode(nodeOffset, Collections.emptyList(), null);
} }
public static PersistentMapDiskNode parse(final long nodeOffset, final byte[] data) { public static PersistentMapDiskNode parse(final long nodeOffset, final DiskBlock diskBlock) {
final byte[] data = diskBlock.getBuffer();
if (data.length != PersistentMap.BLOCK_SIZE) { if (data.length != PersistentMap.BLOCK_SIZE) {
throw new IllegalStateException( throw new IllegalStateException(
"block size must be " + PersistentMap.BLOCK_SIZE + " but was " + data.length); "block size must be " + PersistentMap.BLOCK_SIZE + " but was " + data.length);
@@ -52,7 +57,7 @@ public class PersistentMapDiskNode {
final LongList longs = VariableByteEncoder.decode(data); final LongList longs = VariableByteEncoder.decode(data);
final List<NodeEntry> entries = deserialize(longs, data); final List<NodeEntry> entries = deserialize(longs, data);
return new PersistentMapDiskNode(nodeOffset, entries); return new PersistentMapDiskNode(nodeOffset, entries, diskBlock);
} }
public static List<NodeEntry> deserialize(final LongList keyLengths, final byte[] buffer) { public static List<NodeEntry> deserialize(final LongList keyLengths, final byte[] buffer) {
@@ -91,6 +96,10 @@ public class PersistentMapDiskNode {
return serialize(entries); return serialize(entries);
} }
public DiskBlock getDiskBlock() {
return diskBlock;
}
public long getNodeOffset() { public long getNodeOffset() {
return nodeOffset; return nodeOffset;
} }
@@ -195,7 +204,7 @@ public class PersistentMapDiskNode {
entries.clear(); entries.clear();
entries.addAll(rightEntries); entries.addAll(rightEntries);
return new PersistentMapDiskNode(newBlockOffset, leftEntries); return new PersistentMapDiskNode(newBlockOffset, leftEntries, null);
} }
public static int neededBytesTotal(final List<NodeEntry> entries) { public static int neededBytesTotal(final List<NodeEntry> entries) {

View File

@@ -1,10 +1,12 @@
package org.lucares.pdb.map; package org.lucares.pdb.map;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import org.lucares.pdb.diskstorage.DiskBlock;
import org.lucares.pdb.map.NodeEntry.ValueType; import org.lucares.pdb.map.NodeEntry.ValueType;
import org.testng.Assert; import org.testng.Assert;
import org.testng.annotations.Test; import org.testng.annotations.Test;
@@ -21,11 +23,13 @@ public class PersistentMapDiskNodeTest {
entries.add(newNode(ValueType.VALUE_INLINE, "key4___", "value4----")); entries.add(newNode(ValueType.VALUE_INLINE, "key4___", "value4----"));
final long nodeOffset = ThreadLocalRandom.current().nextInt(); final long nodeOffset = ThreadLocalRandom.current().nextInt();
final PersistentMapDiskNode node = new PersistentMapDiskNode(nodeOffset, entries); final PersistentMapDiskNode node = new PersistentMapDiskNode(nodeOffset, entries, null);
final byte[] buffer = node.serialize(); final byte[] buffer = node.serialize();
final PersistentMapDiskNode actualNode = PersistentMapDiskNode.parse(nodeOffset, buffer); final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
final PersistentMapDiskNode actualNode = PersistentMapDiskNode.parse(nodeOffset,
new DiskBlock(nodeOffset, byteBuffer));
Assert.assertEquals(actualNode.getEntries(), entries); Assert.assertEquals(actualNode.getEntries(), entries);
} }

View File

@@ -57,6 +57,7 @@ public class PersistentMapTest {
} }
} }
@Test(invocationCount = 1)
public void testManyValues() throws Exception { public void testManyValues() throws Exception {
final Path file = dataDirectory.resolve("map.db"); final Path file = dataDirectory.resolve("map.db");
final var insertedValues = new HashMap<String, String>(); final var insertedValues = new HashMap<String, String>();
@@ -118,7 +119,7 @@ public class PersistentMapTest {
} }
} }
@Test @Test(invocationCount = 1)
public void testManySmallValues() throws Exception { public void testManySmallValues() throws Exception {
final Path file = dataDirectory.resolve("map.db"); final Path file = dataDirectory.resolve("map.db");
final var insertedValues = new HashMap<Long, Long>(); final var insertedValues = new HashMap<Long, Long>();
@@ -267,4 +268,64 @@ public class PersistentMapTest {
} }
} }
@Test(invocationCount = 1)
public void testLotsOfValues() throws Exception {
final Path file = dataDirectory.resolve("map.db");
final var insertedValues = new HashMap<Long, Long>();
final SecureRandom rnd = new SecureRandom();
rnd.setSeed(1);
try (final PersistentMap<Long, Long> map = new PersistentMap<>(file, PersistentMap.LONG_CODER,
PersistentMap.LONG_CODER)) {
for (int i = 0; i < 1_000; i++) {
final Long key = (long) (rnd.nextGaussian() * Integer.MAX_VALUE);
final Long value = (long) (rnd.nextGaussian() * Integer.MAX_VALUE);
if (insertedValues.containsKey(key)) {
continue;
}
Assert.assertNull(map.putValue(key, value));
insertedValues.put(key, value);
final boolean failEarly = false;
if (failEarly) {
for (final var entry : insertedValues.entrySet()) {
final Long actualValue = map.getValue(entry.getKey());
if (!Objects.equals(actualValue, entry.getValue())) {
map.print();
}
Assert.assertEquals(actualValue, entry.getValue(),
"value for key " + entry.getKey() + " in the " + i + "th iteration");
}
}
}
}
try (final PersistentMap<Long, Long> map = new PersistentMap<>(file, PersistentMap.LONG_CODER,
PersistentMap.LONG_CODER)) {
final AtomicInteger counter = new AtomicInteger();
final AtomicInteger maxDepth = new AtomicInteger();
map.visitNodeEntriesPreOrder((node, parentNode, nodeEntry, depth) -> {
counter.addAndGet(nodeEntry.isInnerNode() ? 1 : 0);
maxDepth.set(Math.max(maxDepth.get(), depth));
});
final long start = System.nanoTime();
for (final var entry : insertedValues.entrySet()) {
final Long actualValue = map.getValue(entry.getKey());
Assert.assertEquals(actualValue, entry.getValue(),
"value for key " + entry.getKey() + " after all iterations");
}
System.out.println("nodes=" + counter.get() + ", depth=" + maxDepth.get() + ": "
+ (System.nanoTime() - start) / 1_000_000.0 + "ms");
}
}
} }

View File

@@ -0,0 +1,35 @@
package org.lucares.utils.cache;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> {
private final LinkedHashMap<K, V> cache;
public LRUCache(final int maxEntries) {
this.cache = new LinkedHashMap<>(16, 0.75f, true) {
private static final long serialVersionUID = 1L;
protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
return size() > maxEntries;
}
};
}
public V put(final K key, final V value) {
return cache.put(key, value);
}
public V get(final K key) {
return cache.get(key);
}
public V remove(final K key) {
return cache.remove(key);
}
public int size() {
return cache.size();
}
}