【问题标题】:Java hashmap vs hashset performanceJava hashmap vs hashset 性能
【发布时间】:2017-03-01 11:05:12
【问题描述】:

我有一个包含 7.6M 行的文件。每行的格式为:A,B,C,D 其中 B,C,D 是用于计算 A 的重要性级别的值,A 是每行唯一的字符串标识符。我的做法:

private void read(String filename) throws Throwable {
        BufferedReader br  = new BufferedReader(new FileReader(filename));

        Map<String, Double> mmap = new HashMap<>(10000000,0.8f);
        String line;
        long t0 = System.currentTimeMillis();
        while ((line = br.readLine()) != null) {
            split(line);
            mmap.put(splitted[0], 0.0);
        }
        long t1 = System.currentTimeMillis();
        br.close();
        System.out.println("Completed in " + (t1 - t0)/1000.0 + " seconds");
}

private void split(String line) {
    int idxComma, idxToken = 0, fromIndex = 0;
    while ((idxComma = line.indexOf(delimiter, fromIndex)) != -1) {
        splitted[idxToken++] = line.substring(fromIndex, idxComma);
        fromIndex = idxComma + 1;
    }
    splitted[idxToken] = line.substring(fromIndex);
}

其中插入虚拟值 0.0 以用于“分析”目的,并拆分为为该类定义的简单字符串数组。我最初使用 String 的 split() 方法,但发现上面的方法更快。

当我运行上面的代码时,解析文件需要 12 秒,这比我认为的要多得多。例如,如果我将 HashMap 替换为字符串向量并仅从每一行中获取第一个条目(即,我没有将关联值与其关联,因为这应该是摊销常量),则整个文件可以在不到3 秒。

这向我表明 (i) HashMap 中有很多冲突(我试图通过预先分配大小并相应地设置负载因子来最小化调整大小的次数)或 (ii) hashCode() 函数有点慢。我怀疑它的 (ii),因为如果我使用 HashSet,可以在 4 秒内读取文件。

我的问题是:HashMap 执行如此缓慢的原因可能是什么? hashCode() 对于这种大小的地图是不够的,还是我根本上忽略了一些东西?

【问题讨论】:

  • 尝试用一些静态最终常量替换您的0.0 虚拟值。 0.0 被 Double.valueOf 替换,每次都会创建一个新对象。而在HashSet 中,只使用了一个预先分配的虚拟对象。我不确定是不是这个原因,但可能是
  • splitted[] 的最后一个元素将始终占据整行。这不是你想要的。
  • HashSet 在内部由 HashMap 支持,所以唯一的区别是你的虚拟 0.0 的自动装箱。
  • 您能否将 io 和标记化与地图插入分开,以确保您没有专注于错误的事情? micro benchmarking
  • String.split() 速度较慢,因为它会在每次调用时分配一个新的正则表达式 Pattern。尝试创建private static final Pattern SPLITTER = Pattern.compile(","); 然后SPLITTER.split(line)。

标签: java performance hashmap hashcode


【解决方案1】:

HashMap vs Vector: 在 HashMap 中插入比在 Vector 中插入要昂贵得多。虽然两者都是摊销的常数时间操作,但 HashMap 在内部执行了许多其他操作(如生成 hashCode、检查碰撞、解决碰撞等),而 Vector 只是在末尾插入元素(增加结构的大小,如果需要)。

HashMap vs HashSet: HashSet 内部使用 HashMap。因此,如果您将它们用于相同目的,则不应该有任何性能差异。理想情况下,两者都有不同的目的,所以讨论哪个更好是没有用的。

既然你需要 B、C、D 作为值,而 A 作为键,你绝对应该坚持使用 HashMap。如果您真的只想比较性能,请将“null”而不是 0.0 作为所有键的值(因为这是 HashSet 在将键放入其支持的 HashMap 时使用的值)。

更新:HashSet 使用虚拟常量值(静态最终值)插入 HashMap,而不是 null。对于那个很抱歉。您可以将您的 0.0 替换为任何常量,并且性能应该类似于 HashSet。

【讨论】:

    【解决方案2】:

    您可以使用内存效率更高的 Collections 库。

    我建议 Eclipse Collections (https://www.eclipse.org/collections/),它有一个 ObjectDoubleMap (https://www.eclipse.org/collections/javadoc/8.0.0/org/eclipse/collections/api/map/primitive/ObjectDoubleMap.html),它是一个对象映射 (在你的例子中是字符串),它有一个双精度 (是的,原始双精度) 作为关联值。它在处理内存和性能方面要好得多。

    您可以通过以下方式获得一个空实例:

    ObjectDoubleMaps.mutable.empty();
    

    【讨论】:

      【解决方案3】:

      是的,使用0.0 作为虚拟值 VS 静态最终常量作为虚拟值 VS HashSet 检查了您的示例。这是粗略的比较,为了获得更好的精度,我建议使用JHM 工具,但我的HashSet 性能与静态常量和虚拟性能几乎相同。

      因此,性能低下很可能是由于为每一行包装了 0.0 虚拟值(在编译期间它被 Double.valueOf() 替换,这每次都会显式创建一个新的 Double 对象)。

      这可以解释性能低下的原因,因为HashSet 具有预定义的静态最终虚拟对象(不是null,顺便说一句)。

      【讨论】:

        猜你喜欢
        • 2015-02-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-06-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多