1

我们运行了附加的测试。结果一致表明,通过 Array 按索引访问比通过 Map 通过键访问快 10 倍。这种数量级的差异让我们感到惊讶。

我们的 Map 键是 java.lang.String ...计算 Map 键的 java.lang.String.hashcode() 实现的成本是唯一原因吗?在附加的代码中,我只使用了一个键

java.lang.String key = 1; 

在这种情况下编译器/运行时不缓存吗?还是在每次调用时重新计算?

感谢您的任何见解。

public class PerfTest {
static java.util.HashMap<String, Double> map;
static Double[] array = {1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0};

static long nTimes = 1000000;

static{
    map = new java.util.HashMap<String, Double>();
    map.put("1",    new Double(1));
    map.put("2",    new Double(2));
    map.put("3",    new Double(3));
    map.put("4",    new Double(4));
    map.put("5",    new Double(5));
    map.put("6",    new Double(6));
    map.put("7",    new Double(7));
    map.put("8",    new Double(8));
    map.put("9",    new Double(9));
    map.put("10",   new Double(10));        
}

public static void main(String[] args){

    PerfTest tester = new PerfTest();
    long timeInMap  = tester.testHashMap();
    long timeInArray    = tester.testArray();

    System.out.println("Corrected time elapsed in map(in seconds): "        
            + (timeInMap)/1000000000.0);
    System.out.println("Corrected time elapsed in array(in seconds): " 
            + (timeInArray)/1000000000.0);
}

private long testHashMap(){
    int sz = map.size();
    long startTime = System.nanoTime();

    String key = "1";   

    for (int i=0; i <nTimes; i++){
        double sum = 0; 

        for (int j =1; j<=sz; j++){
            sum += map.get(key);                            
        }
    }
    return (System.nanoTime() - startTime);
}

private long testArray(){
    long startTime = System.nanoTime();

    for (int i=0; i <nTimes; i++){
        double sum = 0;

        for (int j=0; j< array.length; j++) {       
            sum += array[j];
        }
    }   
    return (System.nanoTime() - startTime);
   }
 }
4

5 回答 5

6

使用 Java 的系统时间并不是获得真正基准的好方法。我重构了您的代码以使用Google Caliper(除其他外,它可以预热 JVM)......并发现了与您类似的结果。评论者正确地指出我的原始版本不好,而且大部分时间都会System.out.println打电话。

就像我说的,编写基准测试很难。下面更新的是新的正确版本。

结果:

 0% Scenario{vm=java, trial=0, benchmark=HashMap} 51.04 ns; σ=0.22 ns @ 3 trials
50% Scenario{vm=java, trial=0, benchmark=Array} 4.05 ns; σ=0.01 ns @ 3 trials

benchmark    ns linear runtime
  HashMap 51.04 ==============================
    Array  4.05 ==

代码:

import com.google.caliper.Runner;
import com.google.caliper.SimpleBenchmark;

public class PerfTest {
    public static double hashNum = 0;
    public static double arrayNum = 0;

    public static class PerfBenchmark extends SimpleBenchmark {
        static java.util.HashMap<String, Double> map;
        static Double[] array = {1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0};

        static{
            map = new java.util.HashMap<String, Double>();
            map.put("1",    new Double(1));
            map.put("2",    new Double(2));
            map.put("3",    new Double(3));
            map.put("4",    new Double(4));
            map.put("5",    new Double(5));
            map.put("6",    new Double(6));
            map.put("7",    new Double(7));
            map.put("8",    new Double(8));
            map.put("9",    new Double(9));
            map.put("10",   new Double(10));        
        }

        public void timeHashMap(int nTimes){
            int sz = map.size();

            String key = "1";   

            for (int i=0; i <nTimes; i++){
                double sum = 0; 

                for (int j =1; j<=sz; j++){
                    sum += map.get(key);                            
                }

                hashNum += sum;
            }
        }

        public void timeArray(int nTimes){
            for (int i=0; i <nTimes; i++){
                double sum = 0;

                for (int j=0; j< array.length; j++) {       
                    sum += array[j];
                }

                arrayNum += sum;
            }
        }
    }

    public static void main(String[] args){
        Runner.main(PerfBenchmark.class, new String[0]);

        System.out.println(hashNum);
        System.out.println(arrayNum);
    }
}
于 2013-05-01T21:54:24.017 回答
3

我可以重现你的结果,并解释它们。

