处理具有1亿个元素的ArrayList时,提高速度和内存消耗

时间:2015-09-16 07:16:53

标签: java performance arraylist hashset

我使用包含短字符串的文本文件(10位数)。文件大小约为1.5Gb,因此行数达到1亿。

每天我都会收到另一个文件并需要提取新元素(每天数万个)。

解决问题的最佳方法是什么?

我尝试在ArrayList中加载数据 - 每个文件大约需要20秒,但数组的减法需要永远。

我使用此代码:

dataNew.removeAll(dataOld);

试图在HashSets中加载数据 - 创建HashSets是无止境的。 与LinkedHashset相同。

尝试加载到ArrayLists并只对其中一个进行排序

Collections.sort(dataNew);

但它没有加快

的进程
dataNew.removeAll(dataOld);

内存消耗也相当高 - sort()仅在15Gb的堆上完成(13Gb还不够)。

我尝试使用旧的好的linux util diff,它在76分钟内完成了任务(同时吃了8Gb的RAM)。

所以,我的目标是在处理时间的1小时内(或者更少,当然)和消耗15Gb(或更好的8-10Gb)来解决Java中的问题。

有什么建议吗? 也许我不需要对ArrayList进行字母排序,还需要其他东西吗?

更新 这是一份全国范围的无效护照清单。它作为全局列表发布,所以我需要自己提取delta。

数据未排序,每行都是唯一的。所以我必须将100M元素与100M元素进行比较。 Dataline例如是“2404,107263”。无法转换为整数。

有趣的是,当我将最大堆大小增加到16Gb

java -Xms5G -Xmx16G -jar utils.jar

加载到HashSet变得很快(第一个文件为50秒),但程序被系统Out-Of-Memory杀手杀死,因为它在将第二个文件加载到第二个HashSet或ArrayList时占用了大量的RAM

我的代码非常简单:

List<String> setL = Files.readAllLines(Paths.get("filename"));
HashSet<String> dataNew = new HashSet<>(setL);
程序得到的第二个文件

终止

[1408341.392872]内存不足:杀死进程20538(java)得分489或牺牲孩子 [1408341.392874]被杀的过程20531(java)total-vm:20177160kB,anon-rss:16074268kB,file-rss:0kB

UPDATE2:

感谢您的所有想法!

最终解决方案是:使用fastutil库(LongOpenHashSet)将行转换为Long +

RAM消耗量变为3.6Gb,处理时间仅为40秒!

有趣的观察。虽然以默认设置启动java,但是加载1亿个字符串到JDK的本机HashSet无穷无尽(我在1小时后中断),从-Xmx16G开始加速到1分钟。但是内存消耗很荒谬(大约20Gb),处理速度相当不错--2分钟。

如果某人不受RAM限制,原生JDK HashSet在速度方面并不是那么糟糕。

P.S。也许这个任务没有明确解释,但我没有看到任何完全加载至少一个文件的机会。所以,我怀疑内存消耗可以进一步降低。

8 个答案:

答案 0 :(得分:3)

首先,不要执行Files.readAllLines(Paths.get("filename"))然后将所有内容传递给Set,其中包含不必要的大量数据。尽量保持尽可能少的行。

逐行阅读文件并随时处理。这会立即减少你的内存使用量。

Set<String> oldData = new HashSet<>();
try (BufferedReader reader = Files.newBufferedReader(Paths.get("oldData"))) {
    for (String line = reader.readLine(); line != null; line = reader.readLine()) {
        // process your line, maybe add to the Set for the old data?
        oldData.add(line);
    }
}

Set<String> newData = new HashSet<>();
try (BufferedReader reader = Files.newBufferedReader(Paths.get("newData"))) {
    for (String line = reader.readLine(); line != null; line = reader.readLine()) {
        // Is it enough just to remove from old data so that you'll end up with only the difference between old and new?
        boolean oldRemoved = oldData.remove(line);
        if (!oldRemoved) {
            newData.add(line);
        }
    }
}

您最终会得到两个集合,分别只包含旧数据集或新数据集中的数据。

其次,如果可能的话,尝试预先设置容器。它们的大小(通常)在达到其容量时会翻倍,并且在处理大型集合时可能会产生大量开销。

此外,如果您的数据是数字,您只需使用long并保留该数据,而不是尝试保留String的实例?有很多集合库可以让你这样做,例如Koloboke,HPPC,HPPC-RT,GS Collections,fastutil,Trove。即使他们的Objects集合也可能很适合你,因为标准HashSet有很多不必要的对象分配。

答案 1 :(得分:2)

感谢您的所有想法!

最终解决方案是: 使用fastutil库(LongOpenHashSet)将行转换为Long +

RAM消耗量变为3.6Gb,处理时间仅 40秒

有趣的观察。虽然以默认设置启动java,但是加载1亿个字符串到JDK的本机HashSet无穷无尽(我在1小时后中断),从-Xmx16G开始加速到1分钟。但内存消耗是荒谬的(大约20Gb),处理速度相当不错 - 2分钟。

如果某人不受RAM限制,原生JDK HashSet在速度方面并不是那么糟糕。

P.S。也许这个任务没有明确解释,但我没有看到任何完全加载至少一个文件的机会。所以,我怀疑内存消耗可以进一步降低。

答案 2 :(得分:0)

我制作了一个非常简单的拼写检查程序,只检查字典中的单词是否对整个文档来说太慢了。我创建了一个地图结构,效果很好。

Map<String, List<String>> dictionary;

