Skip to content

Commit 1b8bb21

Browse files
Add custom hash mix functions for SyncMap (#17)
* improve(syncmap): add the ability to set a custom hash mix function * fix(syncmap): correct constructor default value for load factor
1 parent 315ca2d commit 1b8bb21

File tree

3 files changed

+170
-43
lines changed

3 files changed

+170
-43
lines changed

collections/src/jmh/java/space/vectrix/sync/collections/SyncMapBenchmark.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void setup() {
7272
final boolean prepopulate = "prepopulate".equalsIgnoreCase(this.mode);
7373

7474
switch(this.implementation) {
75-
case "SyncMap" -> this.map = presized ? new SyncMap<>(this.size) : new SyncMap<>();
75+
case "SyncMap" -> this.map = presized ? new SyncMap<>(Hashing.FASTEST_MIX, this.size) : new SyncMap<>(Hashing.FASTEST_MIX);
7676
case "ConcurrentHashMap" -> this.map = presized ? new ConcurrentHashMap<>(this.size) : new ConcurrentHashMap<>();
7777
case "SynchronizedMap" -> this.map = presized
7878
? Collections.synchronizedMap(new HashMap<>(this.size))
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* This file is part of sync, licensed under the MIT License.
3+
*
4+
* Copyright (c) 2025 vectrix.space
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
package space.vectrix.sync.collections;
25+
26+
import java.util.concurrent.ThreadLocalRandom;
27+
import org.jetbrains.annotations.ApiStatus;
28+
29+
/**
30+
* Provides common constants and utilities for hashing operations used across
31+
* the collections framework.
32+
*
33+
* @author Vectrix
34+
* @since 1.0.0
35+
*/
36+
@ApiStatus.Internal
37+
public interface Hashing {
38+
/**
39+
* The bitmask applied to ensure hash values are non-negative and fit within
40+
* the usable range of 32-bit signed integers.
41+
*/
42+
/* package */ int HASH_BITS = 0x7FFFFFFF;
43+
44+
/**
45+
* A random seed used to introduce variability into hash computations,
46+
* reducing predictability and the likelihood of collision attacks.
47+
*/
48+
/* package */ int HASH_SEED = ThreadLocalRandom.current().nextInt();
49+
50+
/**
51+
* A hash mixing function optimized for raw speed with minimal transformation.
52+
* Provides adequate distribution for small or moderate-sized maps but may
53+
* degrade performance with large datasets or many similar keys.
54+
*/
55+
MixFunction FASTEST_MIX = x -> (x ^ (x >>> 16)) & Hashing.HASH_BITS;
56+
57+
/**
58+
* A hash mixing function that balances performance and moderate hash
59+
* distribution. Suitable for medium-sized maps, but may still degrade under
60+
* high key similarity or very large data sets.
61+
*/
62+
MixFunction FAST_MIX = x -> {
63+
x ^= (x >>> 16);
64+
x ^= (x >>> 13);
65+
return x & Hashing.HASH_BITS;
66+
};
67+
68+
/**
69+
* A hash mixing function that emphasizes stronger distribution while
70+
* maintaining good performance. Incorporates {@link #HASH_SEED} to randomize
71+
* results and reduce vulnerability to hash collision attacks.
72+
*/
73+
MixFunction BALANCED_MIX = x -> {
74+
x ^= Hashing.HASH_SEED;
75+
x ^= (x >>> 16);
76+
x ^= (x >>> 13);
77+
return x & Hashing.HASH_BITS;
78+
};
79+
80+
/**
81+
* Represents a hash mixing function to distribute key-value pairs into
82+
* buckets using an {@code int} hash.
83+
*
84+
* @author Vectrix
85+
* @since 1.0.0
86+
*/
87+
@FunctionalInterface
88+
interface MixFunction {
89+
/**
90+
* Mixes the provided hash for distributing key-value pairs into buckets.
91+
*
92+
* @param hash the hash
93+
* @return the mixed hash
94+
* @since 1.0.0
95+
*/
96+
int mix(final int hash);
97+
}
98+
}

collections/src/main/java/space/vectrix/sync/collections/SyncMap.java

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,6 @@ public class SyncMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K,
100100
*/
101101
/* package */ static final int MINIMUM_TRANSFER_STRIDE = 16;
102102

103-
/**
104-
* Represents the number of usable bits for node hash keys.
105-
*/
106-
/* package */ static final int HASH_BITS = 0x7FFFFFFF;
107-
108103
/**
109104
* Represents the hash for a node that has been transferred.
110105
*/
@@ -122,17 +117,6 @@ public class SyncMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K,
122117

123118
/* ------------------------------ < Utilities > ------------------------------ */
124119

125-
/**
126-
* Spreads the given hash value to a positive value and forces the top bit to
127-
* 0.
128-
*
129-
* @param value the value to spread
130-
* @return the spread value
131-
*/
132-
/* package */ static int spread(final int value) {
133-
return (value ^ (value >>> 16)) & SyncMap.HASH_BITS;
134-
}
135-
136120
/**
137121
* Returns the optimal table size depending on the given capacity.
138122
*
@@ -225,6 +209,11 @@ public class SyncMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K,
225209

226210
/* ------------------------------ < Fields > ------------------------------ */
227211

212+
/**
213+
* Represents the mix function for distributing hashes in the map.
214+
*/
215+
/* package */ final transient Hashing.MixFunction mixFunction;
216+
228217
/**
229218
* Represents the load factor for resizing the map.
230219
*/
@@ -297,40 +286,77 @@ public class SyncMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K,
297286
/* ------------------------- < Public Operations > ------------------------- */
298287

299288
/**
300-
* Initializes a new {@link SyncMap} with {@link SyncMap#DEFAULT_CAPACITY} and
301-
* {@link SyncMap#DEFAULT_LOAD_FACTOR}.
289+
* Initializes a new {@link SyncMap} with {@link Hashing#FAST_MIX},
290+
* {@link SyncMap#DEFAULT_CAPACITY} and {@link SyncMap#DEFAULT_LOAD_FACTOR}.
302291
*
303292
* @since 1.0.0
304293
*/
305294
public SyncMap() {
306-
this(SyncMap.DEFAULT_CAPACITY);
295+
this(Hashing.FAST_MIX);
307296
}
308297

309298
/**
310-
* Initializes a new {@link SyncMap} with the given initial capacity and
311-
* {@link SyncMap#DEFAULT_LOAD_FACTOR}.
299+
* Initializes a new {@link SyncMap} with the given mix function,
300+
* {@link SyncMap#DEFAULT_CAPACITY} and {@link SyncMap#DEFAULT_LOAD_FACTOR}.
301+
*
302+
* @param mixFunction the mix function
303+
* @since 1.0.0
304+
*/
305+
public SyncMap(final Hashing.MixFunction mixFunction) {
306+
this(mixFunction, SyncMap.DEFAULT_CAPACITY);
307+
}
308+
309+
/**
310+
* Initializes a new {@link SyncMap} with {@link Hashing#FAST_MIX}, the
311+
* given initial capacity and {@link SyncMap#DEFAULT_LOAD_FACTOR}.
312312
*
313313
* @param initialCapacity the initial capacity
314314
* @since 1.0.0
315315
*/
316316
public SyncMap(final int initialCapacity) {
317-
this(initialCapacity, SyncMap.DEFAULT_LOAD_FACTOR);
317+
this(Hashing.FAST_MIX, initialCapacity);
318318
}
319319

320320
/**
321-
* Initializes a new {@link SyncMap} with the given initial capacity and load
322-
* factor.
321+
* Initializes a new {@link SyncMap} with the given mix function, the given
322+
* initial capacity and {@link SyncMap#DEFAULT_LOAD_FACTOR}.
323+
*
324+
* @param mixFunction the mix function
325+
* @param initialCapacity the initial capacity
326+
* @since 1.0.0
327+
*/
328+
public SyncMap(final Hashing.MixFunction mixFunction, final int initialCapacity) {
329+
this(mixFunction, initialCapacity, SyncMap.DEFAULT_LOAD_FACTOR);
330+
}
331+
332+
/**
333+
* Initializes a new {@link SyncMap} with {@link Hashing#FAST_MIX}, the
334+
* given initial capacity and the given load factor.
323335
*
324336
* @param initialCapacity the initial capacity
325337
* @param loadFactor the load factor
326338
* @since 1.0.0
327339
*/
328-
@SuppressWarnings({"rawtypes", "unchecked"})
329340
public SyncMap(final int initialCapacity, final float loadFactor) {
341+
this(Hashing.FAST_MIX, initialCapacity, loadFactor);
342+
}
343+
344+
/**
345+
* Initializes a new {@link SyncMap} with the given mix function, the given
346+
* initial capacity and the given load factor.
347+
*
348+
* @param mixFunction the mix function
349+
* @param initialCapacity the initial capacity
350+
* @param loadFactor the load factor
351+
* @since 1.0.0
352+
*/
353+
@SuppressWarnings({"rawtypes", "unchecked"})
354+
public SyncMap(final Hashing.MixFunction mixFunction, final int initialCapacity, final float loadFactor) {
330355
final int capacity = initialCapacity >= SyncMap.MAXIMUM_CAPACITY
331356
? SyncMap.MAXIMUM_CAPACITY
332357
: SyncMap.tableSizeFor(initialCapacity);
333358

359+
this.mixFunction = mixFunction;
334360
this.loadFactor = loadFactor;
335361
this.capacity = capacity;
336362

@@ -358,7 +384,7 @@ public boolean containsKey(final Object key) {
358384
Node<K, V> node, nextNode;
359385
int nodeHash; K nodeKey;
360386

361-
final int hash = SyncMap.spread(key.hashCode());
387+
final int hash = this.mixFunction.mix(key.hashCode());
362388

363389
if(length > 0 && (node = SyncMap.getNode(table, (length - 1) & hash)) != null) {
364390
if((nodeHash = node.hash) == hash) {
@@ -416,7 +442,7 @@ public boolean containsKey(final Object key) {
416442
Node<K, V> node, nextNode;
417443
int nodeHash; K nodeKey;
418444

419-
final int hash = SyncMap.spread(key.hashCode());
445+
final int hash = this.mixFunction.mix(key.hashCode());
420446

421447
if(length > 0 && (node = SyncMap.getNode(table, (length - 1) & hash)) != null) {
422448
if((nodeHash = node.hash) == hash) {
@@ -475,7 +501,7 @@ public V getOrDefault(final Object key, final V defaultValue) {
475501
Node<K, V> node, nextNode;
476502
int nodeHash; K nodeKey;
477503

478-
final int hash = SyncMap.spread(key.hashCode());
504+
final int hash = this.mixFunction.mix(key.hashCode());
479505

480506
if(length > 0 && (node = SyncMap.getNode(table, (length - 1) & hash)) != null) {
481507
if((nodeHash = node.hash) == hash) {
@@ -534,7 +560,7 @@ public V getOrDefault(final Object key, final V defaultValue) {
534560
Node<K, V>[] immutable, mutable = null; int length;
535561
Node<K, V> node; K nodeKey;
536562

537-
final int hash = SyncMap.spread(key.hashCode());
563+
final int hash = this.mixFunction.mix(key.hashCode());
538564

539565
V next;
540566
retry: for(; ; ) {
@@ -642,7 +668,7 @@ public V getOrDefault(final Object key, final V defaultValue) {
642668
Node<K, V>[] immutable, mutable; int length;
643669
Node<K, V> node; K nodeKey;
644670

645-
final int hash = SyncMap.spread(key.hashCode());
671+
final int hash = this.mixFunction.mix(key.hashCode());
646672

647673
V next; long count = 0L;
648674
retry: for(; ; ) {
@@ -738,7 +764,7 @@ public V getOrDefault(final Object key, final V defaultValue) {
738764
Node<K, V>[] immutable, mutable = null; int length;
739765
Node<K, V> node; K nodeKey;
740766

741-
final int hash = SyncMap.spread(key.hashCode());
767+
final int hash = this.mixFunction.mix(key.hashCode());
742768

743769
V next; long count = 0L;
744770
retry: for(; ; ) {
@@ -866,7 +892,7 @@ public V getOrDefault(final Object key, final V defaultValue) {
866892
Node<K, V>[] immutable, mutable = null; int length;
867893
Node<K, V> node; K nodeKey;
868894

869-
final int hash = SyncMap.spread(key.hashCode());
895+
final int hash = this.mixFunction.mix(key.hashCode());
870896

871897
retry: for(; ; ) {
872898
immutable = this.immutableTable; length = immutable.length;
@@ -961,7 +987,7 @@ public V getOrDefault(final Object key, final V defaultValue) {
961987
Node<K, V>[] immutable, mutable = null; int length;
962988
Node<K, V> node; K nodeKey;
963989

964-
final int hash = SyncMap.spread(key.hashCode());
990+
final int hash = this.mixFunction.mix(key.hashCode());
965991

966992
retry: for(; ; ) {
967993
immutable = this.immutableTable; length = immutable.length;
@@ -1094,7 +1120,7 @@ public V getOrDefault(final Object key, final V defaultValue) {
10941120
Node<K, V>[] immutable, mutable; int length;
10951121
Node<K, V> node; K nodeKey;
10961122

1097-
final int hash = SyncMap.spread(key.hashCode());
1123+
final int hash = this.mixFunction.mix(key.hashCode());
10981124

10991125
Object previous;
11001126
retry: for(; ; ) {
@@ -1179,7 +1205,7 @@ public boolean remove(final Object key, final Object value) {
11791205
Node<K, V>[] immutable, mutable; int length;
11801206
Node<K, V> node; K nodeKey;
11811207

1182-
final int hash = SyncMap.spread(key.hashCode());
1208+
final int hash = this.mixFunction.mix(key.hashCode());
11831209

11841210
retry: for(; ; ) {
11851211
immutable = this.immutableTable; length = immutable.length;
@@ -1266,7 +1292,7 @@ public boolean remove(final Object key, final Object value) {
12661292
Node<K, V>[] immutable, mutable; int length;
12671293
Node<K, V> node; K nodeKey;
12681294

1269-
final int hash = SyncMap.spread(key.hashCode());
1295+
final int hash = this.mixFunction.mix(key.hashCode());
12701296

12711297
Object previous;
12721298
retry: for(; ; ) {
@@ -1343,7 +1369,7 @@ public boolean replace(final @NonNull K key, final @NonNull V oldValue, final @N
13431369
Node<K, V>[] immutable, mutable; int length;
13441370
Node<K, V> node; K nodeKey;
13451371

1346-
final int hash = SyncMap.spread(key.hashCode());
1372+
final int hash = this.mixFunction.mix(key.hashCode());
13471373

13481374
Object previous;
13491375
retry: for(; ; ) {
@@ -1472,11 +1498,14 @@ public void clear() {
14721498
/**
14731499
* {@inheritDoc}
14741500
*
1475-
* <p>Creating an {@link Iterator} from this view, takes an immutable
1476-
* snapshot of the entries, meaning, modifications after calling
1477-
* {@link Set#iterator()} will not be visible. Calling
1478-
* {@link Iterator#remove()} will work provided the key-value pair is
1479-
* identical to the pair currently in the map.</p>
1501+
* <p>The {@link Iterator} produced by this view is weakly consistent: it
1502+
* iterates over an immutable snapshot captured at iterator creation time.
1503+
* New insertions after calling {@link Set#iterator()} are not reflected in
1504+
* the traversal.</p>
1505+
*
1506+
* <p>{@link Iterator#remove()} attempts to remove the last returned entry
1507+
* from the backing map, and succeeds only if the key still maps to the same
1508+
* value at the time of removal.</p>
14801509
*/
14811510
@Override
14821511
public Set<Map.Entry<K, V>> entrySet() {

0 commit comments

Comments
 (0)