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



我处理的文本文件中有短字符串(10位数字)。文件大小约为1.5Gb,因此行数达到1亿。

每天我都得到另一个文件,需要提取新的元素(每天数以万计)。

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

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

我使用以下代码:

dataNew.removeAll(dataOld);

尝试在HashSets中加载数据- HashSets的创建是无止境的。LinkedHashset也是一样。

尝试加载到数组列表中并只对其中一个进行排序

Collections.sort(dataNew);

但它并没有加速

dataNew.removeAll(dataOld);

内存消耗也相当高- sort()只完成了15Gb的堆(13Gb是不够的)。

我试过使用旧的linux utidiff,它在76分钟内完成了任务(同时消耗了8Gb内存)。

所以,我的目标是在1小时的处理时间(或者更短)内用Java解决这个问题,并且消耗15Gb(或者更好的是8-10Gb)。

有什么建议吗?也许我不需要对数组列表按字母顺序排序,但还有别的吗?

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

数据未排序,每一行都是唯一的。所以我必须将100万个元素与100万个元素进行比较。数据线如2404,107263。不能转换为整数

有趣的是,当我将最大堆大小增加到16Gb时
java -Xms5G -Xmx16G -jar utils.jar

加载到HashSet变得很快(第一个文件50秒),但是程序被系统内存不足杀手杀死,因为它在加载第二个文件到第二个HashSet或ArrayList时消耗了大量的RAM

我的代码很简单:

List<String> setL = Files.readAllLines(Paths.get("filename"));
HashSet<String> dataNew = new HashSet<>(setL);

在第二个文件上程序得到

死亡

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

更新2:

谢谢你的建议!

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

内存消耗变成3.6Gb,处理时间只有40秒!

有趣的观察。虽然使用默认设置启动java会没完没了地将1亿个字符串加载到JDK的本机HashSet(我在1小时后中断),但使用-Xmx16G将该过程加快到1分钟。但是内存消耗是荒谬的(大约20Gb),处理速度相当好- 2分钟。

如果不受RAM的限制,本地JDK HashSet在速度方面并不差。

注。也许任务没有清楚地解释,但我不认为有任何机会不完全加载至少一个文件。因此,我怀疑内存消耗能否进一步降低很多。

首先,不要执行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有很多不必要的对象分配。

谢谢你的建议!

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

内存消耗变成3.6Gb,处理时间只有40秒!

有趣的观察。虽然使用默认设置启动java会没完没了地将1亿个字符串加载到JDK的本机HashSet(我在1小时后中断),但使用-Xmx16G将该过程加快到1分钟。但是内存消耗是荒谬的(大约20Gb),处理速度相当不错——2分钟。

如果不受RAM的限制,本地JDK HashSet在速度方面并不差。

注。也许任务没有清楚地解释,但我不认为有任何机会不完全加载至少一个文件。因此,我怀疑内存消耗能否进一步降低很多。

请将字符串分成两部分,重复的部分(str1或str2)大多数使用intern(),以便在堆中再次保存相同字符串的重复。这里我在两个部分都使用了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;
    }
}

使用数据库;为了简单起见,可以使用java嵌入式数据库(Derby、HSQL、H2等)。有了这些信息,您就可以真正受益于标准的DB缓存、省时的存储和查询。你的伪代码应该是:

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的数据库浏览器[…]DB浏览器将显示TEST10_LHS_TABLE和TEST10_RHS_TABLE(以及其他)中填充了来自的数据值

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

他们接受输入为CSV文件;但是从文本输入到他们的CSV的转换可以用不到10行代码以流方式完成。然后你只需要调用他们的jar来做一些不同的事情,你就可以在他们的嵌入式DB中以表的形式获得结果。

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

Map<String, List<String>> dictionary;

对于键,我使用单词的前两个字母。这个列表包含了所有以关键字开头的单词。为了加快速度,您可以对列表进行排序,然后使用二分搜索来检查是否存在。我不确定键的最佳长度,如果键太长,可以嵌套映射。最后它变成了一棵树。实际上,三阶结构可能是最好的。

对于这种情况,可以使用trie数据结构:http://www.toptal.com/java/the-trie-a-neglected-data-structure算法如下:

  1. 逐行读取旧文件,并将每行存储在tree中。
  2. 逐行读取新文件,并测试每一行是否正确

进一步的内存优化可以利用只有10位数字的优势,因此4位足以存储一个数字(而不是Java中的每个字符2字节)。您可能需要从以下链接之一调整trie数据结构:

  1. Trie数据结构- Java
  2. http://algs4.cs.princeton.edu/52trie/TrieST.java.html

包含11个字符(实际上最多12个)的String对象将具有64字节的大小(在64位Java压缩ops上)。唯一能够容纳这么多元素且具有合理大小的结构体是数组:

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

所以你可以立即忘记map, set等,因为它们引入了太多的内存开销。但你只需要一个数组。我的方法是:

  1. 加载"旧"数据到一个数组,排序它(这应该足够快)
  2. 创建一个与加载数组大小相同的原始布尔值的备份数组(您也可以在这里使用BitSet)
  3. 从新的数据文件逐行读取。使用二进制搜索检查密码数据是否存在于旧数据数组中。如果条目存在,则将其在布尔数组/bitset中的索引标记为true(您从二进制搜索中获得索引)。如果项目不存在,只需将其保存在某个地方(数组列表可以服务)。
  4. 当所有行被处理时,从旧数组中删除所有在布尔数组/bitset中具有false的项(当然通过索引检查)。最后,将保存在某处的所有新数据添加到数组中。
  5. 可以选择对数组再次排序并保存到磁盘,所以下次加载它时,你可以跳过初始排序。

这应该足够快了。初始排序是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的结构(如果存在某种模式或不存在模式),则可能会有其他优化。

readAllLines()发生时,大量调整ArrayList大小的主要问题。更好的选择是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;
    }

最新更新