如何用Java编写最快的表达式评估器之一
當(dāng)然,標(biāo)題有點吸引人,但確實如此(您當(dāng)然不相信自己沒有偽造自己的基準(zhǔn),但這是另一回事了)。
因此,上周我正在尋找一個小型且可用的庫來評估數(shù)學(xué)表達(dá)式。 我?guī)缀踔苯优既话l(fā)現(xiàn)了這個stackoverflow帖子 。 推薦的庫( Expr )確實非常快,幾乎滿足了我的所有需求。 但是,它沒有提供限制變量范圍的功能(一切都在VM內(nèi)的一個全局命名空間中)。
因此,我做到了,通常這是我們不應(yīng)該做的:我重新發(fā)明了輪子,并編寫了自己的解析器/評估器。 無論如何,這是一個下雨的星期六,所以我認(rèn)為一個小的遞歸降序解析器,一個可以簡化并最終計算表達(dá)式以及一個用于管理變量的小助手的AST似乎并不重要。 事實并非如此。 我有一個初步的實現(xiàn),并運行得非??臁?一旦進(jìn)行了一些測試,使我確信它可以正確地計算所有內(nèi)容,我想知道與原始文章中提到的其他庫相比,評估器的運行速度。 由于沒有手動優(yōu)化每個內(nèi)部循環(huán)和所有內(nèi)容,因此我沒有太大期望,畢竟有些庫還是商業(yè)庫。 因此,當(dāng)我查看結(jié)果時,我感到非常驚訝。 下面的列表顯示了一個微型基準(zhǔn),該基準(zhǔn)使用相應(yīng)的庫評估相同的表達(dá)。 我的庫parsii的測量是使用最終版本完成的,該版本執(zhí)行了一些簡化操作,例如預(yù)先評估常量表達(dá)式。 但是,沒有像字節(jié)碼生成之類的“黑魔法”或該聯(lián)盟中的任何事情完成。
對于性能測量,表達(dá)式x為(2 +(7 – 5)* 3.14159 * x ^(12-10)+ sin(-3.141)”,其中x從0到1000000。對JIT進(jìn)行10次加熱。然后再執(zhí)行15次,平均執(zhí)行時間為:
- PARSII :28.3毫秒
- 曝光 :37.2毫秒
- MathEval :7748.5毫秒
- JEP :647.0毫秒
- MESP :220.8毫秒
- JFEP :274.3毫秒
現(xiàn)在,我敢肯定,這些庫中的每一個都有各自的優(yōu)勢,因此無法直接進(jìn)行比較。 看到一個簡單的實現(xiàn)可以很好地競爭仍然令人驚奇。
對于那些不太了解編譯器構(gòu)造的人,下面簡要介紹一下它的工作原理:
與任何解析器或編譯器一樣,parsii使用經(jīng)典的方法是使用分詞器 ,該工具將字符流轉(zhuǎn)換為令牌流。 因此,作為字符數(shù)組的“ 4”,“”,“ +”,“”,“ 3”,“”,“ *”,“ 8”的“ 4 + 3 * 8”將被轉(zhuǎn)換為:
- 4(整數(shù))
- +(符號)
- 3(整數(shù))
- *(符號)
- 8(整數(shù))
令牌生成器查看當(dāng)前字符,然后確定要查看的令牌類型,然后讀取屬于該令牌的所有字符。 每個標(biāo)記都有其類型,文本內(nèi)容,并且知道其起始位置(行和字符)。 網(wǎng)上有很多深入的教程,因此這里不再贅述。 您可以看一下源代碼,但是正如我所說的,它只是一個簡單的基本實現(xiàn)。
解析器將經(jīng)典的遞歸降序解析器轉(zhuǎn)換為AST(抽象語法樹),然后可以對其進(jìn)行評估。 這是構(gòu)建解析器的最簡單方法之一,因為它完全是手工編寫的,而不是由工具生成的。 這樣的解析器基本上包含每個語法規(guī)則的方法。
再次有很多此類解析器的教程。 但是,大多數(shù)示例遺漏的是正確的錯誤處理。 除了正確,快速地解析表達(dá)式之外,良好的錯誤處理是良好的解析器的主要方面之一。 這并不難:正如您在源代碼中所看到的那樣,解析器在解析表達(dá)式時從不拋出異常。 將收集所有錯誤,并且解析器將繼續(xù)盡可能長的時間。 即使在出現(xiàn)第一個錯誤之后,仍無法正確評估生成的AST,但請務(wù)必繼續(xù)操作,并且應(yīng)該一次報告盡可能多的錯誤,這一點很重要。 令牌生成器使用相同的方法,因為將格式不正確的令牌(例如帶有兩個小數(shù)分隔符的十進(jìn)制數(shù)字)報告給同一錯誤列表。
評估AST是解析表達(dá)式的結(jié)果,這非常容易。 語法樹的每個節(jié)點都有一個評估方法,該方法將由其父節(jié)點從根節(jié)點開始調(diào)用。 此處eval的結(jié)果是對表達(dá)式求值的結(jié)果。 可以在BinaryOperation中找到這種方法的基本示例,它表示+,-,*等操作。
為了稍微縮短評估時間,執(zhí)行了三個優(yōu)化:
首先,在解析AST之后,在根節(jié)點上調(diào)用一種稱為simple的方法,該方法會傳播到每個子節(jié)點。 然后,每個節(jié)點決定是否可以找到自己的子表達(dá)式的更簡單表示形式。 例如:對于二進(jìn)制運算 ,我們檢查兩個操作數(shù)是否都是常數(shù)(數(shù)字)。 在這種情況下,我們對表達(dá)式求值并返回包含操作結(jié)果的新常量。 對于所有參數(shù)都恒定的函數(shù),也可以這樣做。
在表達(dá)式中使用變量時完成第二次優(yōu)化。 幼稚的方法是使用映射并在需要時讀取或?qū)懭胱兞康闹怠?盡管這確實可行,但是在執(zhí)行時需要進(jìn)行很多查找。 因此,我們有一個名為Variable的特殊類,其中包含變量的名稱和數(shù)值。 解析表達(dá)式時,將在范圍(基本上只是一個映射)中查找一次該變量,然后從現(xiàn)在開始使用。 由于每個查找返回相同的實例,因此在評估表達(dá)式時對變量的訪問與對字段的讀取或?qū)懭胍粯颖阋?#xff0c;因為我們僅訪問Variable的value字段。
第三次也是最后一次優(yōu)化可能不會經(jīng)常發(fā)揮作用。 但由于易于實現(xiàn),因此還是可以實現(xiàn)。 它基本上被稱為“惰性求值”,并在調(diào)用函數(shù)時使用。 函數(shù)不會自動求值其所有參數(shù),然后自動執(zhí)行函數(shù)調(diào)用。 它寧可查看參數(shù),也可以憑自己決定要評估的參數(shù),而不是要決定的參數(shù)。 在if函數(shù)中可以找到使用它的示例。
parsii是根據(jù)MIT許可獲得許可的。 可以在GitHub上找到所有源代碼以及預(yù)編譯的jar。
翻譯自: https://www.javacodegeeks.com/2014/01/how-to-write-one-of-the-fastest-expression-evaluators-in-java.html
總結(jié)
以上是生活随笔為你收集整理的如何用Java编写最快的表达式评估器之一的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ddos最大规模的攻击方式(ddos最大
- 下一篇: linux的rz命令(linux的rz)