日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

高性能Java解析器实现过程详解

發(fā)布時間:2024/3/26 java 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 高性能Java解析器实现过程详解 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

如果你沒有指定數(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
JsonParser28.1437.8628.23611.09210.96711.16928.51728.04928.111
JsonParser2 + Builder13.43113.41613.41617.79918.37718.43941.63641.83943.025
JsonParser13.63413.75914.10219.70319.03220.43843.74241.76242.541
JsonParser + Builder19.89019.17319.60929.53126.58228.00356.84760.73158.235
Gson Stream44.78845.00644.67933.72734.00833.82169.54569.11168.578
Gson Stream + Builder45.49045.33445.30236.97238.00137.25374.86576.56577.517
Gson Map66.00465.67664.78842.90042.77842.21483.93284.91186.156
Gson Reflection76.30076.47377.17469.82567.75068.734135.177137.483134.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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。