【问题标题】:Performance of ArrayList vs HashSetArrayList 与 HashSet 的性能
【发布时间】:2015-04-10 03:59:49
【问题描述】:

我知道当我们需要存储重复项时,我们更喜欢ArrayList 而不是HashSet,并且HashSet 使用hashCode() 函数来计算其数组中每个元素的索引。

所以,这意味着如果我们想要存储单个元素,那么ArrayList 应该比HashSet 花费更少的时间。如果我在任何地方错了,请纠正我。

但是当我通过代码检查性能时,我得到了不同的行为。

案例一:

import java.util.*;
class HashsetVSArraylist
{
 public static void main(String args[])
 {
  ArrayList<Integer> a1=new ArrayList<Integer>();
  long nanos = System.nanoTime();

  a1.add(1);

  System.out.println("ArrayList Time:"+(System.nanoTime()-nanos)+"ns");

  HashSet<Integer> h1=new HashSet<Integer>();
  nanos = System.nanoTime();

  h1.add(2);

  System.out.println("HashSet Insertion Time:"+(System.nanoTime()-nanos)+"ns");
 }
}
Output:
ArrayList Time:495087ns
HashSet Insertion Time:21757ns

案例 2:

import java.util.*;
class HashsetVSArraylist
{
 public static void main(String args[])
 {
  HashSet<Integer> h1=new HashSet<Integer>();
  long nanos = System.nanoTime();

  h1.add(2);

  System.out.println("HashSet Insertion Time:"+(System.nanoTime()-nanos)+"ns");

  ArrayList<Integer> a1=new ArrayList<Integer>();
  nanos = System.nanoTime();

  a1.add(1);

  System.out.println("ArrayList Time:"+(System.nanoTime()-nanos)+"ns");
 }
}
Output:
HashSet Insertion Time:582527ns
ArrayList Time:21758ns

现在,我假设HashSet 应该花费更多时间来插入单个元素。但是,在这两种情况下,行为都是不同的……在代码中排在第二位的数据结构花费的时间更少。此外,当插入的元素数量超过 1000 个时,行为会发生变化。

请解释发生了什么。

【问题讨论】:

  • 您的测试只有在您测量大量元素时才有效。尝试将 100,000 个元素添加到 5 次中,然后取平均值。
  • 你应该看看这个...... [这个][1] [1]:stackoverflow.com/questions/17985029/hashset-vs-arraylist
  • 您最初假设hashCode 会导致较小的开销在某些情况下确实成立。通过小数组的线性搜索可以比散列查找执行得更好。 (并且还有缓存考虑因素。HashSet 可能会做更多的负载。)但是,它是一种微优化,而不是您想要设计代码的类型。差别很小。

标签: java arraylist hashset


【解决方案1】:

你的基准被打破了。在尝试使用 Java 进行基准测试之前,请阅读:Dynamic compilation and performance measurement 和:Anatomy of a flawed microbenchmark。

简短的解释是,您尝试测量的总持续时间太长,much太短了,基准测试结果将被微型操作系统淹没,并且CPU 详细信息,以及 Java VM 在开始运行时仍在忙于将字节码编译为机器码这一事实。

同时,当 ArrayList 和 HashList 用于两个不同的目的时,比较它们的性能有点疯狂,但在其他条件相同的情况下,ArrayList 的实现要简单得多,所以你的假设肯定是正确的;它会更快。

【讨论】:

    【解决方案2】:

    Java 在将整数原语转换为 Integer 对象时所做的自动装箱隐藏了这里的真正问题。

    当您调用 a1.add(1) 时,这实际上是在调用 a1.add(Integer.valueOf(1))

    第一次引用 Integer 类的静态 valueOf 方法时,会导致 Integer 类中的静态初始化程序执行,这会创建数百个静态对象,并且在您的系统上大约需要 500 毫秒。

    即便如此,幕后还有很多其他事情会干扰此测试,例如其他静态初始化程序、动态内存分配、系统资源分配以及无数其他事情。

    如果您可以设计一个测试来消除或最小化这些变量,那么您会发现从长远来看,添加到 ArrayList 总是比 HashSet 快,但对于任何给定的插入都不是。幸运的是,我们不应该关心单次插入的速度。

    例如,想象一下尝试向 ArrayList 添加值的最坏情况,但 ArrayList 处于其最大分配大小。 ArrayList 尝试分配更多空间,但系统已达到其当前内存分配上限,因此它需要等待 VM 为系统分配更多内存。同时垃圾收集器被启动。在这种情况下,通常可能需要

    【讨论】:

    • 整数从很早的时候就存在于虚拟机中。在调用main 时,Integer 类肯定已经初始化了。
    • 为什么 IntegerCache 会在 main 之前初始化?没有Integer对象传入main,主类中没有Integers被引用?
    • 在你进入 main 之前已经加载了几百个类。使用-verbose:class 参数来查看它们。
    • @Boann 我们都是正确的。 Integer 是提前加载的,但是 IntegerCache 子类只有在 Integer.valueOf 被调用时才被初始化。试试这个 -verbose:class public class HelloWorld{ public static void main(String args[]) { System.out.println("Start\n"); Integer.valueOf(1); System.out.println("End\n"); } }
    • 看看Integer和IntegerCache在加载时间上的差异很有意思;这是我忽略考虑的事情。但至少在我的虚拟机上,这两个类在“开始”之前很久就加载了。似乎sun.misc.Signal 在调用signals.put(sig.number, sig); 时首先调用Integer.valueOf(通过自动装箱)。
    猜你喜欢
    • 2012-04-29
    • 1970-01-01
    • 1970-01-01
    • 2016-03-06
    • 2016-10-08
    • 1970-01-01
    • 2013-09-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多