From f3e07c60aec4d25a73c81719d920727d8df529b3 Mon Sep 17 00:00:00 2001 From: Steven Ewald Date: Mon, 22 Jan 2024 15:33:57 -0800 Subject: [PATCH 1/4] Hollow history table now resizes to next largest doubled prime --- .../history/keyindex/HollowOrdinalMapper.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/hollow/src/main/java/com/netflix/hollow/tools/history/keyindex/HollowOrdinalMapper.java b/hollow/src/main/java/com/netflix/hollow/tools/history/keyindex/HollowOrdinalMapper.java index 9874a1d362..94044a7d1d 100644 --- a/hollow/src/main/java/com/netflix/hollow/tools/history/keyindex/HollowOrdinalMapper.java +++ b/hollow/src/main/java/com/netflix/hollow/tools/history/keyindex/HollowOrdinalMapper.java @@ -201,15 +201,33 @@ private int hashFromIndex(int index) { return hashKeyRecord(fieldObjects); } + // We want to keep hash table sizes prime numbers, but we don't want to have to compute very large primes during + // runtime. These are precomputed primes, each ~2x the size of the previous + private static int nextLargestPrime(int num) { + int[] precomputedPrimes = new int[] + {4139, 8287, 16553, 33107, 66221, 132421, 264839, 529673, 1059343, 2118661, 4237319, 8474633, + 16949269, 33898507, 67796999, 135593987, 271187993, 542375947, 1084751881, Integer.MAX_VALUE}; + + for(int prime : precomputedPrimes) { + if(prime >= num) { + return prime; + } + } + + return Integer.MAX_VALUE; + } + private void expandAndRehashTable() { prepareForRead(); - int[] newTable = new int[hashToAssignedOrdinal.length*2]; + int newTableSize = nextLargestPrime(hashToAssignedOrdinal.length*2); + + int[] newTable = new int[newTableSize]; Arrays.fill(newTable, ORDINAL_NONE); - int[][] newFieldMappings = new int[primaryKey.numFields()][hashToAssignedOrdinal.length*2]; - IntList[][] newFieldHashToOrdinal = new IntList[primaryKey.numFields()][hashToAssignedOrdinal.length*2]; - assignedOrdinalToIndex = Arrays.copyOf(assignedOrdinalToIndex, hashToAssignedOrdinal.length*2); + int[][] newFieldMappings = new int[primaryKey.numFields()][newTableSize]; + IntList[][] newFieldHashToOrdinal = new IntList[primaryKey.numFields()][newTableSize]; + assignedOrdinalToIndex = Arrays.copyOf(assignedOrdinalToIndex, newTableSize); for(int fieldIdx=0;fieldIdx Date: Wed, 24 Jan 2024 13:04:54 -0800 Subject: [PATCH 2/4] Constructing atomic array from plain array to avoid iterative lazy sets --- .../hollow/core/memory/ByteArrayOrdinalMap.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java b/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java index f3ebd123af..de09be1ea8 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java +++ b/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java @@ -578,13 +578,9 @@ private int rehashPreviouslyAddedData(long key) { * Create an AtomicLongArray of the specified size, each value in the array will be EMPTY_BUCKET_VALUE */ private AtomicLongArray emptyKeyArray(int size) { - AtomicLongArray arr = new AtomicLongArray(size); - // Volatile store not required, could use plain store - // See VarHandles for JDK >= 9 - for (int i = 0; i < arr.length(); i++) { - arr.lazySet(i, EMPTY_BUCKET_VALUE); - } - return arr; + long[] mtArray = new long[size]; + Arrays.fill(mtArray, EMPTY_BUCKET_VALUE); + return new AtomicLongArray(mtArray); } public ByteDataArray getByteData() { From 3037b661d13f392f1c5e8d509712c55c7fd6a057 Mon Sep 17 00:00:00 2001 From: Steven Ewald Date: Thu, 25 Jan 2024 14:03:22 -0800 Subject: [PATCH 3/4] Changed ByteArrayOrdinalMap to use prime table sizes --- .../core/memory/ByteArrayOrdinalMap.java | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java b/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java index de09be1ea8..20814ab20f 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java +++ b/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java @@ -59,6 +59,20 @@ public class ByteArrayOrdinalMap { private long[] pointersByOrdinal; + private static int nextLargestPrime(int num) { + int[] precomputedPrimes = new int[] + {257, 521, 1049, 2099, 4139, 8287, 16553, 33107, 66221, 132421, 264839, 529673, 1059343, 2118661, 4237319, 8474633, + 16949269, 33898507, 67796999, 135593987, 271187993, 542375947, 1084751881, Integer.MAX_VALUE}; + + for(int prime : precomputedPrimes) { + if(prime >= num) { + return prime; + } + } + + return Integer.MAX_VALUE; + } + /** * Creates a byte array ordinal map with a an initial capacity of 256 elements, * and a load factor of 70%. @@ -72,7 +86,7 @@ public ByteArrayOrdinalMap() { * rounded up to the nearest power of two, and a load factor of 70%. */ public ByteArrayOrdinalMap(int size) { - size = bucketSize(size); + size = nextLargestPrime(size); this.freeOrdinalTracker = new FreeOrdinalTracker(); this.byteData = new ByteDataArray(WastefulRecycler.DEFAULT_INSTANCE); @@ -132,8 +146,7 @@ private synchronized int assignOrdinal(ByteDataArray serializedRepresentation, i /// Note that this also requires pointersAndOrdinals be volatile so resizes are also visible AtomicLongArray pao = pointersAndOrdinals; - int modBitmask = pao.length() - 1; - int bucket = hash & modBitmask; + int bucket = indexFromHash(hash, pao.length()); long key = pao.get(bucket); while (key != EMPTY_BUCKET_VALUE) { @@ -141,7 +154,7 @@ private synchronized int assignOrdinal(ByteDataArray serializedRepresentation, i return (int) (key >>> BITS_PER_POINTER); } - bucket = (bucket + 1) & modBitmask; + bucket = (bucket + 1) % pao.length(); key = pao.get(bucket); } @@ -192,6 +205,11 @@ private int findFreeOrdinal(int preferredOrdinal) { return freeOrdinalTracker.getFreeOrdinal(); } + private static int indexFromHash(int hashedValue, int length) { + int modulus = hashedValue % length; + return modulus < 0 ? modulus + length : modulus; + } + /** * Assign a predefined ordinal to a serialized representation.

