我处理的文本文件中有短字符串(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算法如下:
- 逐行读取旧文件,并将每行存储在tree中。
- 逐行读取新文件,并测试每一行是否正确
进一步的内存优化可以利用只有10位数字的优势,因此4位足以存储一个数字(而不是Java中的每个字符2字节)。您可能需要从以下链接之一调整trie数据结构:
- Trie数据结构- Java
- 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等,因为它们引入了太多的内存开销。但你只需要一个数组。我的方法是:
- 加载"旧"数据到一个数组,排序它(这应该足够快)
- 创建一个与加载数组大小相同的原始布尔值的备份数组(您也可以在这里使用BitSet)
- 从新的数据文件逐行读取。使用二进制搜索检查密码数据是否存在于旧数据数组中。如果条目存在,则将其在布尔数组/bitset中的索引标记为true(您从二进制搜索中获得索引)。如果项目不存在,只需将其保存在某个地方(数组列表可以服务)。
- 当所有行被处理时,从旧数组中删除所有在布尔数组/bitset中具有
false
的项(当然通过索引检查)。最后,将保存在某处的所有新数据添加到数组中。 - 可以选择对数组再次排序并保存到磁盘,所以下次加载它时,你可以跳过初始排序。
这应该足够快了。初始排序是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;
}