Java 频率映射哈希映射中的长加法器与整数

Java 频率映射哈希映射中的长加法器与整数,java,java-8,Java,Java 8,我正在使用HashMap在单线程环境中构建一个频率映射。键是需要跟踪其频率的字符串 如果使用HashMap,则每个增量都需要一个新的整数 如果我可以简单地调用increment(),那么长加法器在这个用例中的性能会更好吗?一些初步测试表明,LongAdder确实表现得稍好一些,但我不确定为什么。测试以确定递增整数类型的相对性能 import java.util.HashMap; import java.util.List; import java.util.Map; import java.ut

我正在使用HashMap在单线程环境中构建一个频率映射。键是需要跟踪其频率的字符串

如果使用HashMap,则每个增量都需要一个新的整数


如果我可以简单地调用increment(),那么长加法器在这个用例中的性能会更好吗?一些初步测试表明,LongAdder确实表现得稍好一些,但我不确定为什么。

测试以确定递增整数类型的相对性能

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;
        }
    }
}
细节

  • 使用地图使数值更接近。国际海事组织,距离太近,无法做出明确的决定
  • 但是,使用最后三种类型就地递增似乎是最好的,因为值本身不必在映射中更新。对于
    LongAdder
    也一样,但同步代码可能是其性能不佳的一个因素(或测试设计者)。但是,可能有很多因素,包括我访问地图值的方法

我想我受够了。希望它能对这个问题有所帮助。

这是针对多线程环境的。除非您有并发问题,否则我看不出这会是一个什么样的改进。是的,这就是为什么我尝试了1亿次添加。长加法器的速度始终是整数的两倍。我不知道为什么会这样。这取决于地图条目的数量与每个条目的平均增量数量。装箱与同步开销。但是您不需要支付同步开销。您可以创建自己的对象,其中包含一个可变的
int
字段。或者使用长度为1的
int[]
的常用技巧,尽管带有字段的专用对象可能会稍快一些,因为它不需要范围检查。对于初学者,将每个测试放入一个单独的方法中,多次调用这些方法进行预热,然后测量它们的执行时间。同样值得在单独的运行时测试它们,例如,让主程序获取一个参数,告诉要测试的方法。虽然我认为在这种情况下这并不重要。但是请注意,OP在
HashMap
值更新的上下文中使用了增量操作,这可能比整个增量需要更多的时间。使用普通的
int
不是一个选项,但是长度为1的
int[]
就可以了(或者是一个带有可变
int
字段的专用对象)。@holger我接受了你的建议并做了一些更改。测试类应该按原样运行。请注意文章末尾的异常情况。在最好的情况下,优化器会检测到您没有使用总和。您可以更改返回总和的方法,然后对其执行一些操作,例如,如果(sum!=expectedValue)抛出…。但即使这样,优化器也可能会识别出您正在执行n个增量,可以通过返回值n来替换。如果您想更接近OPs场景,请使用
Map
并真正增加其值。然后将多余的时间测量代码移到调用方…现在这更接近预期。请注意,您不能直接将原语值存储在
HashMap
中,因此,
long
long
的操作完全相同,这解释了接近的数字。同样的情况也适用于
int
Integer
,因此差异是有问题的标志。也许是垃圾收集发生了。@tourniquet\u grab如前所述,
int
Integer
实际上也是这样做的。当你比较
int
long
long
的结果时,它们是合理的。
Integer
的结果是一个异常值。
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