2014-12-04 63 views
0

當從另一個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中,是線程安全的,當文件完全獲得時,讀者線程發送消息給另一個線程,告訴他們停止。最後,我必須將此緩衝區保存到文件中。你怎麼看?這是一個好主意?

+1

看到'newTopology.getElement()。getEntity()'和所有,你可能會使用臨時變量。並描述應用程序; NetBeans IDE可以開箱即用,但通常這應該值得研究。記錄中的一些toString調用可能代價很高。首先寫入文件可能會更好。至少根據文件大小提供初始容量,'new StringBuilder(100000);'。 – 2014-12-04 13:46:10

+0

嗨Joop,謝謝你的迴應,newTopology.getElement()。getEntity()是一個屬性,從一個XML文件解析,它只計算一次。我將按照您對StringBuilder初始大小的建議。 – aironman 2014-12-04 14:06:27

+1

無論何時需要擴展StringBuilder,Java都會創建一個新的char緩衝區,並將當前數據複製到新數組中,然後釋放舊數組。隨着StringBuilder變得越來越大,這個過程需要越來越多的時間。你爲什麼不直接把結果寫回文件? – markbernard 2014-12-04 14:15:15

回答

1

我想關於生產者的消費者多線程戰略,一個讀線程讀取每一行,並把它們放入一個循環緩衝區,另一個線程把它由一個過程他們一個和預先行添加到在線程安全的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在運行程序時做什麼?

0

我在我的項目中使用了superCSV庫來處理大量的行。它比手動讀取線條相對快。 Reference