高性能Java解析器实现过程详解
如果你沒有指定數(shù)據(jù)或語言標(biāo)準(zhǔn)的或開源的Java解析器, 可能經(jīng)常要用Java實現(xiàn)你自己的數(shù)據(jù)或語言解析器。或者,可能有很多解析器可選,但是要么太慢,要么太耗內(nèi)存,或者沒有你需要的特定功能。或者開源解析器存在缺陷,或者開源解析器項目被取消諸如此類原因。上述原因都沒有你將需要實現(xiàn)你自己的解析器的事實重要。
當(dāng)你必需實現(xiàn)自己的解析器時,你會希望它有良好表現(xiàn),靈活,功能豐富,易于使用,最后但更重要是易于實現(xiàn),畢竟你的名字會出現(xiàn)在代碼中。本文中,我將介紹一種用Java實現(xiàn)高性能解析器的方式。該方法不具排他性,它是簡約的,并實現(xiàn)了高性能和合理的模塊化設(shè)計。該設(shè)計靈感來源于VTD-XML ,我所見到的最快的java XML解析器,比StAX和SAX Java標(biāo)準(zhǔn)XML解析器更快。
兩個基本解析器類型
解析器有多種分類方式。在這里,我只比較兩個基本解析器類型的區(qū)別:
- 順序訪問解析器(Sequential access parser)
- 隨機訪問解析器(Random access parser)
順序訪問意思是解析器解析數(shù)據(jù),解析完畢后將解析數(shù)據(jù)移交給數(shù)據(jù)處理器。數(shù)據(jù)處理器只訪問當(dāng)前已解析過的數(shù)據(jù);它不能回頭處理先前的數(shù)據(jù)和處理前面的數(shù)據(jù)。順序訪問解析器已經(jīng)很常見,甚至作為基準(zhǔn)解析器,SAX和StAX解析器就是最知名的例子。
隨機訪問解析器是可以在已解析的數(shù)據(jù)上或讓數(shù)據(jù)處理代碼向前和向后(隨機訪問)。隨機訪問解析器例子見XML DOM解析器。
順序訪問解析器只能讓你在文檔流中訪問剛解析過的“窗口”或“事件”,而隨機訪問解析器允許你按照想要的方式訪問遍歷。
設(shè)計概要
我這里介紹的解析器設(shè)計屬于隨機訪問變種。
隨機訪問解析器實現(xiàn)總是比順序訪問解析器慢一些,這是因為它們一般建立在某種已解析數(shù)據(jù)對象樹上,數(shù)據(jù)處理器能訪問上述數(shù)據(jù)。創(chuàng)建對象樹實際上在CPU時鐘上是慢的,并且耗費大量內(nèi)存。
代替在解析數(shù)據(jù)上構(gòu)建對象樹,更高性能的方式是建立指向原始數(shù)據(jù)緩存的索引緩存。索引指向已解析數(shù)據(jù)的元素起始點和終點。代替通過對象樹訪問數(shù)據(jù),數(shù)據(jù)處理代碼直接在含有原始數(shù)據(jù)的緩存中訪問已解析數(shù)據(jù)。如下是兩種方法的示意圖:
因為沒找到更好的名字,我就叫該解析器為“索引疊加解析器”。該解析器在原始數(shù)據(jù)上新建了一個索引疊加層。這個讓人想起數(shù)據(jù)庫構(gòu)建存儲在硬盤上的數(shù)據(jù)索引的方式。它在原始未處理的數(shù)據(jù)上創(chuàng)建了指針,讓瀏覽和搜索數(shù)據(jù)更快。
如前所說,該設(shè)計受VTD-XML的啟發(fā), VTD是虛擬令牌描述符(Virtual Token Descriptor)的英文縮寫。因此,你可以叫它虛擬令牌描述符解析器。不過,我更喜歡索引疊加的命名,因為這是虛擬令牌描述符代表,在原始數(shù)據(jù)上的索引。
常規(guī)解析器設(shè)計
一般解析器設(shè)計會將解析過程分為兩步。第一步將數(shù)據(jù)分解為內(nèi)聚的令牌,令牌是一個或多個已解析數(shù)據(jù)的字節(jié)或字符。第二步解釋這些令牌并基于這些令牌構(gòu)建更大的元素。兩步示意圖如下:
圖中元素并不是指XML元素(盡管XML元素也解析元素),而更大“數(shù)據(jù)元素”構(gòu)造了已解析數(shù)據(jù)。在我XML文檔中表示XML元素,而在JSON 文檔中則表示JSON對象,諸如此類。
舉例說明,字符串將被分解為如下令牌:
< myelement >一旦數(shù)據(jù)分解為多個令牌,解析器更容易理解它們和判斷這些令牌構(gòu)造的大元素。解析器將會識別XML元素以 ‘<’令牌開頭后面是字符串令牌(元素名稱),然后是一系列可選的屬性,最后是‘>’令牌。
索引疊加解析器設(shè)計
兩步方法也將用于我們的解析器設(shè)計。輸入數(shù)據(jù)首先由分析器組件分解為多個令牌。 然后解析器解析這些令牌識別輸入數(shù)據(jù)的大元素邊界。
你也可以增加可選的第三步驟—“元素導(dǎo)航步驟”到解析過程中。 若解析器從已解析數(shù)據(jù)中構(gòu)造對象樹,那么對象樹一般會包含對象樹導(dǎo)航的鏈接。當(dāng)我們構(gòu)建元素索引緩存代替對象樹時,我們需要一個獨立組件幫助數(shù)據(jù)處理代碼導(dǎo)航元素索引緩存。
我們解析器設(shè)計概覽參見如下示意圖:
我們首先將所有數(shù)據(jù)讀到數(shù)據(jù)緩存內(nèi)。為了保證可以通過解析中創(chuàng)建的索引隨機訪問原始數(shù)據(jù),所有原始數(shù)據(jù)必需放到內(nèi)存中。
接著,分析器將數(shù)據(jù)分解為多個令牌。開始索引,結(jié)束索引和令牌類型都會保存于分析器中一個內(nèi)部令牌緩存。使用令牌緩存使其向前和向后訪問成為可能,上述情況下解析器需要令牌緩存。
第三步,解析器查找從分析器獲取的令牌,在上下文中校驗它們,并判斷它們表示的元素。然后,解析器基于分析器獲取的令牌構(gòu)造元素索引(索引疊加)。解析器逐一獲得來自分析器的令牌。因此,分析器實際上不需要馬上將所有數(shù)據(jù)分解成令牌。而僅僅是在特定時間點找到一個令牌。
數(shù)據(jù)處理代碼能訪問元素緩存,并用它訪問原始數(shù)據(jù)。或者,你可能會將數(shù)據(jù)緩存封裝到元素訪問組件中,讓訪問元素緩存更容易。
該設(shè)計基于已解析數(shù)據(jù)構(gòu)建對象樹,但它需建立訪問結(jié)構(gòu)—元素緩存,由索引(整型數(shù)組)指向含有原始數(shù)據(jù)的數(shù)據(jù)緩存。我們能使用這些索引訪問存于原始數(shù)據(jù)緩存的數(shù)據(jù)。
下面小節(jié)將從設(shè)計的不同方面更詳細(xì)地進(jìn)行介紹。
數(shù)據(jù)緩存
數(shù)據(jù)緩存是含有原始數(shù)據(jù)的一種字節(jié)或字符緩存。令牌緩存和元素緩存持有數(shù)據(jù)緩存的索引。
為了隨機訪問解析過了的數(shù)據(jù),內(nèi)存表示上述信息的機制是必要的。我們不使用對象樹而是用包含原始數(shù)據(jù)的數(shù)據(jù)緩存。
將所有數(shù)據(jù)放在內(nèi)存中需消耗大塊的內(nèi)存。若數(shù)據(jù)含有的元素是相互獨立的,如日志記錄,將整個日志文件放在內(nèi)存中將是矯枉過正了。相反,你可以拉大塊的日志文件,該文件存有完整的日志記錄。因為每個日志記錄可完全解析,并且獨立于其它日志記錄的處理,所以我們不需要在同一時間將整個日志文件放到內(nèi)存中。
標(biāo)記分析器和標(biāo)記緩存
分析器將數(shù)據(jù)緩分解為多個令牌。令牌信息存儲在令牌緩存中,包含如下內(nèi)容:
- 令牌定位(起始索引)
- 令牌長度
- 令牌類型 (可選)
上述信息放在數(shù)組中。如下實例說明處理邏輯:
public class IndexBuffer {public int[] position = null;public int[] length = null;public byte[] type = null; /* assuming a max of 256 types (1 byte / type) */ }當(dāng)分析器找到數(shù)據(jù)緩存中令牌時,它將構(gòu)建位置數(shù)組的起始索引位置,長度數(shù)組的令牌長度和類型數(shù)組的令牌類型。
若不使用可選的令牌類型數(shù)組,你仍能通過查看令牌數(shù)據(jù)來區(qū)分令牌類型。這是性能和內(nèi)存消耗的權(quán)衡。
解析器
解析器是在性質(zhì)上與分析器類似,只不過它采用令牌作為輸入和輸出的元素索引。如同使用令牌,一個元素由它的位置(起始索引),長度,以及可選的元素類型來決定。這些數(shù)字存儲在與存儲令牌相同的結(jié)構(gòu)中。
再者,類型數(shù)組是可選的。若你很容易基于元素的第一個字節(jié)或字符確定元素類型,你不必存儲元素類型。
元素緩存中標(biāo)記的要素精確粒度取決于數(shù)據(jù)被解析,以及需要后面數(shù)據(jù)處理的代碼。例如,如果你實現(xiàn)一個XML解析器,你可能會標(biāo)記為每個“解析器元素”的開始標(biāo)簽, 屬性和結(jié)束標(biāo)簽。
元素緩存(索引)
解析器生成帶有指向元數(shù)據(jù)的索引的元素緩存。該索引標(biāo)記解析器從數(shù)據(jù)中獲取的元素的位置(起始索引),長度和類型。你可以使用這些索引來訪問原始數(shù)據(jù)。
看一看上文的IndexBuffer代碼,你就知道元素緩存每個元素使用9字節(jié);四個字節(jié)標(biāo)記位置,四個自己是令牌長度,一個字節(jié)是令牌類型。
你可以減少IndexBuffer 的內(nèi)存消耗。例如,如果你知道元素從不會超過65,536字節(jié),那么你可以用短整型數(shù)組代替整型來存令牌長度。這將每個元素節(jié)省兩個字節(jié),使內(nèi)存消耗降低為每個元素7個字節(jié)。
此外,如果知道將解析這些文件長度從不會超過16,777,216字節(jié),你只需要三個字節(jié)標(biāo)識位置(起始索引)。在位置數(shù)組中,每一整型第四字節(jié)可以保存元素類型,省去了一個類型數(shù)組。如果您有少于128的令牌類型,您可以使用7位的令牌類型而不是八個。這使您可以花25位在位置上,這增加了位置范圍最大到33,554,432。如果您令牌類型少于64,您可以安排另一個位給位置,諸如此類。
VTD-XML實際上會將所有這些信息壓縮成一個Long型,以節(jié)省空間。處理速度會有損失,因為額外的位操作收拾單獨字段到單個整型或long型中,不過你可以節(jié)省一些內(nèi)存。總而言之,這是一個權(quán)衡。
元素導(dǎo)航組件
元素導(dǎo)航組件幫助正在處理數(shù)據(jù)的代碼訪問元素緩存。務(wù)必記住,一個語義對象或元素(如XML元素)可能包括多個解析器元素。為了方便訪問,您可以創(chuàng)建一個元素導(dǎo)航器對象,可以在語義對象級別訪問解析器元素。例如,一個XML元素導(dǎo)航器組件可以通過在起始標(biāo)記和到起始標(biāo)記來訪問元素緩存。
使用元素導(dǎo)航組件是你的自由。如果要實現(xiàn)一個解析器在單個項目中的使用,你可以要跳過它。但是,如果你正在跨項目中重用它,或作為開源項目發(fā)布它,你可能需要添加一個元素導(dǎo)航組件,這取決于如何訪問已解析數(shù)據(jù)的復(fù)雜度。
案例學(xué)習(xí):一個JSON解析器
為了讓索引疊加解析器設(shè)計更清晰,我基于索引疊加解析器設(shè)計用Java實現(xiàn)了一個小的JSON解析器。你可以在GitHub上找到完整的代碼。
JSON是JavaScript Object Notation的簡寫。JSON是一種流行的數(shù)據(jù)格式,基于AJAX來交換Web服務(wù)器和瀏覽器之間的數(shù)據(jù),Web瀏覽器已經(jīng)內(nèi)置了JSON解析為JavaScript對象的原生支持。后文,我將假定您熟悉JSON。
如下是一個JSON簡單示例:
{ "key1" : "value1" , "key2" : "value2" , [ "valueA" , "valueB" , "valueC" ] }JSON分析器將JSON字符串分解為如下令牌:
這里下劃線用于強調(diào)每個令牌的長度。
分析器也能判斷每個令牌的基本類型。如下是同一個JSON示例,只是增加了令牌類型:
注意令牌類型不是語義化的。它們只是說明基本令牌類型,而不是它們代表什么。
解析器解釋基本令牌類型,并使用語義化類型來替換它們。如下示例是同一個JSON示例,只是由語義化類型(解析器元素)代替:
一旦解析器完成了上述JSON解析,你將有一個索引,包含上面打標(biāo)記元素的位置,長度和元素類型。你可以訪問索引從JSON抽取你需要的數(shù)據(jù)。
在GitHub庫中的實現(xiàn)包含兩個JSON解析器。其中一個分割解析過程為JsonTokenizer和JsonParser(如本文前面所述),以及一個為JsonParser2結(jié)合分析和解析過程為一個階段,一個類。JsonParser2速度更快,但更難理解。因此,我會在下面的章節(jié)快速介紹一下在JsonTokenizer和JsonParser類,但會跳過JsonParser2。
(本文第一個版本有讀者指出,從該指數(shù)疊加分析器的輸出是不是難于從原始數(shù)據(jù)緩沖區(qū)中提取數(shù)據(jù)。正如前面提到的,這就是添加一個元素導(dǎo)航組件的原因。為了說明這樣的元素導(dǎo)航組件的原理,我已經(jīng)添加了JsonNavigator類。稍后,我們也將快速瀏覽一下這個類。)
JsonTokenizer.parseToken()方法
為了介紹分析和解析過程實現(xiàn)原理,我們看一下JsonTokenizer 和JsonParser 類的核心代碼部分。提醒,完整代碼可以在GitHub 訪問 。
如下是JsonTokenizer.parseToken()方法,解析數(shù)據(jù)緩存的下一個索引:
public void parseToken() {skipWhiteSpace();this.tokenBuffer.position[this.tokenIndex] = this.dataPosition;switch (this.dataBuffer.data[this.dataPosition]) {case '{': {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_CURLY_BRACKET_LEFT;}break;case '}': {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_CURLY_BRACKET_RIGHT;}break;case '[': {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_SQUARE_BRACKET_LEFT;}break;case ']': {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_SQUARE_BRACKET_RIGHT;}break;case ',': {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_COMMA;}break;case ':': {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_COLON;}break;case '"': { parseStringToken(); } break;case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9': {parseNumberToken();this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_NUMBER_TOKEN;}break;case 'f': {if (parseFalse()) {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_BOOLEAN_TOKEN;}}break;case 't': {if (parseTrue()) {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_BOOLEAN_TOKEN;}}break;case 'n': {if (parseNull()) {this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_NULL_TOKEN;}}break;}this.tokenBuffer.length[this.tokenIndex] = this.tokenLength;}如你所見,代碼相當(dāng)簡潔。首先,skipWhiteSpace()調(diào)用跳過存在于當(dāng)前位置的數(shù)據(jù)中的空格。接著,當(dāng)前令牌(數(shù)據(jù)緩存的索引)的位置存于tokenBuffer 。第三,檢查下一個字符,并根據(jù)字符是什么(它是什么樣令牌)來執(zhí)行switch-case 結(jié)構(gòu)。最后,保存當(dāng)前令牌的令牌長度。
這的確是分析一個數(shù)據(jù)緩沖區(qū)的完整過程。請注意,一旦一個字符串索引開始被發(fā)現(xiàn),該分析器調(diào)用parseStringToken()方法,通過掃描的數(shù)據(jù),直到字符串令牌結(jié)尾。這比試圖處理parseToken()方法內(nèi)所有邏輯執(zhí)行更快,也更容易實現(xiàn)。
JsonTokenizer 內(nèi)方法的其余部分只是輔助parseToken()方法,或者移動數(shù)據(jù)位置(索引)到下一個令牌(當(dāng)前令牌的第一個位置),諸如此類。
JsonParser.parseObject()方法
JsonParser類主要的方法是parseObject()方法,它主要處理從JsonTokenizer得到令牌的類型,并試圖根據(jù)上述類型的輸入數(shù)據(jù)找到JSON對象中。
如下是parseObject() 方法:
private void parseObject(JsonTokenizer tokenizer) {assertHasMoreTokens(tokenizer);tokenizer.parseToken();assertThisTokenType(tokenizer.tokenType(), TokenTypes.JSON_CURLY_BRACKET_LEFT);setElementData(tokenizer, ElementTypes.JSON_OBJECT_START);tokenizer.nextToken();tokenizer.parseToken();byte tokenType = tokenizer.tokenType();while (tokenType != TokenTypes.JSON_CURLY_BRACKET_RIGHT) {assertThisTokenType(tokenType, TokenTypes.JSON_STRING_TOKEN);setElementData(tokenizer, ElementTypes.JSON_PROPERTY_NAME);tokenizer.nextToken();tokenizer.parseToken();tokenType = tokenizer.tokenType();assertThisTokenType(tokenType, TokenTypes.JSON_COLON);tokenizer.nextToken();tokenizer.parseToken();tokenType = tokenizer.tokenType();switch (tokenType) {case TokenTypes.JSON_STRING_TOKEN: {setElementData(tokenizer, ElementTypes.JSON_PROPERTY_VALUE_STRING);}break;case TokenTypes.JSON_STRING_ENC_TOKEN: {setElementData(tokenizer, ElementTypes.JSON_PROPERTY_VALUE_STRING_ENC);}break;case TokenTypes.JSON_NUMBER_TOKEN: {setElementData(tokenizer, ElementTypes.JSON_PROPERTY_VALUE_NUMBER);}break;case TokenTypes.JSON_BOOLEAN_TOKEN: {setElementData(tokenizer, ElementTypes.JSON_PROPERTY_VALUE_BOOLEAN);}break;case TokenTypes.JSON_NULL_TOKEN: {setElementData(tokenizer, ElementTypes.JSON_PROPERTY_VALUE_NULL);}break;case TokenTypes.JSON_CURLY_BRACKET_LEFT: {parseObject(tokenizer);}break;case TokenTypes.JSON_SQUARE_BRACKET_LEFT: {parseArray(tokenizer);}break;}tokenizer.nextToken();tokenizer.parseToken();tokenType = tokenizer.tokenType();if (tokenType == TokenTypes.JSON_COMMA) {tokenizer.nextToken(); //skip , tokens if found here.tokenizer.parseToken();tokenType = tokenizer.tokenType();}}setElementData(tokenizer, ElementTypes.JSON_OBJECT_END); } }parseObject()方法希望看到一個左花括號({),后跟一個字符串標(biāo)記,一個冒號和另一個字符串令牌或數(shù)組的開頭([])或另一個JSON對象。當(dāng)JsonParser從JsonTokenizer獲取這些令牌時,它存儲開始,長度和這些令牌在自己elementBuffer中的語義。然后,數(shù)據(jù)處理代碼可以瀏覽這個elementBuffer后,從輸入數(shù)據(jù)中提取任何需要的數(shù)據(jù)。
看過JsonTokenizer和JsonParser類的核心部分后能讓我們理解分析和解析的工作方式。為了充分理解代碼是如何運作的,你可以看看完整的JsonTokenizer和JsonParser實現(xiàn)。他們每個都不到200行,所以它們應(yīng)該是易于理解的。
JsonNavigator組件
JsonNavigator是一個元素訪問組件。它可以幫助我們訪問 JsonParser 和JsonParser2創(chuàng)建的元素索引。兩個組件產(chǎn)生的索引是相同的,所以來自兩個組件的任何一個索引都可以。如下是代碼示例:
JsonNavigator jsonNavigator = new JsonNavigator(dataBuffer, elementBuffer);一旦JsonNavigator創(chuàng)建,您可以使用它的導(dǎo)航方法,next(),previous()等等。你可以使用asString(),asInt()和asLong()來提取數(shù)據(jù)。你可以使用isEqualUnencoded(String)來比較在數(shù)據(jù)緩沖器中元素的常量字符串。
使用JsonNavigator類看起來非常類似于使用GSON流化API。可以比較一下AllBenchmarks類的gsonStreamBuildObject(Reader)方法,和JsonObjectBuilder類parseJsonObject(JsonNavigator)方法。
他們看起來很相似,不是么? 只是,parseJsonObject()方法能夠使用JsonNavigator的一些優(yōu)化(在本文后面討論),像數(shù)組中基本元素計數(shù),以及對JSON字段名稱更快的字符串比較。
基準(zhǔn)化分析
VTD-XML對StAX,SAX和DOM解析器等XML解析器做了的廣泛的基準(zhǔn)化比較測試。在核心性能上,VTD-XML贏得了他們。
為了對索引疊加解析器的性能建立一些信任依據(jù),我已經(jīng)參考GSON實現(xiàn)了我的JSON解析器。本文的第一個版本只測算了解析一個JSON文件的速度與通過GSON反射構(gòu)造對象。基于讀者的意見,我現(xiàn)在已經(jīng)擴大了基準(zhǔn),基于四種不同的模式來測算GSON:
1、訪問JSON文件所有元素,但不做任何數(shù)據(jù)處理。
2、訪問JSON文件所有元素,并建立一個JSONObject。
3、解析JSON文件,并構(gòu)建了一個Map對象。
4、解析JSON文件,并使用反射它建立一個JSONObject。
請記住,GSON是一個高質(zhì)量的產(chǎn)品,經(jīng)過了很好的測試,也具有良好的錯誤報告等。只有我的JSON解析器是在概念驗證級別。基準(zhǔn)測試只是用來獲得性能上的差異指標(biāo)。他們不是最終的數(shù)據(jù)。也請閱讀下文的基準(zhǔn)討論。
如下是一些基準(zhǔn)結(jié)構(gòu)化組織的細(xì)節(jié):
· 為了平衡JIT,盡量減小一次性開銷,諸如此類。JSON輸入完成1000萬次的小文件解析,100萬次中等文件和大文件。
· 基準(zhǔn)化測試分別重復(fù)三個不同類型的文件, 看看解析器如何做小的,中等和大文件。上述文件類型大小分別為58字節(jié),783字節(jié)和1854字節(jié)。這意味著先迭代1000萬次的一個小文件,進(jìn)行測算。然后是中等文件,最后在大文件。上述文件存于GitHub庫的數(shù)據(jù)目錄中。
· 在解析和測算前,文件完全裝載進(jìn)內(nèi)存中。這樣解析耗時不包含裝載時間。
· 1000萬次迭代(或100萬次迭代)測算都是在自己的進(jìn)程中進(jìn)行。這意味著,每個文件在單獨的進(jìn)程進(jìn)行解析。一個過程運行一次。每個文件都測算3次。解析文件1000萬次的過程啟動和停止3次。流程是順序進(jìn)行的,而不是并行。
如下是毫秒級的執(zhí)行時間數(shù)據(jù):
| 基準(zhǔn) | |||||||||
| ? | 小1 | 小2 | 小3 | 中1 | 中2 | 中3 | 大1 | 大2 | 大3 |
| JsonParser2 | 8.143 | 7.862 | 8.236 | 11.092 | 10.967 | 11.169 | 28.517 | 28.049 | 28.111 |
| JsonParser2 + Builder | 13.431 | 13.416 | 13.416 | 17.799 | 18.377 | 18.439 | 41.636 | 41.839 | 43.025 |
| JsonParser | 13.634 | 13.759 | 14.102 | 19.703 | 19.032 | 20.438 | 43.742 | 41.762 | 42.541 |
| JsonParser + Builder | 19.890 | 19.173 | 19.609 | 29.531 | 26.582 | 28.003 | 56.847 | 60.731 | 58.235 |
| Gson Stream | 44.788 | 45.006 | 44.679 | 33.727 | 34.008 | 33.821 | 69.545 | 69.111 | 68.578 |
| Gson Stream + Builder | 45.490 | 45.334 | 45.302 | 36.972 | 38.001 | 37.253 | 74.865 | 76.565 | 77.517 |
| Gson Map | 66.004 | 65.676 | 64.788 | 42.900 | 42.778 | 42.214 | 83.932 | 84.911 | 86.156 |
| Gson Reflection | 76.300 | 76.473 | 77.174 | 69.825 | 67.750 | 68.734 | 135.177 | 137.483 | 134.337 |
如下是比較基準(zhǔn)所處理事情的說明:
| 描述 | |
| JsonParser2 | 使用JsonParser2解析文件和定位索引。 |
| JsonParser2 + Builder | 使用JsonParser2解析文件和在解析文件上構(gòu)建JsonObject。 |
| JsonParser | 使用JsonParser解析文件和定位索引。 |
| JsonParser + Builder | 使用JsonParser解析文件和在解析文件上構(gòu)建JsonObject。 |
| Gson Stream | 使用Gson streaming API解析文件和迭代訪問所有令牌。 |
| Gson Stream + Builder | 解析文件和構(gòu)建JsonObject。 |
| Gson Map | 解析文件和構(gòu)建Map。 |
| Gson Reflection | 使用反射解析文件和構(gòu)建JsonObject。 |
如你所見,索引疊加實現(xiàn)(JsonParser和JsonParser2)比Gson更快。下面我們將討論一下產(chǎn)生上述結(jié)果的原因的推測。
性能分析
GSON Streaming API并非更快的主要原因是當(dāng)遍歷時所有數(shù)據(jù)都從流中抽取,即使不需要這些數(shù)據(jù)。每一個令牌變成一個string,int,double等,存在消耗。這也是為什么用Gson streaming API解析JSON文件和構(gòu)建JsonOject和訪問元素本身是一樣快。 唯一增加的顯式時間是JsonObject內(nèi)部的JsonObject和數(shù)組的實例化。
數(shù)據(jù)獲取不能解釋這一切,盡管,使用JsonParser2構(gòu)建一個JSONObject比使用Gson streaming API構(gòu)建JSONObject幾乎快兩倍。如下說明了一些我看到的索引疊加解析器比流式解析器的性能優(yōu)勢:
首先,如果你看一下小的和大的文件的測試數(shù)據(jù),每一次解析式GSON都存在一次性開銷。 JsonParser2+ JsonParser和GSON基準(zhǔn)測試間的性能差異在小的文件上更明顯。可能原因是theCharArrayReader創(chuàng)建,或類似的事情。也可能是GSON內(nèi)部的某項處理。
第二,索引疊加解析器可以允許你控制你想抽取的數(shù)據(jù)量。這個讓你更細(xì)粒度的控制解析器的性能。
第三, 若一個字符串令牌含有需要手動從UTF-8轉(zhuǎn)換為UTF-16的轉(zhuǎn)義字符(如“\”\ t\ N \ R“),JsonParser和JsonParser2在分析時能夠識別。如果一個字符串令牌不包含轉(zhuǎn)義字符,JsonNavigator可以用一個比它們更快的字符串創(chuàng)建機制。
第四,JsonNavigator能夠讓數(shù)據(jù)緩沖區(qū)中的數(shù)據(jù)的字符串比較更快。 當(dāng)你需要檢查字段名是否等于常量名時,非常方便。使用Gson’s streaming API,你將需將字段名抽取為一個String對象,并比較常量字符串和String對象。JsonNavigator可以直接比較常量字符串和數(shù)據(jù)緩沖區(qū)中的字符,而無需先創(chuàng)建一個String對象。這可以節(jié)省一個String對象的實例化,并從數(shù)據(jù)緩沖區(qū)中的數(shù)據(jù)復(fù)制到一個String對象的時間,它是僅用于比較(如檢查JSON字段名稱是否等于“key”或“name”或其它)。JsonNavigator使用方式如下所示:
if(jsonNavigator.isEqualUnencoded("fieldName")) { }第五,JsonNavigator可以在其索引向前遍歷,計數(shù)包含原始值(字符串,數(shù)字,布爾值,空值等,但不包含對象或嵌套數(shù)組)數(shù)組中的元素數(shù)量。當(dāng)你不知道數(shù)組包含有多少個元素,我們通常抽取元素并把它們放到一個List中。一旦你遇到數(shù)組結(jié)束的標(biāo)記,將List轉(zhuǎn)成數(shù)組。這意味著構(gòu)建了非必要的List對象。此外,即使該數(shù)組包含原始值,如整數(shù)或布爾值,所有抽取的數(shù)據(jù)也必須要插入到List對象。抽取數(shù)值插入List時進(jìn)行了不必要的對象創(chuàng)建(至少是不必要的自動裝箱)。再次,創(chuàng)建基礎(chǔ)值數(shù)組時,所有的對象都必須再次轉(zhuǎn)換成原始類型,然后插入到數(shù)組中。如下所示是Gson streaming API工作代碼:
List<Integer> elements = new ArrayList<Integer>(); reader.beginArray(); while (reader.hasNext()) {elements.add(reader.nextInt()); } reader.endArray(); int[] ints = new int[elements.size()]; for (int i = 0; i < ints.length; i++) {ints[i] = elements.get(i); }當(dāng)知道數(shù)組包含的元素數(shù)時,我們可以立即創(chuàng)建最終的Java數(shù)組,然后將原始值直接放入數(shù)組。在插入數(shù)值到數(shù)組時,這節(jié)省了List實例化和構(gòu)建,原始值自動裝箱和對象轉(zhuǎn)換到原始值的時間。如下所示是使用JsonNavigator功能相同的代碼:
int[] ints = new int[jsonNavigator.countPrimitiveArrayElements()]; for (int i = 0, n = ints.length; i < n; i++) {ints[i] = jsonNavigator.asInt();jsonNavigator.next(); }即使剛剛從JSON數(shù)組構(gòu)建List對象,知道元素的個數(shù)可以讓你從一開始就能正確的實例化一個ArrayList對象。這樣,你就避免了在達(dá)到預(yù)設(shè)閾值時需動態(tài)調(diào)整ArrayList大小的麻煩。如下是示例代碼:
List<String> strings = new ArrayList<String>(jsonNavigator.countPrimitiveArrayElements()); jsonNavigator.next(); // skip over array start. while (ElementTypes.JSON_ARRAY_END != jsonNavigator.type()) {strings.add(jsonNavigator.asString());jsonNavigator.next(); } jsonNavigator.next(); //skip over array end.第六,當(dāng)需訪問原始數(shù)據(jù)緩沖區(qū)時,可以在很多地方用ropes代替String對象。一個rope是一個含有char數(shù)組引用的一個字符串令牌,有起始位置和長度。可以進(jìn)行字符串比較,就像一個字符串復(fù)制rope等。某些操作可能用rope要比字符串對象快。因為不復(fù)制原始數(shù)據(jù),它們還占用更少的內(nèi)存。
第七,如果需要做很多來回的數(shù)據(jù)訪問,您可以創(chuàng)建更高級的索引。 VTD-XML中的索引包含元素的縮進(jìn)層次,以及同一層的下一個元素(下一個同級)的引用,帶有更高縮進(jìn)層的第一個元素(初始元素),等等。這些都是增加到線性解析器元素索引頂部的整型索引。這種額外的索引可以讓已解析數(shù)據(jù)的遍歷速度更快。
性能和錯誤報告
若看看JsonParser和JsonParser2代碼,你將看到更快的JsonParser2比JsonParser更糟糕的錯誤報告。當(dāng)分析和解析階段一分為二時,良好的數(shù)據(jù)驗證和錯誤報告更易于實現(xiàn)。
通常情況下,這種差異將觸發(fā)爭論,在解析器的實現(xiàn)進(jìn)行取舍時,優(yōu)先考慮性能還是錯誤報告。然而,在索引疊加解析器中,這一討論是沒有必要的。
因為原始數(shù)據(jù)始終以其完整的形式存在于內(nèi)存中,你可以同時具有快和慢的解析器解析相同的數(shù)據(jù)。您可以快速啟動快的解析器,若解析失敗,您可以使用較慢的解析器來檢測其中輸入數(shù)據(jù)中的錯誤位置。當(dāng)快的解析器失敗時,只要將原始數(shù)據(jù)交給較慢的解析器。基于這種方式,你可以獲得兩個解析的優(yōu)點。
基準(zhǔn)分析
基于數(shù)據(jù)(GSON)創(chuàng)建的對象樹與僅標(biāo)識在數(shù)據(jù)中找到的數(shù)據(jù)索引進(jìn)行比較,而沒有討論比較的標(biāo)的,這是不公平的比較。
在應(yīng)用程序內(nèi)部解析文件通常需要如下步驟:
首先是數(shù)據(jù)從硬盤或者網(wǎng)絡(luò)上裝載。接著,解碼數(shù)據(jù),例如從UTF-8到UTF-16。第三步,解析數(shù)據(jù)。第四步,處理數(shù)據(jù)。
為了只測量原始的解析器速度, 我預(yù)裝載待解析的文件到內(nèi)存。 該基準(zhǔn)測試的代碼沒有以任何方式處理數(shù)據(jù)。盡管該基準(zhǔn)化測試只是測試基礎(chǔ)的解析速度,在運行的應(yīng)用程序中,性能差異并沒有轉(zhuǎn)化成性能顯著提高。如下是原因:
流式解析器總是能在所有數(shù)據(jù)裝載進(jìn)內(nèi)存前開始解析數(shù)據(jù)。我的JSON解析器現(xiàn)在實現(xiàn)版本不能這樣做。這意味著即使它在基礎(chǔ)解析基準(zhǔn)上更快,在現(xiàn)實運行的應(yīng)用程序中,我的解析器必須等待數(shù)據(jù)裝載,這將減慢整體的處理速度。如下圖說明:
為了加速整體解析速度,你很可能修改我的解析器為數(shù)據(jù)裝載時即可以解析數(shù)據(jù)。但是很可能會減慢基本解析性能。但整體速度仍可能更快。
此外,通過在執(zhí)行的基準(zhǔn)測試之前數(shù)據(jù)預(yù)加載到內(nèi)存中,我也跳過數(shù)據(jù)解碼步驟。數(shù)據(jù)從UTF-8轉(zhuǎn)碼為UTF-16是也存在消耗。在現(xiàn)實應(yīng)用程序中,你不可以跳過這一步。每個待解析的文件來必須要解碼。這是所有解析器都要支持的一點。流式解析器可以在讀數(shù)據(jù)時進(jìn)行解碼。索引疊加分析器也可以在讀取數(shù)據(jù)到緩沖區(qū)時進(jìn)行解碼。
VTD-XML 和Jackson?(另一個JSON解析器)使用另一種技術(shù)。它們不會解碼所有的原始數(shù)據(jù)。相反,它們直接在原始數(shù)據(jù)上進(jìn)行分析,消費各種數(shù)據(jù)格式,如(ASCII,UTF-8等)。這可以節(jié)省昂貴的解碼步驟,解碼要使用相當(dāng)復(fù)雜分析器。
一般來說,要想知道那個解析器在你的應(yīng)用程序更快,需要基于你真實需要解析的數(shù)據(jù)的基準(zhǔn)上進(jìn)行全量測試。
索引疊加解析器一般討論
我聽到的一個反對索引疊加分析器的論點是,要能夠指向原始數(shù)據(jù),而不是將其抽取到一個對象樹,解析時保持所有數(shù)據(jù)在內(nèi)存中是必要的。在處理大文件時,這將導(dǎo)致內(nèi)存消耗暴增。
一般來說,流式分析器(如SAX或StAX)在解析大文件時將整個文件存入內(nèi)存。然而,只有文件中的數(shù)據(jù)可以以更小的塊進(jìn)行解析和處理,每個塊都是獨立進(jìn)行處理的,這種說法才是對的。例如,一個大的XML文件包含一列元素,其中每一個元素都可以單獨被解析和處理(如日志記錄列表)。如果數(shù)據(jù)能以獨立的塊進(jìn)行解析,你可以實現(xiàn)一個工作良好的索引疊加解析器。
如果文件不能以獨立塊進(jìn)行解析,你仍然需要提取必要的信息到一些結(jié)構(gòu),這些結(jié)構(gòu)可以為處理后面塊的代碼進(jìn)行訪問。盡管使用流式解析器可以做到這一點,你也可以使用索引疊加解析器進(jìn)行處理。
從輸入數(shù)據(jù)中創(chuàng)建對象樹的解析器通常會消耗比原數(shù)據(jù)大小的對象樹更多的內(nèi)存。對象實例相關(guān)聯(lián)的內(nèi)存開銷,加上需要保持對象之間的引用的額外數(shù)據(jù),這是主要原因。
此外,因為所有的數(shù)據(jù)都需要同時在內(nèi)存中,你需要解析前分配一個數(shù)據(jù)緩沖區(qū),大到足以容納所有的數(shù)據(jù)。但是,當(dāng)你開始解析它們時,你并不知道文件大小,如何辦呢?
假如你有一個網(wǎng)頁應(yīng)用程序(如Web服務(wù),或者服務(wù)端應(yīng)用),用戶使用它上傳文件。你不可能知道文件大小,所以開始解析前無法分配合適的緩存給它。基于安全考慮,你應(yīng)該總是設(shè)置一個最大允許文件大小。否則,用戶可以通過上傳超大文件讓你的應(yīng)用崩潰。或者,他們可能甚至寫一個程序,偽裝成上傳文件的瀏覽器,并讓該程序不停地向服務(wù)器發(fā)送數(shù)據(jù)。您可以分配一個緩沖區(qū)適合所允許的最大文件大小。這樣,你的緩沖區(qū)不會因有效文件耗光。如果它耗光了空間,那說明你的用戶已經(jīng)上傳了過大的文件。
免責(zé)聲明:內(nèi)容和圖片源自網(wǎng)絡(luò),版權(quán)歸原作者所有,如有侵犯您的原創(chuàng)版權(quán)請告知,我們將盡快刪除相關(guān)內(nèi)容。
IT行業(yè)、互聯(lián)網(wǎng)、開發(fā)語言(Java、前端HTML5、Python、UI/UE、云計算、自動化測試、大數(shù)據(jù)、人工智能、物聯(lián)網(wǎng)、游戲開發(fā)、網(wǎng)絡(luò)安全、GO語言、PHP)相關(guān)資訊,大連千鋒會第一時間送到大家身邊,也可以關(guān)注微信公眾號【
dalianqianfengjiaoyu】了解相關(guān)行業(yè)資訊。
總結(jié)
以上是生活随笔為你收集整理的高性能Java解析器实现过程详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为wlan配置直连二层组网直接转发
- 下一篇: java将实体类转为json_JavaW