再生产

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("array lookup") {
                Double[] array = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };

                @Override
                int run(int iterations) throws Throwable {
                    double sum = 0;
                    for (int i = 0; i < iterations; i++) {

                        for (int j = 0; j < array.length; j++) {
                            sum += array[j];
                        }
                    }
                    return (int) sum;
                }
            }, new Benchmark("map lookup") {
                Map<String, Double> map = new HashMap<>();
                {
                    map.put("1",    new Double(1));
                    map.put("2",    new Double(2));
                    map.put("3",    new Double(3));
                    map.put("4",    new Double(4));
                    map.put("5",    new Double(5));
                    map.put("6",    new Double(6));
                    map.put("7",    new Double(7));
                    map.put("8",    new Double(8));
                    map.put("9",    new Double(9));
                    map.put("10",   new Double(10));   
                }
                @Override int run(int iterations) throws Throwable {
                    String key = "1";   
                    double sum = 0; 
                    for (int i=0; i <iterations; i++){
                        for (int j =1; j<=map.size(); j++){
                            sum += map.get(key);
                        }
                    }                   
                    return (int) sum;
                }

            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

在我有点过时的笔记本上,而 JDK 1.7 是 -server 模式,这会打印:

array lookup   15.250 ns
map lookup    124.946 ns

你问,为什么我得到的结果与 durron597 不同?正如我在评论中指出的那样,他每次迭代都会打印到 System.out。如果这确实是 I/O,它比地图查找要昂贵得多。您可以通过将基准更改为:

            }, new Benchmark("array lookup with printing") {
                Double[] array = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };

                @Override
                int run(int iterations) throws Throwable {
                    for (int i = 0; i < iterations; i++) {
                        double sum = 0;

                        for (int j = 0; j < array.length; j++) {
                            sum += array[j];
                        }
                        System.out.println(sum);
                    }
                    return 0;
                }
            }, new Benchmark("map lookup with printing") {
                Map<String, Double> map = new HashMap<>();
                {
                    map.put("1",    new Double(1));
                    map.put("2",    new Double(2));
                    map.put("3",    new Double(3));
                    map.put("4",    new Double(4));
                    map.put("5",    new Double(5));
                    map.put("6",    new Double(6));
                    map.put("7",    new Double(7));
                    map.put("8",    new Double(8));
                    map.put("9",    new Double(9));
                    map.put("10",   new Double(10));   
                }
                @Override int run(int iterations) throws Throwable {
                    String key = "1";   
                    for (int i=0; i <iterations; i++){
                        double sum = 0; 
                        for (int j =1; j<=map.size(); j++){
                            sum += map.get(key);
                        }
                        System.out.println(sum);
                    }                   
                    return 0;
                }
            }

打印以下时间(如果 System.out 是一个文件,则 Eclipse 控制台的数字相似)

array lookup with printing  43301.251 ns
map lookup with printing    18330.935 ns

这大约是不打印的 100 倍,所以我们这里主要测量 I/O。

解释

数组查找只涉及检查数组索引是否有效、将索引添加到基地址以及从内存中读取一个字。在您的情况下,您甚至在数组上循环,这使 Java Hotspot VM 可以跳过边界检查。

HashMap 查找执行以下代码:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

final Entry<K,V> getEntry(Object key) {
    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

final int hash(Object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

static int indexFor(int h, int length) {
    return h & (length-1);
}

// from String.class
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}


public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    ... 
}

即使考虑到很少使用某些分支,映射查找所包含的操作也远远超过数组访问。需要更长的时间也就不足为奇了。

当然,这种性能差异在实际代码中几乎不重要——而且比较是不公平的,因为 Map 比数组灵活得多。

于 2013-05-01T23:10:04.497 回答
2

是的,这里的费用是计算密钥。

如果您知道索引,则没有理由使用像 HashMap 这样的复杂数据结构来代替普通数组。

当您的密钥未知并且基于对象的内容时,您可能希望使用 HashMap。因此,一个更有效的示例是从您要查找的对象开始并在数组中搜索它,而不是知道它在哪里,因为 HashMap 就是这样做的。

于 2013-05-01T21:52:38.507 回答
1

get()需要散列键,并且还需要对键执行相等比较(因为有可能两个不同的键散列到后备数组中的相同索引) - 如果你这样做,你的性能比较会更加不平衡使用了超过 10 个键/数组元素,因为这会增加该String#equals方法的平均成本(尽管您可以使用 a 来避免这种情况HashMap<Integer, Double>

这就是 HashMap#get 正在做的事情 -for循环适用于表存储散列到后备数组中相同索引的多个键的情况(这可能在您的性能测试中没有发生,这意味着循环仅执行一次迭代)

for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
        return e.value;
}
于 2013-05-01T21:55:24.757 回答
1

如果您了解 HashMap 实际上是一个哈希表,它将您的数据分散在整个底层数组中,并且必须计算一个索引,在数组中查找它并将其返回,这并不奇怪。另一方面,数组是一个连续的内存块,在查找索引位置时不涉及任何计算。

除此之外,您正在以非常可预测的顺序访问数组,因此像所有现代处理器一样预取内存不会导致任何未命中。

于 2013-05-01T21:53:44.737 回答