Skip to content

Commit

Permalink
Add TrieMap.computeIfAbsent()
Browse files Browse the repository at this point in the history
This is another logical operation -- combining lookup with insert. This
patch adds explicit support for it, so it happens in a single pass (most
of the time).

Note that concurrent activity may lead to the value being computed
multiple times.

Nevertheless fixes #147.

Signed-off-by: Robert Varga <[email protected]>
  • Loading branch information
rovarga committed Jan 27, 2025
1 parent 0842609 commit 1d9e15f
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 1 deletion.
42 changes: 42 additions & 0 deletions triemap/src/main/java/tech/pantheon/triemap/CNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static tech.pantheon.triemap.Result.RESTART;

import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

Expand Down Expand Up @@ -91,6 +92,47 @@ static <K, V> MainNode<K, V> dual(final SNode<K, V> first, final @NonNull K key,
}
}

@Nullable Object computeIfAbsent(final MutableTrieMap<K, V> ct, final Gen startGen, final int hc,
final @NonNull K key, final @NonNull Function<? super K, ? extends V> fn, final int lev,
final INode<K, V> parent) {
final int idx = hc >>> lev & 0x1f;
final int flag = 1 << idx;
final int mask = flag - 1;
final int pos = Integer.bitCount(bitmap & mask);

if ((bitmap & flag) == 0) {
final var val = fn.apply(key);
return val == null || insert(ct, parent, pos, flag, key, val, hc) ? val : RESTART;
}

// 1a) insert below
final var cnAtPos = array[pos];
if (cnAtPos instanceof INode<K, V> in) {
// try to renew if needed and enter next level
return startGen != in.gen && !renew(ct, parent, startGen)
? RESTART : in.computeIfAbsent(ct, startGen, hc, key, fn, lev + LEVEL_BITS, parent);
} else if (cnAtPos instanceof SNode<K, V> sn) {
return computeIfAbsent(ct, parent, pos, sn, key, fn, hc, lev);
} else {
throw invalidElement(cnAtPos);
}
}

private @Nullable Object computeIfAbsent(final MutableTrieMap<K, V> ct, final INode<K, V> in, final int pos,
final SNode<K, V> sn, final @NonNull K key, final @NonNull Function<? super K, ? extends V> fn,
final int hc, final int lev) {
if (sn.matches(hc, key)) {
return sn.value();
}
final var val = fn.apply(key);
if (val == null) {
return null;
}

final var rn = gen == in.gen ? this : renewed(ct, gen);
return in.gcasWrite(ct, rn.updatedAt(pos, new INode<>(in, sn, key, val, hc, lev), gen)) ? val : RESTART;
}

@Nullable Object lookup(final TrieMap<K, V> ct, final Gen startGen, final int hc, final @NonNull K key,
final int lev, final INode<K, V> parent) {
// 1) a multinode
Expand Down
17 changes: 17 additions & 0 deletions triemap/src/main/java/tech/pantheon/triemap/INode.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand Down Expand Up @@ -254,6 +255,22 @@ public int elementSize(final ImmutableTrieMap<K, V> ct) {
}
}

@Nullable Object computeIfAbsent(final MutableTrieMap<K, V> ct, final Gen startGen, final int hc,
final @NonNull K key, final @NonNull Function<? super K, ? extends V> fn, final int lev,
final INode<K, V> parent) {
final var m = gcasRead(ct);
if (m instanceof CNode<K, V> cn) {
return cn.computeIfAbsent(ct, startGen, hc, key, fn, lev, this);
} else if (m instanceof TNode) {
clean(ct, parent, lev);
return RESTART;
} else if (m instanceof LNode<K, V> ln) {
return ln.entries.computeIfAbsent(ct, this, ln, key, fn);
} else {
throw invalidElement(m);
}
}

/**
* Inserts a key value pair, overwriting the old pair if the keys match.
*
Expand Down
12 changes: 12 additions & 0 deletions triemap/src/main/java/tech/pantheon/triemap/LNodeEntries.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static tech.pantheon.triemap.PresencePredicate.PRESENT;
import static tech.pantheon.triemap.Result.RESTART;

import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

Expand Down Expand Up @@ -86,6 +87,17 @@ static <K,V> LNodeEntries<K, V> of(final @NonNull K k1, final @NonNull V v1,
return entry != null ? entry.value() : null;
}

@Nullable Object computeIfAbsent(final MutableTrieMap<K, V> ct, final INode<K, V> in, final LNode<K, V> ln,
final @NonNull K key, final @NonNull Function<? super K, ? extends V> fn) {
final var entry = findEntry(key);
if (entry != null) {
return entry.value();
}

final var val = fn.apply(key);
return val == null || in.gcasWrite(ct, toInserted(ln, key, val)) ? val : RESTART;
}

final boolean insert(final MutableTrieMap<K, V> ct, final INode<K, V> in, final LNode<K, V> ln,
final @NonNull K key, final @NonNull V val) {
final var entry = findEntry(key);
Expand Down
19 changes: 19 additions & 0 deletions triemap/src/main/java/tech/pantheon/triemap/MutableTrieMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

Expand Down Expand Up @@ -138,6 +139,24 @@ public V replace(final K key, final V value) {
return (V) res;
}

@Override
@SuppressWarnings("unchecked")
public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {
final var k = requireNonNull(key);
final int hc = computeHash(key);
final var fn = requireNonNull(mappingFunction);

// Keep looping as long as RESTART is being returned
Object res;
do {
// Keep looping as long as we do not get a reply
final var r = readRoot();
res = r.computeIfAbsent(this, r.gen, hc, k, fn, 0, null);
} while (res == RESTART);

return (V) res;
}

@Override
public int size() {
return immutableSnapshot().size();
Expand Down
4 changes: 4 additions & 0 deletions triemap/src/main/java/tech/pantheon/triemap/TrieMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.AbstractMap;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;

/**
* This is a port of Scala's TrieMap class from the Scala Collections library. This implementation does not support
Expand Down Expand Up @@ -144,6 +145,9 @@ public final V get(final Object key) {
@Override
public abstract V replace(K key, V value);

@Override
public abstract V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction);

@Override
public abstract int size();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package tech.pantheon.triemap;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.fail;

Expand All @@ -38,7 +39,7 @@ void testConcurrentMapComputeIfAbsentDoesNotComputeIfPresent() {
for (int i = 0; i < COUNT; i++) {
map.put(i, Integer.toString(i));
assertEquals(Integer.toString(i), map.computeIfAbsent(i,
(ignored) -> fail("Should not have called function")));
ignored -> fail("Should not have called function")));
}
}

Expand All @@ -63,4 +64,11 @@ void testConflictingHash() {
// Check with equivalent key
assertSame(v3, map.computeIfAbsent(k3dup, k -> v3));
}

@Test
void testComputeNull() {
final var map = TrieMap.create();
assertNull(map.computeIfAbsent("key", k -> null));
assertEquals("{}", map.toString());
}
}

0 comments on commit 1d9e15f

Please sign in to comment.