Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ public long hashOf(CIEntry entry) {
static CIEntry[] _create_flat(float loadFactor) {
// 16 distinct case-insensitive keys (foo-0..quux-3).
CIEntry[] table =
FlatHashtable.create(CIEntry.class, PREFIXES.length * NUM_SUFFIXES, loadFactor);
FlatHashtable.create(PREFIXES.length * NUM_SUFFIXES, loadFactor, CIEntry[]::new);
for (int suffix = 0; suffix < NUM_SUFFIXES; ++suffix) {
for (String prefix : PREFIXES) {
String key = prefix + "-" + suffix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public long hashOf(ItEntry entry) {
@Setup(Level.Trial)
public void setUp() {
// Sparse so the bucket is one contiguous run with a clean terminating empty slot.
table = FlatHashtable.create(ItEntry.class, BUCKET_SIZE, FlatHashtable.LOW_LOAD_FACTOR);
table = FlatHashtable.create(BUCKET_SIZE, FlatHashtable.LOW_LOAD_FACTOR, ItEntry[]::new);
for (int i = 0; i < BUCKET_SIZE; ++i) {
FlatHashtable.insert(table, new ItEntry(BUCKET_HASH, i)); // all share the hash => one bucket
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public long hashOf(IntEntry entry) {

static IntEntry[] newFilledFlat() {
// Sized to the key count (FlatHashtable is fixed-capacity, no resize): load factor <= 0.5.
IntEntry[] table = FlatHashtable.create(IntEntry.class, INSERTION_KEYS.length);
IntEntry[] table = FlatHashtable.create(INSERTION_KEYS.length, IntEntry[]::new);
for (int i = 0; i < INSERTION_KEYS.length; ++i) {
FlatHashtable.insert(table, new IntEntry(INSERTION_KEYS[i], i), IntEntryKeyStrategy.INSTANCE);
}
Expand Down
141 changes: 78 additions & 63 deletions internal-api/src/main/java/datadog/trace/util/FlatHashtable.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import datadog.trace.api.function.Strategy;
import datadog.trace.api.function.StrategyConsumer;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

Expand Down Expand Up @@ -166,12 +166,19 @@ public static long hash(@Nullable Object key) {

private final boolean growable;
private final float loadFactor;
private final IntFunction<TEntry[]> newArray; // mints a grown table of the precise type
private TEntry[] table;
private int size;
private int limit; // grow trigger when growable; hard cap when fixed

private D1(TEntry[] table, float loadFactor, boolean growable, int limit) {
private D1(
TEntry[] table,
float loadFactor,
boolean growable,
int limit,
IntFunction<TEntry[]> newArray) {
this.growable = growable;
this.newArray = newArray;
this.loadFactor = loadFactor;
this.table = table;
this.size = 0;
Expand All @@ -184,14 +191,15 @@ private D1(TEntry[] table, float loadFactor, boolean growable, int limit) {
*/
@Nonnull
public static <K, E extends D1.Entry<K>> D1<K, E> createFixed(
@Nonnull Class<E> type, int maxCapacity) {
return createFixed(type, maxCapacity, DEFAULT_LOAD_FACTOR);
int maxCapacity, @Nonnull IntFunction<E[]> newArray) {
return createFixed(maxCapacity, DEFAULT_LOAD_FACTOR, newArray);
}

@Nonnull
public static <K, E extends D1.Entry<K>> D1<K, E> createFixed(
@Nonnull Class<E> type, int maxCapacity, float loadFactor) {
return new D1<>(create(type, maxCapacity, loadFactor), loadFactor, false, maxCapacity);
int maxCapacity, float loadFactor, @Nonnull IntFunction<E[]> newArray) {
return new D1<>(
create(maxCapacity, loadFactor, newArray), loadFactor, false, maxCapacity, newArray);
}

/**
Expand All @@ -200,15 +208,15 @@ public static <K, E extends D1.Entry<K>> D1<K, E> createFixed(
*/
@Nonnull
public static <K, E extends D1.Entry<K>> D1<K, E> createGrowable(
@Nonnull Class<E> type, int initialCapacity) {
return createGrowable(type, initialCapacity, DEFAULT_LOAD_FACTOR);
int initialCapacity, @Nonnull IntFunction<E[]> newArray) {
return createGrowable(initialCapacity, DEFAULT_LOAD_FACTOR, newArray);
}

@Nonnull
public static <K, E extends D1.Entry<K>> D1<K, E> createGrowable(
@Nonnull Class<E> type, int initialCapacity, float loadFactor) {
E[] table = create(type, initialCapacity, loadFactor);
return new D1<>(table, loadFactor, true, (int) (table.length * loadFactor));
int initialCapacity, float loadFactor, @Nonnull IntFunction<E[]> newArray) {
E[] table = create(initialCapacity, loadFactor, newArray);
return new D1<>(table, loadFactor, true, (int) (table.length * loadFactor), newArray);
}

public int size() {
Expand Down Expand Up @@ -302,7 +310,7 @@ public boolean insert(@Nonnull TEntry entry) {
}

private void grow() {
table = resize(table);
table = resize(table, newArray);
limit = (int) (table.length * loadFactor);
}

Expand Down Expand Up @@ -378,12 +386,19 @@ public static long hash(@Nullable Object key1, @Nullable Object key2) {

private final boolean growable;
private final float loadFactor;
private final IntFunction<TEntry[]> newArray; // mints a grown table of the precise type
private TEntry[] table;
private int size;
private int limit; // grow trigger when growable; hard cap when fixed

private D2(TEntry[] table, float loadFactor, boolean growable, int limit) {
private D2(
TEntry[] table,
float loadFactor,
boolean growable,
int limit,
IntFunction<TEntry[]> newArray) {
this.growable = growable;
this.newArray = newArray;
this.loadFactor = loadFactor;
this.table = table;
this.size = 0;
Expand All @@ -396,14 +411,15 @@ private D2(TEntry[] table, float loadFactor, boolean growable, int limit) {
*/
@Nonnull
public static <K1, K2, E extends D2.Entry<K1, K2>> D2<K1, K2, E> createFixed(
@Nonnull Class<E> type, int maxCapacity) {
return createFixed(type, maxCapacity, DEFAULT_LOAD_FACTOR);
int maxCapacity, @Nonnull IntFunction<E[]> newArray) {
return createFixed(maxCapacity, DEFAULT_LOAD_FACTOR, newArray);
}

@Nonnull
public static <K1, K2, E extends D2.Entry<K1, K2>> D2<K1, K2, E> createFixed(
@Nonnull Class<E> type, int maxCapacity, float loadFactor) {
return new D2<>(create(type, maxCapacity, loadFactor), loadFactor, false, maxCapacity);
int maxCapacity, float loadFactor, @Nonnull IntFunction<E[]> newArray) {
return new D2<>(
create(maxCapacity, loadFactor, newArray), loadFactor, false, maxCapacity, newArray);
}

/**
Expand All @@ -412,15 +428,15 @@ public static <K1, K2, E extends D2.Entry<K1, K2>> D2<K1, K2, E> createFixed(
*/
@Nonnull
public static <K1, K2, E extends D2.Entry<K1, K2>> D2<K1, K2, E> createGrowable(
@Nonnull Class<E> type, int initialCapacity) {
return createGrowable(type, initialCapacity, DEFAULT_LOAD_FACTOR);
int initialCapacity, @Nonnull IntFunction<E[]> newArray) {
return createGrowable(initialCapacity, DEFAULT_LOAD_FACTOR, newArray);
}

@Nonnull
public static <K1, K2, E extends D2.Entry<K1, K2>> D2<K1, K2, E> createGrowable(
@Nonnull Class<E> type, int initialCapacity, float loadFactor) {
E[] table = create(type, initialCapacity, loadFactor);
return new D2<>(table, loadFactor, true, (int) (table.length * loadFactor));
int initialCapacity, float loadFactor, @Nonnull IntFunction<E[]> newArray) {
E[] table = create(initialCapacity, loadFactor, newArray);
return new D2<>(table, loadFactor, true, (int) (table.length * loadFactor), newArray);
}

public int size() {
Expand Down Expand Up @@ -520,7 +536,7 @@ public boolean insert(@Nonnull TEntry entry) {
}

private void grow() {
table = resize(table);
table = resize(table, newArray);
limit = (int) (table.length * loadFactor);
}

Expand Down Expand Up @@ -700,27 +716,28 @@ public static int capacityFor(int cardinalityLimit, float loadFactor) {
}

/**
* Allocates a correctly-typed table for a cardinality budget ({@link #capacityFor} slots).
* Passing {@code type} makes the array's runtime component type {@code E} rather than {@code
* Object[]} — typed reads, real array-store checks, and a monomorphic element type for the JIT.
* Callers can't {@code new E[]} themselves under erasure; this does the one reflective allocation
* at construction (off any hot path). Note: this {@code create} mints the backing array; {@link
* CreateStrategy#create} mints an entry — different types, no ambiguity at the call site.
* Allocates a table for a cardinality budget ({@link #capacityFor} slots). {@code newArray} is an
* array constructor of the entry type, e.g. {@code MyEntry[]::new}, so the table's runtime
* component type is {@code E} rather than {@code Object[]}: typed reads, real array-store checks,
* and a precise element type for the JIT. Unlike {@code Array.newInstance}, an array constructor
* isn't reflective, so the table works unchanged under GraalVM native-image with no reflection
* registration. Note: this {@code create} mints the backing array; {@link CreateStrategy#create}
* mints an entry -- different types, no ambiguity at the call site.
*/
@SuppressWarnings("unchecked")
@Nonnull
public static <E> E[] create(@Nonnull Class<E> type, int cardinalityLimit) {
return (E[]) Array.newInstance(type, capacityFor(cardinalityLimit));
public static <E> E[] create(int cardinalityLimit, @Nonnull IntFunction<E[]> newArray) {
return newArray.apply(capacityFor(cardinalityLimit));
}

/**
* {@link #create(Class, int)} at an explicit {@code loadFactor} (see {@link #capacityFor(int,
* float)} and the {@link #DEFAULT_LOAD_FACTOR} / {@link #LOW_LOAD_FACTOR} constants).
* {@link #create(int, IntFunction)} at an explicit {@code loadFactor} (see {@link
* #capacityFor(int, float)} and the {@link #DEFAULT_LOAD_FACTOR} / {@link #LOW_LOAD_FACTOR}
* constants).
*/
@SuppressWarnings("unchecked")
@Nonnull
public static <E> E[] create(@Nonnull Class<E> type, int cardinalityLimit, float loadFactor) {
return (E[]) Array.newInstance(type, capacityFor(cardinalityLimit, loadFactor));
public static <E> E[] create(
int cardinalityLimit, float loadFactor, @Nonnull IntFunction<E[]> newArray) {
return newArray.apply(capacityFor(cardinalityLimit, loadFactor));
}

/**
Expand Down Expand Up @@ -843,12 +860,13 @@ static int home(long hash, int mask) {
* Doubles capacity and rehashes every entry into a new table — call when {@link #insert} returns
* {@code false} and you want to grow rather than reject; the caller stores the returned array
* back. Convenience over the {@link HashStrategy}-taking overload for {@link Entry}-based entries
* (the home comes from {@link Entry#hash}). See {@link #resizingInsert(Entry[], Entry)} to do
* both in one call, and its note on growing over unbounded key domains.
* (the home comes from {@link Entry#hash}). See {@link #resizingInsert(Entry[], Entry,
* IntFunction)} to do both in one call, and its note on growing over unbounded key domains.
*/
@Nonnull
public static <E extends Entry> E[] resize(@Nonnull E[] table) {
E[] grown = allocateGrown(table);
public static <E extends Entry> E[] resize(
@Nonnull E[] table, @Nonnull IntFunction<E[]> newArray) {
E[] grown = newArray.apply(grownLength(table.length));
for (final E e : table) {
if (e != null) {
placeAt(grown, e, e.hash);
Expand All @@ -858,13 +876,14 @@ public static <E extends Entry> E[] resize(@Nonnull E[] table) {
}

/**
* {@link #resize(Entry[])} for any entry type: each entry's home comes from {@link
* {@link #resize(Entry[], IntFunction)} for any entry type: each entry's home comes from {@link
* HashStrategy#hashOf}. Not a {@link StrategyConsumer} — the rehash is a cold, one-off traversal,
* not a hot specialization site.
*/
@Nonnull
public static <E> E[] resize(@Nonnull E[] table, @Nonnull HashStrategy<E> hashStrat) {
E[] grown = allocateGrown(table);
public static <E> E[] resize(
@Nonnull E[] table, @Nonnull HashStrategy<E> hashStrat, @Nonnull IntFunction<E[]> newArray) {
E[] grown = newArray.apply(grownLength(table.length));
for (final E e : table) {
if (e != null) {
placeAt(grown, e, hashStrat.hashOf(e));
Expand All @@ -873,20 +892,12 @@ public static <E> E[] resize(@Nonnull E[] table, @Nonnull HashStrategy<E> hashSt
return grown;
}

/**
* A new, empty table of twice the capacity, of the same runtime component type as {@code table}.
*/
@SuppressWarnings("unchecked")
private static <E> E[] allocateGrown(E[] table) {
return (E[]) Array.newInstance(table.getClass().getComponentType(), grownLength(table.length));
}

/**
* {@code currentLength << 1}, guarded against overflow. {@code currentLength << 1} overflows to a
* negative value at {@code currentLength == 1 << 30}; without this check the failure surfaces as
* an opaque {@code NegativeArraySizeException} from {@code Array.newInstance} in {@link
* #allocateGrown}. Same overflow class as {@link #capacityFor}'s guard -- this is the growth-time
* counterpart.
* an opaque {@code NegativeArraySizeException} from the array constructor in {@link
* #resize(Entry[], IntFunction)}. Same overflow class as {@link #capacityFor}'s guard -- this is
* the growth-time counterpart.
*/
static int grownLength(int currentLength) {
int grownLength = currentLength << 1;
Expand All @@ -899,11 +910,11 @@ static int grownLength(int currentLength) {

/**
* {@link #insert(Entry[], Entry) insert} that grows on demand: adds {@code entry}, {@link
* #resize(Entry[]) resizing} first if the table is full, and returns the table to store back —
* the <b>same</b> array if it fit, a <b>new larger</b> one if it grew:
* #resize(Entry[], IntFunction) resizing} first if the table is full, and returns the table to
* store back — the <b>same</b> array if it fit, a <b>new larger</b> one if it grew:
*
* <pre>{@code
* table = FlatHashtable.resizingInsert(table, entry); // always reassign
* table = FlatHashtable.resizingInsert(table, entry, MyEntry[]::new); // always reassign
* }</pre>
*
* Same comparison-free, caller-ensures-absence contract as {@link #insert}.
Expand All @@ -924,25 +935,29 @@ static int grownLength(int currentLength) {
* size (as {@link D1} / {@link D2} do) can grow at a load factor of its own choosing instead.
*/
@Nonnull
public static <E extends Entry> E[] resizingInsert(@Nonnull E[] table, @Nonnull E entry) {
public static <E extends Entry> E[] resizingInsert(
@Nonnull E[] table, @Nonnull E entry, @Nonnull IntFunction<E[]> newArray) {
E[] t = table;
while (!insert(t, entry)) {
t = resize(t); // one doubling always suffices; the loop is belt-and-braces
t = resize(t, newArray); // one doubling always suffices; the loop is belt-and-braces
}
return t;
}

/**
* {@link #resizingInsert(Entry[], Entry)} for any entry type (home via {@link
* {@link #resizingInsert(Entry[], Entry, IntFunction)} for any entry type (home via {@link
* HashStrategy#hashOf}). Same grows-unboundedly caution and same probe-to-full growth trigger.
*/
@StrategyConsumer
@Nonnull
public static <E> E[] resizingInsert(
@Nonnull E[] table, @Nonnull E entry, @Nonnull HashStrategy<E> hashStrat) {
@Nonnull E[] table,
@Nonnull E entry,
@Nonnull HashStrategy<E> hashStrat,
@Nonnull IntFunction<E[]> newArray) {
E[] t = table;
while (!insert(t, entry, hashStrat)) {
t = resize(t, hashStrat);
t = resize(t, hashStrat, newArray);
}
return t;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ static final class CollidingKeyEntry extends FlatHashtable.D1.Entry<CollidingKey
}

private static FlatHashtable.D1<String, StringIntEntry> growable(int initialCapacity) {
return FlatHashtable.D1.createGrowable(StringIntEntry.class, initialCapacity);
return FlatHashtable.D1.createGrowable(initialCapacity, StringIntEntry[]::new);
}

private static FlatHashtable.D1<String, StringIntEntry> fixed(int maxCapacity) {
return FlatHashtable.D1.createFixed(StringIntEntry.class, maxCapacity);
return FlatHashtable.D1.createFixed(maxCapacity, StringIntEntry[]::new);
}

@Test
Expand Down Expand Up @@ -171,7 +171,7 @@ void nullKeyIsPermittedAndDistinctFromAbsent() {
void hashCollisionsResolveByEquality() {
// Two distinct keys with the same hashCode: the probe must distinguish them via matches().
FlatHashtable.D1<CollidingKey, CollidingKeyEntry> table =
FlatHashtable.D1.createGrowable(CollidingKeyEntry.class, 4);
FlatHashtable.D1.createGrowable(4, CollidingKeyEntry[]::new);
CollidingKey k1 = new CollidingKey("first", 17);
CollidingKey k2 = new CollidingKey("second", 17);
CollidingKeyEntry e1 = new CollidingKeyEntry(k1, 100);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ static final class CollidingPairEntry extends FlatHashtable.D2.Entry<CollidingKe
}

private static FlatHashtable.D2<String, Integer, PairEntry> growable(int initialCapacity) {
return FlatHashtable.D2.createGrowable(PairEntry.class, initialCapacity);
return FlatHashtable.D2.createGrowable(initialCapacity, PairEntry[]::new);
}

private static FlatHashtable.D2<String, Integer, PairEntry> fixed(int maxCapacity) {
return FlatHashtable.D2.createFixed(PairEntry.class, maxCapacity);
return FlatHashtable.D2.createFixed(maxCapacity, PairEntry[]::new);
}

@Test
Expand Down Expand Up @@ -260,7 +260,7 @@ void fixedInsertReturnsFalseWhenFull() {
@Test
void hashCollisionsResolveByKeyEquality() {
FlatHashtable.D2<CollidingKey, CollidingKey, CollidingPairEntry> table =
FlatHashtable.D2.createGrowable(CollidingPairEntry.class, 8);
FlatHashtable.D2.createGrowable(8, CollidingPairEntry[]::new);
CollidingKey key1 = new CollidingKey("a", 7);
CollidingKey key2 = new CollidingKey("x", 3);
CollidingPairEntry e = new CollidingPairEntry(key1, key2, 100);
Expand Down
Loading
Loading