对于密钥,我使用单词的前2个字母。该列表包含以密钥开头的所有单词。为了加快速度,您可以对列表进行排序,然后使用二进制搜索来检查是否存在。我不确定密钥的最佳长度,如果密钥太长,您可以嵌套地图。最终它变成了一棵树。实际上,特里结构可能是最好的。

答案 3 :(得分:0)

请将字符串拆分为两个,并且重复的任何部分(str1或str2)最多使用ontern(),以便在Heap中再次保存复制相同的字符串。在这里我使用intern()来表示样本但不使用它,除非它们重复最多。

Set<MyObj> lineData = new HashSet<MyObj>();
String line = null;
BufferedReader bufferedReader = new BufferedReader(new FileReader(file.getAbsoluteFile()));
while((line = bufferedReader.readLine()) != null){
    String[] data = line.split(",");
    MyObj myObj = new MyObj();
    myObj.setStr1(data[0].intern());
    myObj.setStr1(data[1].intern());
    lineData.add(myObj);
}

public class MyObj {

    private String str1;
    private String str2;

    public String getStr1() {
        return str1;
    }

    public void setStr1(String str1) {
        this.str1 = str1;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((str1 == null) ? 0 : str1.hashCode());
        result = prime * result + ((str2 == null) ? 0 : str2.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Test1 other = (Test1) obj;
        if (str1 == null) {
            if (other.str1 != null)
                return false;
        } else if (!str1.equals(other.str1))
            return false;
        if (str2 == null) {
            if (other.str2 != null)
                return false;
        } else if (!str2.equals(other.str2))
            return false;
        return true;
    }

    public String getStr2() {
        return str2;
    }

    public void setStr2(String str2) {
        this.str2 = str2;
    }

}

答案 4 :(得分:0)

使用数据库;为了简单起见,使用Java嵌入式数据库(Derby,HSQL,H2,...)。有了这么多信息,您就可以从标准数据库缓存,节省时间的存储和查询中受益。你的伪代码是:

if first use,
   define new one-column table, setting column as primary-key
   iterate through input records, for each:
       insert record into table
otherwise
   open database with previous records
   iterate through input records, for each:
       lookup record in DB, update/report as required 

或者,如果您在他们的教程中使用现有的“table-diff”库(例如DiffKit),您可以做更少的工作:

java -jar ../diffkit-app.jar -demoDB
     

然后在您喜欢的内容中配置与此演示数据库的连接   JDBC启用数据库浏览器   [...]   您的数据库浏览器将显示表TEST10_LHS_TABLE和   TEST10_RHS_TABLE(以及其他)填充了来自的数据值   相应的CSV文件。

那就是:DiffKit基本上完成了我上面提到的,将文件加载到数据库表中(它们使用嵌入式H2),然后通过数据库查询比较这些表。

他们接受输入为CSV文件;但是,从文本输入到CSV的转换可以在不到10行代码中以流方式完成。然后你只需要调用他们的jar来做差异,你就可以在嵌入式数据库中将结果作为表格。

答案 5 :(得分:0)

您可以针对此类情况使用特里数据结构:http://www.toptal.com/java/the-trie-a-neglected-data-structure 算法如下:

  1. 逐行读取旧文件并将每行存储在trie中。
  2. 逐行读取新文件并测试每一行是否为 在特里:如果不是,那么这是一个新添加的行。
  3. 进一步的内存优化可以利用只有10位数,因此4位足以存储一个数字(而不是Java中每个字符2个字节)。您可能需要从以下链接之一调整trie数据结构:

    1. Trie data structures - Java
    2. http://algs4.cs.princeton.edu/52trie/TrieST.java.html

答案 6 :(得分:0)

包含11个字符(实际上最多12个字符)的String对象将具有64字节的大小(在具有压缩oops的64位Java上)。唯一可以容纳如此多元素并且大小合理的结构是一个数组:

100,000,000 * (64b per String object + 4b per reference) = 6,800,000,000b ~ 6.3Gb

因此,您可以立即忘记地图,集等,因为它们会引入太多的内存开销。但阵列实际上就是您所需要的。我的方法是:

  1. 加载&#34; old&#34;数据到数组中,对它进行排序(这应该足够快)
  2. 创建一个与加载数组大小相同的原始布尔值的备份数组(您也可以在这里使用BitSet)
  3. 从新数据文件中逐行读取。使用二进制搜索检查旧数据阵列中是否存在密码数据。如果该项存在,则将它在布尔数组/ bitset中的索引标记为true(从二进制搜索中获取索引)。如果该项目不存在,只需将其保存在某处(数组列表可以提供)。
  4. 当处理完所有行时,从旧数组中删除布尔数组/ bitset中false的所有项目(当然按索引检查)。最后将您保存的所有新数据添加到数组中。
  5. 可选择再次对数组进行排序并保存到磁盘,因此下次加载时可以跳过初始排序。
  6. 这应该足够快imo。初始排序是O(n log(n)),而二进制搜索是O(log(n)),因此你应该最终得到(不包括最终删除+添加,最多可以是2n):

    n log(n) (sort) + n log(n) (binary check for n elements) = 2 n log(n)
    

    如果你能解释一下你所拥有的String的结构(如果有某种模式或不存在),那么可能会有其他优化。

答案 7 :(得分:-1)

发生ArrayList时大量调整readAllLines()的主要问题。更好的选择是LinkedList来插入数据

try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
        List<String> result = new LinkedList<>();
        for (;;) {
            String line = reader.readLine();
            if (line == null)
                break;
            result.add(line);
        }
        return result;
    }