*

@@ -215,12 +233,11 @@ public void put(ByteDataArray serializedRepresentation, int ordinal) { AtomicLongArray pao = pointersAndOrdinals; - int modBitmask = pao.length() - 1; - int bucket = hash & modBitmask; + int bucket = indexFromHash(hash, pao.length()); long key = pao.get(bucket); while (key != EMPTY_BUCKET_VALUE) { - bucket = (bucket + 1) & modBitmask; + bucket = (bucket + 1) % pao.length(); key = pao.get(bucket); } @@ -295,8 +312,7 @@ public int get(ByteDataArray serializedRepresentation) { private int get(ByteDataArray serializedRepresentation, int hash) { AtomicLongArray pao = pointersAndOrdinals; - int modBitmask = pao.length() - 1; - int bucket = hash & modBitmask; + int bucket = indexFromHash(hash, pao.length()); long key = pao.get(bucket); // Linear probing to resolve collisions @@ -310,7 +326,7 @@ private int get(ByteDataArray serializedRepresentation, int hash) { return (int) (key >>> BITS_PER_POINTER); } - bucket = (bucket + 1) & modBitmask; + bucket = (bucket + 1) % pao.length(); key = pao.get(bucket); } @@ -484,7 +500,7 @@ private boolean compare(ByteDataArray serializedRepresentation, long key) { * @param size the size to increase to, rounded up to the nearest power of two. */ public void resize(int size) { - size = bucketSize(size); + size = nextLargestPrime(size); if (pointersAndOrdinals.length() < size) { growKeyArray(size); @@ -507,7 +523,7 @@ private void growKeyArray() { private void growKeyArray(int newSize) { AtomicLongArray pao = pointersAndOrdinals; - assert (newSize & (newSize - 1)) == 0; // power of 2 + //assert (newSize & (newSize - 1)) == 0; // power of 2 assert pao.length() < newSize; AtomicLongArray newKeys = emptyKeyArray(newSize); @@ -545,15 +561,13 @@ private void populateNewHashArray(AtomicLongArray newKeys, long[] valuesToAdd) { private void populateNewHashArray(AtomicLongArray newKeys, long[] valuesToAdd, int length) { assert length <= valuesToAdd.length; - int modBitmask = newKeys.length() - 1; - for (int i = 0; i < length; i++) { long value = valuesToAdd[i]; if (value != EMPTY_BUCKET_VALUE) { int hash = rehashPreviouslyAddedData(value); - int bucket = hash & modBitmask; + int bucket = indexFromHash(hash, newKeys.length()); while (newKeys.get(bucket) != EMPTY_BUCKET_VALUE) { - bucket = (bucket + 1) & modBitmask; + bucket = (bucket + 1) % newKeys.length(); } // Volatile store not required, could use plain store // See VarHandles for JDK >= 9 From f0ce52ff2b10954d95959e3ce61f81a596698de5 Mon Sep 17 00:00:00 2001 From: Steven Ewald Date: Thu, 25 Jan 2024 16:06:10 -0800 Subject: [PATCH 4/4] Remove modulo from most LP loops --- .../core/memory/ByteArrayOrdinalMap.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java b/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java index 20814ab20f..cbee6980e7 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java +++ b/hollow/src/main/java/com/netflix/hollow/core/memory/ByteArrayOrdinalMap.java @@ -154,7 +154,10 @@ private synchronized int assignOrdinal(ByteDataArray serializedRepresentation, i return (int) (key >>> BITS_PER_POINTER); } - bucket = (bucket + 1) % pao.length(); + bucket = (bucket + 1); + if(bucket == pao.length()) { + bucket = 0; + } key = pao.get(bucket); } @@ -237,7 +240,10 @@ public void put(ByteDataArray serializedRepresentation, int ordinal) { long key = pao.get(bucket); while (key != EMPTY_BUCKET_VALUE) { - bucket = (bucket + 1) % pao.length(); + bucket = (bucket + 1); + if(bucket==pao.length()) { + bucket = 0; + } key = pao.get(bucket); } @@ -326,7 +332,10 @@ private int get(ByteDataArray serializedRepresentation, int hash) { return (int) (key >>> BITS_PER_POINTER); } - bucket = (bucket + 1) % pao.length(); + bucket = (bucket + 1); + if(bucket == pao.length()) { + bucket = 0; + } key = pao.get(bucket); } @@ -567,7 +576,9 @@ private void populateNewHashArray(AtomicLongArray newKeys, long[] valuesToAdd, i int hash = rehashPreviouslyAddedData(value); int bucket = indexFromHash(hash, newKeys.length()); while (newKeys.get(bucket) != EMPTY_BUCKET_VALUE) { - bucket = (bucket + 1) % newKeys.length(); + bucket = (bucket + 1); + if(bucket == newKeys.length()) + bucket = 0; } // Volatile store not required, could use plain store // See VarHandles for JDK >= 9