在Java中创建大型csv文件变得非常慢
当从另一个csv文件开始创建csv文件时,我遇到了性能问题。 这是原始文件的外观:在Java中创建大型csv文件变得非常慢
country,state,co,olt,olu,splitter,ont,cpe,cpe.latitude,cpe.longitude,cpe.customer_class,cpe.phone,cpe.ip,cpe.subscriber_id
COUNTRY-0001,STATE-0001,CO-0001,OLT-0001,OLU0001,SPLITTER-0001,ONT-0001,CPE-0001,28.21487,77.451775,ALL,SIP:[email protected],SIP:[email protected],CPE_SUBSCRIBER_ID-QHLHW4
COUNTRY-0001,STATE-0002,CO-0002,OLT-0002,OLU0002,SPLITTER-0002,ONT-0002,CPE-0002,28.294018,77.068924,ALL,SIP:[email protected],SIP:[email protected],CPE_SUBSCRIBER_ID-AH8NJQ
潜在它可能是数百万这样的线,我已经检测到的问题1.280.000线。
这是算法:
File csvInputFile = new File(csv_path);
int blockSize = 409600;
brCsvInputFile = new BufferedReader(frCsvInputFile, blockSize);
String line = null;
StringBuilder sbIntermediate = new StringBuilder();
skipFirstLine(brCsvInputFile);
while ((line = brCsvInputFile.readLine()) != null) {
createIntermediateStringBuffer(sbIntermediate, line.split(REGEX_COMMA));
}
private static void skipFirstLine(BufferedReader br) throws IOException {
String line = br.readLine();
String[] splitLine = line.split(REGEX_COMMA);
LOGGER.debug("First line detected! ");
createIndex(splitLine);
createIntermediateIndex(splitLine);
}
private static void createIndex(String[] splitLine) {
LOGGER.debug("START method createIndex.");
for (int i = 0; i < splitLine.length; i++)
headerIndex.put(splitLine[i], i);
printMap(headerIndex);
LOGGER.debug("COMPLETED method createIndex.");
}
private static void createIntermediateIndex(String[] splitLine) {
LOGGER.debug("START method createIntermediateIndex.");
com.tekcomms.c2d.xml.model.v2.Metadata_element[] metadata_element = null;
String[] servicePath = newTopology.getElement().getEntity().getService_path().getLevel();
if (newTopology.getElement().getMetadata() != null)
metadata_element = newTopology.getElement().getMetadata().getMetadata_element();
LOGGER.debug(servicePath.toString());
LOGGER.debug(metadata_element.toString());
headerIntermediateIndex.clear();
int indexIntermediateId = 0;
for (int i = 0; i < servicePath.length; i++) {
String level = servicePath[i];
LOGGER.debug("level is: " + level);
headerIntermediateIndex.put(level, indexIntermediateId);
indexIntermediateId++;
// its identificator is going to be located to the next one
headerIntermediateIndex.put(level + "ID", indexIntermediateId);
indexIntermediateId++;
}
// adding cpe.latitude,cpe.longitude,cpe.customer_class, it could be
// better if it would be metadata as well.
String labelLatitude = newTopology.getElement().getEntity().getLatitude();
// indexIntermediateId++;
headerIntermediateIndex.put(labelLatitude, indexIntermediateId);
String labelLongitude = newTopology.getElement().getEntity().getLongitude();
indexIntermediateId++;
headerIntermediateIndex.put(labelLongitude, indexIntermediateId);
String labelCustomerClass = newTopology.getElement().getCustomer_class();
indexIntermediateId++;
headerIntermediateIndex.put(labelCustomerClass, indexIntermediateId);
// adding metadata
// cpe.phone,cpe.ip,cpe.subscriber_id,cpe.vendor,cpe.model,cpe.customer_status,cpe.contact_telephone,cpe.address,
// cpe.city,cpe.state,cpe.zip,cpe.bootfile,cpe.software_version,cpe.hardware_version
// now i need to iterate over each Metadata_element belonging to
// topology.element.metadata
// are there any metadata?
if (metadata_element != null && metadata_element.length != 0)
for (int j = 0; j < metadata_element.length; j++) {
String label = metadata_element[j].getLabel();
label = label.toLowerCase();
LOGGER.debug(" ==label: " + label + " index_pos: " + j);
indexIntermediateId++;
headerIntermediateIndex.put(label, indexIntermediateId);
}
printMap(headerIntermediateIndex);
LOGGER.debug("COMPLETED method createIntermediateIndex.");
}
读取整个数据集,1.280.000线取800毫秒!所以这个问题是这种方法
private static void createIntermediateStringBuffer(StringBuilder sbIntermediate, String[] splitLine) throws ClassCastException,
NullPointerException {
LOGGER.debug("START method createIntermediateStringBuffer.");
long start, end;
start = System.currentTimeMillis();
ArrayList<String> hashes = new ArrayList<String>();
com.tekcomms.c2d.xml.model.v2.Metadata_element[] metadata_element = null;
String[] servicePath = newTopology.getElement().getEntity().getService_path().getLevel();
LOGGER.debug(servicePath.toString());
if (newTopology.getElement().getMetadata() != null) {
metadata_element = newTopology.getElement().getMetadata().getMetadata_element();
LOGGER.debug(metadata_element.toString());
}
for (int i = 0; i < servicePath.length; i++) {
String level = servicePath[i];
LOGGER.debug("level is: " + level);
if (splitLine.length > getPositionFromIndex(level)) {
String name = splitLine[getPositionFromIndex(level)];
sbIntermediate.append(name);
hashes.add(name);
sbIntermediate.append(REGEX_COMMA).append(HashUtils.calculateHash(hashes)).append(REGEX_COMMA);
LOGGER.debug(" ==sbIntermediate: " + sbIntermediate.toString());
}
}
// end=System.currentTimeMillis();
// LOGGER.info("COMPLETED adding name hash. " + (end - start) + " ms. " + (end - start)/1000 + " seg.");
// adding cpe.latitude,cpe.longitude,cpe.customer_class, it should be
// better if it would be metadata as well.
String labelLatitude = newTopology.getElement().getEntity().getLatitude();
if (splitLine.length > getPositionFromIndex(labelLatitude)) {
String lat = splitLine[getPositionFromIndex(labelLatitude)];
sbIntermediate.append(lat).append(REGEX_COMMA);
}
String labelLongitude = newTopology.getElement().getEntity().getLongitude();
if (splitLine.length > getPositionFromIndex(labelLongitude)) {
String lon = splitLine[getPositionFromIndex(labelLongitude)];
sbIntermediate.append(lon).append(REGEX_COMMA);
}
String labelCustomerClass = newTopology.getElement().getCustomer_class();
if (splitLine.length > getPositionFromIndex(labelCustomerClass)) {
String customerClass = splitLine[getPositionFromIndex(labelCustomerClass)];
sbIntermediate.append(customerClass).append(REGEX_COMMA);
}
// end=System.currentTimeMillis();
// LOGGER.info("COMPLETED adding lat,lon,customer. " + (end - start) + " ms. " + (end - start)/1000 + " seg.");
// watch out metadata are optional, it can appear as a void chain!
if (metadata_element != null && metadata_element.length != 0)
for (int j = 0; j < metadata_element.length; j++) {
String label = metadata_element[j].getLabel();
LOGGER.debug(" ==label: " + label + " index_pos: " + j);
if (splitLine.length > getPositionFromIndex(label)) {
String actualValue = splitLine[getPositionFromIndex(label)];
if (!"".equals(actualValue))
sbIntermediate.append(actualValue).append(REGEX_COMMA);
else
sbIntermediate.append("").append(REGEX_COMMA);
} else
sbIntermediate.append("").append(REGEX_COMMA);
LOGGER.debug(" ==sbIntermediate: " + sbIntermediate.toString());
}//for
sbIntermediate.append("\n");
end = System.currentTimeMillis();
LOGGER.info("COMPLETED method createIntermediateStringBuffer. " + (end - start) + " ms. ");
}
正如你所看到的,这种方法增加了一个预先计算的行到的StringBuffer,读取输入CSV文件中的每一行,从该行计算新的数据,最后生成的行添加到StringBuffer,所以最后我可以用这个缓冲区创建文件。
我已经运行jconsole,我可以看到没有内存泄漏,我可以看到表示创建对象的锯齿和gc回忆garbaje。它永远不会拖垮内存堆栈阈值。
我注意到的一件事是,向StringBuffer添加新行所需的时间在几ms范围内完成(5,6,10),但随着时间增加到(100-200 )毫秒和我怀疑在不久的将来,所以可能这是战斗马。
我试图对代码进行分析,我知道有3圈,但他们都非常短裤,在只有8个元素的第一个循环迭代:
for (int i = 0; i < servicePath.length; i++) {
String level = servicePath[i];
LOGGER.debug("level is: " + level);
if (splitLine.length > getPositionFromIndex(level)) {
String name = splitLine[getPositionFromIndex(level)];
sbIntermediate.append(name);
hashes.add(name);
sbIntermediate.append(REGEX_COMMA).append(HashUtils.calculateHash(hashes)).append(REGEX_COMMA);
LOGGER.debug(" ==sbIntermediate: " + sbIntermediate.toString());
}
}
我已经meassured需要时间从分割线中获得名称,它毫无价值,0毫秒,与计算哈希方法相同,为0毫秒。
其他循环实际上是相同的,迭代0到n,其中n是一个非常小的int,例如3到10,所以我不明白为什么它需要更多时间来完成该方法,唯一的我发现的事情是,添加一个新行到缓冲区正在变慢进程。
我正在考虑一个生产者消费者多线程策略,读取每一行并将它们放入循环缓冲区的读取器线程,另一个线程逐个处理它们,处理它们并将预先计算的行添加到StringBuffer中,是线程安全的,当文件完全获得时,读者线程发送消息给另一个线程,告诉他们停止。最后,我必须将此缓冲区保存到文件中。你怎么看?这是一个好主意?
我想关于生产者的消费者多线程战略,一个读线程读取每一行,并把它们放入一个循环缓冲区,另一个线程把它由一个过程他们一个和预先行添加到在线程安全的StringBuffer中,当文件被完全获得时,阅读器线程向另一个线程发送消息,告诉他们停止。最后,我必须将此缓冲区保存到文件中。你怎么看?这是一个好主意?
也许,但这是相当多的工作,我会尝试一些更简单的第一。
line.split(REGEX_COMMA)
你REGEX_COMMA
是被编译成一个正则表达式一百万次的字符串。这是微不足道的,但我会尝试使用Pattern
来代替。
你正在生产大量垃圾与你的分裂。也许你应该通过手动将输入分成重用的ArrayList<String>
(这只是几行)而避免它。
如果您只需要将结果写入文件,最好避免构建一个巨大的字符串。也许List<String>
甚至List<StringBuilder>
会更好,也许可以直接写入缓冲流。
您似乎只使用ASCII。您的编码与平台有关,这可能意味着您使用的UTF-8可能很慢。切换到更简单的编码可能会有所帮助。
使用byte[]
而不是String
很可能有帮助。字节是字符的一半大小,读取文件时不需要转换。你所做的所有操作都可以通过字节来完成。
我注意到的一件事是,向StringBuffer添加新行所需的时间在几ms范围内完成(5,6,10),但随着时间增加到(100 -200)毫秒,我怀疑在不久的将来会更多,所以这可能是战马。
正在调整大小,可以通过使用建议的ArrayList<String>
来加快大小,因为要复制的数据量要低得多。当缓冲区变大时写出数据也可以。
我已经测量了从splitline获取名称所需的时间,它毫无价值,0毫秒,与计算哈希方法相同,0毫秒。
从来没有使用currentTimeMillis
作为nanoTime
是严格更好。使用分析器。分析器的问题在于它改变了它应该测量的内容。作为一个穷人的分析器,您可以计算可疑方法内所有时间花费的总和,并将其与总时间进行比较。
什么是CPU负载和GC在运行程序时做什么?
看到'newTopology.getElement()。getEntity()'和所有,你可能会使用临时变量。并描述应用程序; NetBeans IDE可以开箱即用,但通常这应该值得研究。记录中的一些toString调用可能代价很高。首先写入文件可能会更好。至少根据文件大小提供初始容量,'new StringBuilder(100000);'。 – 2014-12-04 13:46:10
嗨Joop,谢谢你的回应,newTopology.getElement()。getEntity()是一个属性,从一个XML文件解析,它只计算一次。我将按照您对StringBuilder初始大小的建议。 – aironman 2014-12-04 14:06:27
无论何时需要扩展StringBuilder,Java都会创建一个新的char缓冲区,并将当前数据复制到新数组中,然后释放旧数组。随着StringBuilder变得越来越大,这个过程需要越来越多的时间。你为什么不直接把结果写回文件? – markbernard 2014-12-04 14:15:15