我使用包含短字符串的文本文件(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。也许这个任务没有明确解释,但我没有看到任何完全加载至少一个文件的机会。所以,我怀疑内存消耗可以进一步降低。
答案 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 算法如下:
进一步的内存优化可以利用只有10位数,因此4位足以存储一个数字(而不是Java中每个字符2个字节)。您可能需要从以下链接之一调整trie数据结构:
答案 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
因此,您可以立即忘记地图,集等,因为它们会引入太多的内存开销。但阵列实际上就是您所需要的。我的方法是:
false
的所有项目(当然按索引检查)。最后将您保存的所有新数据添加到数组中。 这应该足够快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;
}