【问题标题】:HashSet vs ArrayList CPU usage is highHashSet vs ArrayList CPU使用率高
【发布时间】:2015-08-06 08:01:57
【问题描述】:

我有 104k 字符串值,其中 89k 是唯一的。我想检查此列表中是否存在字符串。

这是我的类及其保存所有这些记录的方法。

public class TestClass {
    private static TestClass singletonObj = null;
    private List<String> stringList= null;

    public static synchronized TestClass getInstance() {
        if(singletonObj == null) {
            singletonObj = new TestClass();
        }
        return singletonObj;
    }


    public boolean isValidString(String token) {
        if(stringList == null) {
            init();
        }
        if(stringList != null && token != null && !token.isEmpty())
            return stringList.contains(token.toLowerCase());
        return false;
    }

    private init() {
     stringList = new ArrayList<String>();
     // put all 104k values in this data structure.
    }
}

我的应用程序尝试同时使用这个isValidString() 方法,每秒大约有 20 个请求。这工作正常,但是当我尝试将数据结构更改为HashSet 时,CPU 使用率变得非常高。根据我的理解,Hashset 应该比 ArrayList[o(n)] 表现更好[o(1)]。谁能解释一下为什么会这样?

【问题讨论】:

  • 你的 init() 方法不应该在同步块中吗?
  • 同意@Codebender 如果你的调用是单例的那么你可以在getInstance()的时候创建它,我相信它永远只有一个列表或集合。
  • @Sthita - 你的isValidString() 应该有一个同步块,确保init(); 只被调用一次。您还需要检查null 两次。
  • @ankitkatiyar91,如果你让inValidString()同步它会破坏多线程的整个目的,你应该只让if(stringList==null)条件同步。
  • 拥有一个延迟加载的stringList 的延迟实例化单例有什么意义?您必须在查询有意义之前加载集合,因此您不会在那里获得任何并行性。那么为什么不在构造函数中初始化呢?

标签: java performance arraylist hashset


【解决方案1】:

根据这篇文章的底部,我创建了一个简单的类来生成 20 个线程,这些线程每秒访问您的字典检查器。

我无法复制您的结果 - 但这可能是因为我可以访问的输入数据。我使用了您的 TestClass 实现,从英语开放单词列表 (EOWL) 中导入了约 130,000 个单词。在ArrayListHashSet 作为stringList 的类型时,没有看到持续的高CPU 使用率。

我的猜测是你的问题是由于你的输入数据。我尝试添加我的输入字典两次以创建重复 - 显然使用 ArrayList 这只会使列表长度增加一倍,但使用 HashSet,这意味着代码正在丢弃重复项。您注意到大约 1/5 的输入数据是重复的。在我的测试中有 1/2 重复时,我确实看到 CPU轻微 增加了大约 2 秒,然后在 stringList 初始化后几乎没有下降。

如果您的输入字符串比我使用的单个单词更复杂,则此“blip”可能会更长。所以也许这就是你的问题。或者 - 也许你有一些其他代码来包装这个占用 CPU 的部分。

注意我会像 cmets 中的其他人一样提醒您注意您对 init 的实施。在我的实验中,我看到许多线程可以在字典完全初始化之前调用字典检查,从而为同一个测试词给出不一致的结果。如果它是一个单例对象,为什么不从你的构造函数中调用它呢?

代码清单

带有一些输入数据代码的 TestClass:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;

public class TestClass {
    private static TestClass singletonObj = null;
    //private List<String> stringList = null;

    private HashSet<String> stringList = null;

    public static synchronized TestClass getInstance() {
        if (singletonObj == null) {
            singletonObj = new TestClass();
        }
        return singletonObj;
    }

    public boolean isValidString(String token) {
        if (stringList == null) {
            init();
        }
        if (stringList != null && token != null && !token.isEmpty())
            return stringList.contains(token.toLowerCase());
        return false;
    }

    private void init() {
        String dictDir = "C:\\Users\\Richard\\Documents\\EOWL_CSVs";
        File[] csvs = (new File(dictDir)).listFiles();
        stringList = new HashSet<String>();
        Scanner inFile = null;

        for (File f : csvs) {
            try {
                inFile = new Scanner(new FileReader(f));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                System.exit(1);
            }

            while (inFile.hasNext()) {
                stringList.add(inFile.next().toLowerCase()
                        .replaceAll("[^a-zA-Z ]", ""));
            }
            inFile.close();
        }

        System.out.println("Dictionary initialised with " + stringList.size()
                + " members");
    }
}

线程访问它:

import java.io.FileNotFoundException;

public class DictChecker extends Thread {

    TestClass t = null;
    public static int classId = 0;
    String className = null;
    
    
    public void doWork()
    {
        String testString = "Baby";
        if (t.isValidString(testString))
        {
            System.out.println("Got a valid string " + testString + " in class " + className);
        }
        else
        {
            System.out.println(testString + " not in the dictionary");
        }
    }
    
    public void run()
    {
        while (true)
        {
            try {
                DictChecker.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            doWork();
        }
    }
    
    public DictChecker()
    {
        t = TestClass.getInstance();
        className = "dChecker" + classId;
        classId += 1;
        System.out.println("Initialised " + className + " in thread " + this.getName());
    }
    
    public static void main(String[] args) throws FileNotFoundException
    {
        for (int i = 0; i < 20; i++)
        {
             (new DictChecker()).start();
             try {
                DictChecker.sleep(50);//simply to distribute load over the second
            } catch (InterruptedException e) {
                e.printStackTrace();
            } 
        }
    }
}

【讨论】:

  • @J Richard Snape 是的,我正在从构造函数初始化它,它目前是具有初始大小的 HashSet,到目前为止看起来还不错。
  • @sthita 很高兴你能按照你想要的方式工作:)
【解决方案2】:

我的猜测是HashSet,作为一个基于散列的结构,计算每个字符串的散列码从插入到散列集的那一刻,即在方法init中。这可能是 CPU 变高的时期,也是我们在迭代结构的值时为获得更好的吞吐量而付出的一部分代价。

如果我是对的,在init方法结束后,CPU应该会下降,程序的速度应该会大大提高,这就是使用HashSet的好处。

顺便说一句:优化的可靠方法是预先调整结构大小

  • ArrayList 的初始大小应等于将包含的最大元素数。
  • 并且 HashSet 的初始大小比最大值大 1.7。

顺便说一句:String.hash 的标准哈希算法计算字符串的所有字符。例如,也许您可​​以满足于仅计算前 100 个字符(具体取决于您正在处理的数据的性质)。然后,您可以将您的字符串封装到您自己的类中,用您自己的哈希算法覆盖hashCode 方法,并覆盖equals 方法以执行严格的比较。

【讨论】:

  • 不,CPU 使用率根本没有下降,如果我使用 HashSet,一段时间后应用程序会停止响应。我正在使用 ArrayList 并给出初始大小。
  • 您是否使用具有初始大小的 HashSet?如果不是,那可能是原因:一个过满的小散列结构变得低效,因为它变成了一组长列表。
  • @Sthita - 检查 GC 是否也在启动
  • @LittleSanti 使用初始大小设置 HashSet 看起来不错,我认为对我来说很有效。今天我将继续监控我的应用程序并查看其影响。谢谢
【解决方案3】:

JDK HashSet 建立在 HashMap&lt;T, Object&gt; 之上,其中 value 是一个单例“present”对象。这意味着 HashSet 的内存消耗与 HashMap 相同:为了存储 SIZE 值,您需要 32 * SIZE + 4 * CAPACITY 字节(加上值的大小)。

对于ArrayList,它是java.util.ArrayList 的容量乘以引用大小(4 bytes on 32bit, 8bytes on 64bit) + [Object header + one int and one references]

所以 HashSet 绝对不是一个内存友好的集合。

取决于您使用的是32-bit 还是64-bit 虚拟机。也就是说,8-byte 引用对 HashSet 的伤害比 ArrayList 更严重——根据链接的内存消耗图表,为每个引用添加一个额外的 4 bytes,使 ArrayList 每个元素达到 ~12 字节,而 HashSet 达到 @每个元素 987654335@ 个字节。)

ArrayList 是使用对象数组实现的。下图显示了 ArrayList 在 32 位 Java 运行时的内存使用和布局:

ArrayList 在 32 位 Java 运行时的内存使用和布局

上图显示,当一个ArrayList被创建时,结果是一个ArrayList对象使用32 bytes的内存,以及一个默认大小为10的Object数组,总计88 bytes的空ArrayList 的内存。这意味着ArrayList 的大小不准确,因此具有默认容量,恰好是10 entries

ArrayList 的属性

Default capacity - 10

Empty size - 88 字节

Overhead - 48 字节加上每个条目 4 字节

Overhead 收集 10K - ~40K

Search/insert/delete performance- O(n) — 所用时间与元素数量呈线性关系

HashSet 的功能比 HashMap 少,因为它不能包含多个空条目并且不能有重复的条目。该实现是 HashMap 的包装器,其中 HashSet 对象管理允许放入 HashMap 对象的内容。限制 HashMap 能力的附加功能意味着 HashSet 的内存开销略高。

32 位 Java 运行时 HashSet 的内存使用和布局

上图显示了 java.util.HashSet 对象的浅堆(单个对象的内存使用量)(以字节为单位),以及保留堆(单个对象及其子对象的内存使用量)(以字节为单位)。浅堆大小为16 bytes,保留堆大小为144 bytes。创建 HashSet 时,其默认容量(可放入集合中的条目数)为 16 entries。当一个 HashSet 以默认容量创建并且没有条目放入集合时,它占用144 bytes。这是一个额外的 16 bytes 超过 HashMap 的内存使用量。

下表显示了 HashSet 的属性:

HashSet 的属性

Default capacity -16 个条目

Empty size -144 字节

Overhead -16 字节加上 HashMap 开销

Overhead for a 10K collection -16 字节加上 HashMap 开销

Search/insert/delete performance - O(1) - 所用时间是常数时间,与元素数量无关 (假设没有哈希冲突)

【讨论】:

  • 参考表明迭代性能更好,这里我们只是想比较/检查列表/集合中令牌的可用性。
  • “如果你不关心... contains 的性能”但contains 我们在这里最常调用的方法,对吧?跨度>
  • @Sthita - 请参阅参考 2 中的容量和上面 arrayList 的添加容量。
  • OP 是在谈论 CPU 使用率,而不是内存。他们显然确实关心包含的性能。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-09
  • 2014-07-16
  • 2014-08-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多