tourniquet_grab
tourniquet_grab

Reputation: 911

LongAdder vs Integer in Hash map for frequency map

I am constructing a frequency map in a single-threaded environment using a HashMap. The keys are the Strings whose frequencies needs to be tracked.

If I use HashMap<String, Integer>, each increment needs a new Integer.

Would a LongAdder perform better for this use case as I can simply call increment()? Some rudimentary testing showed that LongAdder does indeed perform slightly better but I am not sure why.

Upvotes: 0

Views: 231

Answers (1)

WJS
WJS

Reputation: 40062

Testing to determine relative performance of incrementing integral types.

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Function;

public class LongAdderTest {
    
    public static void main(String[] args) {
        new LongAdderTest().start();
    }
    
    public void start() {
        int N = 100_000_000;
        int warmup = 3;
        String[] testNames = { "LongAdder", "Long", "Integer", "long",
                "int", "Object", "int[]", "long[]" };
        List<Function<Integer, Long>> tests = List.of(
                this::longAdderTest, this::longWrapperTest,
                this::integerWrapperTest, this::primitiveLongTest,
                this::primitiveIntTest, this::objectTest,
                this::intArrayTest, this::longArrayTest);
        
        int i = 0;
        for (Function<Integer, Long> test : tests) {
            runTest(test, warmup, N, testNames[i++]);
        }
    }
    
    public void runTest(Function<Integer, Long> test, int warmup,
            int iterations, String testName) {
        // warmup cycle
        for (int i = 0; i < warmup; i++) {
            long v = test.apply(iterations);
            if (v != iterations) {
                System.out
                        .println("Unexpected result - return = " + v);
            }
        }
        long start = System.nanoTime();
        long val = test.apply(iterations);
        System.out.printf("%-10s : %12f  %d%n", testName,
                (System.nanoTime() - start) / 1_000_000., val);
    }
    
    public long longAdderTest(int iter) {
        LongAdder val = new LongAdder();
        Map<String, LongAdder> freq = new HashMap<>();
        freq.put("A", val);
        for (int i = 0; i < iter; i++) {
            freq.get("A").increment();
        }
        return freq.get("A").longValue();
    }
    
    public long longWrapperTest(int iter) {
        Long val = 0L;
        Map<String, Long> freq = new HashMap<>();
        freq.put("A", val);
        for (int i = 0; i < iter; i++) {
            freq.computeIfPresent("A", (k, v) -> v + 1);
        }
        return freq.get("A");
    }
    
    public long integerWrapperTest(int iter) {
        Integer val = 0;
        Map<String, Integer> freq = new HashMap<>();
        freq.put("A", val);
        for (int i = 0; i < iter; i++) {
            freq.computeIfPresent("A", (k, v) -> v + 1);
        }
        return freq.get("A");
    }
    
    public long primitiveLongTest(int iter) {
        Map<String, Long> freq = new HashMap<>();
        long val = 0;
        freq.put("A", val);
        for (int i = 0; i < iter; i++) {
            freq.computeIfPresent("A", (k, v) -> v + 1);
        }
        return freq.get("A");
    }
    
    public long primitiveIntTest(int iter) {
        Map<String, Integer> freq = new HashMap<>();
        int val = 0;
        freq.put("A", val);
        for (int i = 0; i < iter; i++) {
            freq.computeIfPresent("A", (k, v) -> v + 1);
        }
        return freq.get("A");
    }
    
    public long intArrayTest(int iter) {
        Map<String, int[]> freq = new HashMap<>();
        int[] val = { 0 };
        freq.put("A", val);
        for (int i = 0; i < iter; i++) {
            freq.get("A")[0] += 1;
        }
        return freq.get("A")[0];
    }
    
    public long longArrayTest(int iter) {
        Map<String, long[]> freq = new HashMap<>();
        long[] val = { 0L };
        freq.put("A", val);
        for (int i = 0; i < iter; i++) {
            freq.get("A")[0] += 1;
        }
        return freq.get("A")[0];
        
    }
    
    public long objectTest(int iter) {
        MyLongIncrement longObject = new MyLongIncrement(0);
        Map<String, MyLongIncrement> freq = new HashMap<>();
        freq.put("A", longObject);
        for (int i = 0; i < iter; i++) {
            freq.get("A").increment();
        }
        return freq.get("A").get();
    }
    
    static class MyLongIncrement {
        long val;
        
        public MyLongIncrement(long v) {
            this.val = v;
        }
        
        public long get() {
            return val;
        }
        
        public void increment() {
            val += 1l;
        }
    }
}

Sample run.

LongAdder  :  4166.724472  100000000
Long       :  2929.021352  100000000
Integer    :  5487.358323  100000000
long       :  2993.538570  100000000
int        :  2505.171838  100000000
Object     :  1032.322116  100000000
int[]      :  1132.710126  100000000
long[]     :  1107.633331  100000000

Details

  • Using a Map brought the values closer. Imo, too close to make a definitive call.
  • But it would seem that incrementing in place with the last three types might be best since the value itself does not have to be updated in the map. Same for the LongAdder but the synchronization code could be a factor (or the test designer) for its less than stellar performance. But then, there could be many factors including my method of accessing the map value.

I think I'm done with this. Hope it shed some light on the issue.

Upvotes: 1

Related Questions