Skip to content

Commit 25895fc

Browse files
committed
GH-5779 reduce cleanup cost ConcurrentCache in LMDB
1 parent 4aa8422 commit 25895fc

1 file changed

Lines changed: 122 additions & 41 deletions

File tree

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/ConcurrentCache.java

Lines changed: 122 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,123 @@
88
*
99
* SPDX-License-Identifier: BSD-3-Clause
1010
*******************************************************************************/
11+
// Some portions generated by Codex
1112
package org.eclipse.rdf4j.sail.lmdb;
1213

13-
import java.util.Iterator;
14-
import java.util.concurrent.ConcurrentHashMap;
14+
import java.util.Arrays;
15+
import java.util.Objects;
1516

1617
/**
17-
* Limited-size concurrent cache. The actual cleanup to keep the size limited is done once per
18-
* <code>CLEANUP_INTERVAL</code> invocations of the protected method <code>cleanUp</code>. <code>cleanUp</code> method
19-
* is called every time by <code>put</code> The maximum size is maintained approximately. Cleanup is not done if size is
20-
* less than <code>capacity + CLEANUP_INTERVAL / 2</code>.
18+
* Fixed-size concurrent cache with approximate FIFO eviction per hash set. The cache never grows beyond its slot
19+
* budget; capacity is rounded up to a power-of-two number of slots and entries are evicted in O(1).
2120
*/
2221
public class ConcurrentCache<K, V> {
2322

24-
private static final int CLEANUP_INTERVAL = 1024;
23+
private static final int WAYS = 4;
2524

26-
private static final float LOAD_FACTOR = 0.75f;
25+
private final Entry<K, V>[] entries;
26+
private final int[] nextVictim;
27+
private final int setMask;
28+
private final int setShift;
2729

28-
private final int capacity;
29-
30-
private volatile int cleanupTick = 0;
31-
32-
protected final ConcurrentHashMap<K, V> cache;
30+
private volatile long generation = 1;
3331

32+
@SuppressWarnings("unchecked")
3433
public ConcurrentCache(int capacity) {
35-
this.capacity = capacity;
36-
this.cache = new ConcurrentHashMap<>((int) (capacity / LOAD_FACTOR), LOAD_FACTOR);
34+
if (capacity <= 0) {
35+
entries = (Entry<K, V>[]) new Entry[0];
36+
nextVictim = new int[0];
37+
setMask = 0;
38+
setShift = 0;
39+
return;
40+
}
41+
42+
int slotCount = nextPowerOfTwo(Math.max(WAYS, capacity));
43+
int setCount = slotCount / WAYS;
44+
entries = (Entry<K, V>[]) new Entry[slotCount];
45+
nextVictim = new int[setCount];
46+
setMask = setCount - 1;
47+
setShift = Integer.numberOfTrailingZeros(setCount);
3748
}
3849

3950
public V get(Object key) {
40-
return cache.get(key);
51+
Objects.requireNonNull(key);
52+
if (entries.length == 0) {
53+
return null;
54+
}
55+
56+
int hash = spread(key.hashCode());
57+
int base = setBase(hash);
58+
int preferredOffset = preferredOffset(hash);
59+
long currentGeneration = generation;
60+
61+
for (int i = 0; i < WAYS; i++) {
62+
Entry<K, V> entry = entries[base + ((preferredOffset + i) & (WAYS - 1))];
63+
if (entry != null && entry.hash == hash && entry.generation == currentGeneration
64+
&& sameKey(key, entry.key)) {
65+
return entry.value;
66+
}
67+
}
68+
return null;
4169
}
4270

4371
public V put(K key, V value) {
44-
cleanUp();
45-
return cache.put(key, value);
72+
Objects.requireNonNull(key);
73+
Objects.requireNonNull(value);
74+
if (entries.length == 0) {
75+
return null;
76+
}
77+
78+
int hash = spread(key.hashCode());
79+
int setIndex = hash & setMask;
80+
int base = setIndex * WAYS;
81+
int preferredOffset = preferredOffset(hash);
82+
long currentGeneration = generation;
83+
int emptySlot = -1;
84+
85+
for (int i = 0; i < WAYS; i++) {
86+
int slot = base + ((preferredOffset + i) & (WAYS - 1));
87+
Entry<K, V> entry = entries[slot];
88+
if (entry == null) {
89+
if (emptySlot < 0) {
90+
emptySlot = slot;
91+
}
92+
continue;
93+
}
94+
95+
if (entry.hash == hash && entry.generation == currentGeneration && sameKey(key, entry.key)) {
96+
if (entry.value != value) {
97+
entries[slot] = new Entry<>(key, value, hash, currentGeneration);
98+
}
99+
return entry.value;
100+
}
101+
}
102+
103+
if (emptySlot >= 0) {
104+
entries[emptySlot] = new Entry<>(key, value, hash, currentGeneration);
105+
return null;
106+
}
107+
108+
int firstVictim = nextVictim[setIndex] & (WAYS - 1);
109+
for (int i = 0; i < WAYS; i++) {
110+
int victimOffset = (firstVictim + i) & (WAYS - 1);
111+
int slot = base + victimOffset;
112+
Entry<K, V> victim = entries[slot];
113+
if (victim == null || victim.generation != currentGeneration || onEntryRemoval(victim.key)) {
114+
entries[slot] = new Entry<>(key, value, hash, currentGeneration);
115+
nextVictim[setIndex] = (victimOffset + 1) & (WAYS - 1);
116+
return null;
117+
}
118+
}
119+
120+
return null;
46121
}
47122

48123
public void clear() {
49-
cache.clear();
124+
long nextGeneration = generation + 1;
125+
generation = nextGeneration == 0 ? 1 : nextGeneration;
126+
Arrays.fill(entries, null);
127+
Arrays.fill(nextVictim, 0);
50128
}
51129

52130
/**
@@ -58,35 +136,38 @@ protected boolean onEntryRemoval(K key) {
58136
return true;
59137
}
60138

61-
protected void cleanUp() {
62-
// This is not thread-safe, but the worst that can happen is that we may (rarely) get slightly longer
63-
// cleanup intervals or run cleanUp twice
64-
cleanupTick++;
65-
if (cleanupTick <= CLEANUP_INTERVAL) {
66-
return;
67-
}
68-
69-
cleanupTick %= CLEANUP_INTERVAL;
139+
private int setBase(int hash) {
140+
return (hash & setMask) * WAYS;
141+
}
70142

71-
synchronized (cache) {
143+
private int preferredOffset(int hash) {
144+
return (hash >>> setShift) & (WAYS - 1);
145+
}
72146

73-
final int size = cache.size();
74-
if (size < capacity + CLEANUP_INTERVAL / 2) {
75-
return;
76-
}
147+
private static int spread(int hash) {
148+
return hash ^ (hash >>> 16);
149+
}
77150

78-
Iterator<K> iter = cache.keySet().iterator();
151+
private static boolean sameKey(Object key, Object entryKey) {
152+
return key == entryKey || key.equals(entryKey);
153+
}
79154

80-
float removeEachTh = (float) size / (size - capacity);
155+
private static int nextPowerOfTwo(int n) {
156+
return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(n - 1));
157+
}
81158

82-
for (int i = 0; iter.hasNext(); i++) {
159+
private static final class Entry<K, V> {
83160

84-
K key = iter.next();
161+
private final K key;
162+
private final V value;
163+
private final int hash;
164+
private final long generation;
85165

86-
if (i % removeEachTh < 1) {
87-
cache.computeIfPresent(key, (k, v) -> onEntryRemoval(k) ? null : v);
88-
}
89-
}
166+
private Entry(K key, V value, int hash, long generation) {
167+
this.key = key;
168+
this.value = value;
169+
this.hash = hash;
170+
this.generation = generation;
90171
}
91172
}
92173
}

0 commit comments

Comments
 (0)