成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

V8是如何快速地解析JavaScript延遲解析

開發 前端
解析是將源代碼轉換成一個中間表示形式供編譯器使用的步驟(在V8中,是字節碼編譯器Ignition)。解析和編譯發生在web頁面啟動的關鍵路徑上,在啟動期間,并不是所有提供給瀏覽器的函數都需要被調用。

解析是將源代碼轉換成一個中間表示形式供編譯器使用的步驟(在V8中,是字節碼編譯器Ignition)。解析和編譯發生在web頁面啟動的關鍵路徑上,在啟動期間,并不是所有提供給瀏覽器的函數都需要被調用。盡管開發人員可以使用異步和延遲腳本來延遲這些代碼的加載,但這并不總是可行的。此外,許多web頁面的代碼只能被特定的特性使用,這樣一來,在每個頁面單獨運行期間,用戶是根本無法訪問這些代碼的。

[[266569]]

急切地編譯不必要的代碼會產生實際的資源成本:

  • 創建這些不必要的代碼會占用CPU的一部分時間,這會導致啟動時實際需要的代碼延遲加載。
  • 代碼對象會占用內存,至少在回收機制判定當前代碼不再需要并允許垃圾收集器回收之前是這樣的。
  • ***腳本結束執行時編譯的代碼最終會緩存在磁盤上,占用磁盤空間。

由于這些原因,所有主流瀏覽器都實現了延遲解析。以前的做法是為每個函數生成一個抽象語法樹(AST),然后將其編譯為字節碼,而使用了延遲解析之后,解析器就可以“預解析”它遇到的函數,而不需要對這些函數進行完全解析。它通過切換到預解析器來實現這一點,而預解析器是解析器的一個副本,它只做最基本的工作,否則就會跳過該函數。預解析器驗證它跳過的函數在語法上是否是有效的,并生成正確編譯外部函數所需的所有信息。在后邊調用預解析的函數時,將按需對其進行完全解析和編譯。

變量分配

使預解析復雜化的主要問題是變量分配。

出于性能原因,函數激活是在機器堆棧上進行管理的。例如,如果函數g調用了參數為1和2的函數f:

 

V8是如何快速地解析JavaScript: 延遲解析

 

首先將接收器(即f的this值,由于它是一個草率的函數調用,所以它是globalThis)推入堆棧,接著是被調用的函數f。然后再將參數1和2推入堆棧。此時函數f被調用。為了執行調用,我們首先將g的狀態保存在堆棧上: 包括f的“返回指令指針”(rip;我們需要返回什么代碼)以及“幀指針”(fp;返回時堆棧應該是什么樣子的)。然后我們輸入f,它為局部變量c分配空間,以及它可能需要的任何臨時空間。這確保了當函數激活超出作用域時,函數使用的任何數據都會消失: 它只是從堆棧中彈出。

 

V8是如何快速地解析JavaScript: 延遲解析

 

對帶有參數a,b和局部變量c的函數f的調用的堆棧分配布局。

這種設置的問題是函數可以引用在外部函數中聲明的變量。內部函數存活的時間可能會比它們被創建時的激活時間要長:

V8是如何快速地解析JavaScript: 延遲解析

在上面的例子中,從inner到make_f中聲明的變量d的引用會在make_f返回后進行計算。為了實現這一點,使用詞法閉包的語言的虛擬機會在一個稱為“上下文”的結構中分配從堆上的內部函數中引用的變量。

 

V8是如何快速地解析JavaScript: 延遲解析

 

通過將make_f的參數復制到一個上下文中來對它進行調用,該調用的堆棧布局會在堆上進行分配,供捕捉d的inner稍后使用。

這意味著對于函數中聲明的每個變量,我們需要知道內部函數是否引用了該變量,以便決定是在棧上分配該變量,還是在堆上分配的上下文中分配該變量。當我們計算一個函數的字面量時,我們分配一個閉包,它指向函數的代碼和當前上下文: 包含函數可能需要訪問的變量值的對象。

長話短說,我們至少需要跟蹤預解析器中的變量引用。

如果我們只跟蹤引用,就會過多估計引用的變量。在外部函數中聲明的變量可以通過內部函數中的重新聲明來隱藏,從而創建一個來自該內部函數的引用,并將其指向內部聲明,而不是外部聲明。如果我們無條件地在上下文中分配外部變量,程序性能就會受到影響。因此,要使變量分配能正確地處理預解析過程,我們需要確保預解析后的函數正確地跟蹤變量引用和聲明。

頂層代碼是這條規則的一個例外。一個腳本的頂層總是堆分配的,因為變量在腳本之間是可見的。接近良好工作的體系結構的一個簡單方法是簡單地運行預解析器,而不需要對快速解析的頂層函數進行變量跟蹤;并為內部函數使用完整的解析器,但在編譯的時候跳過它們。這比預解析過程成本更高,因為我們不需要構建整個AST,但它使我們啟動并運行。這正是V8在新版本V8 v6.3 / Chrome 63中所做的。

向預解析器說明變量的情況

跟蹤預解析器中的變量聲明和引用是非常復雜的,因為在JavaScript中,某些部分表達式的含義從一開始就不清楚。例如,假設我們有一個帶參數d的函數f,它有一個內部函數g,從表達式看起來g可能引用了d。

 

V8是如何快速地解析JavaScript: 延遲解析

 

它最終可能確實會引用d,因為我們看到的tokens標記是析構賦值表達式的一部分。

 

V8是如何快速地解析JavaScript: 延遲解析

 

它最終也可能是一個帶有析構參數d的箭頭函數,在這種情況下,f中的d就沒有被g引用。

 

V8是如何快速地解析JavaScript: 延遲解析

 

最初,我們的預解析器是作為解析器的獨立副本實現的,沒有太多的共享,這導致兩個解析器會隨著時間的推移而產生分歧。通過將解析器和預解析器重寫為基于實現了奇異遞歸模板模式的ParserBase,我們成功地***化了共享,同時也保留了單獨副本的性能優勢。這大大簡化了向預解析器添加全部變量跟蹤的工作,因為這個實現的大部分內容可以在解析器和預解析器之間共享。

實際上,忽略變量聲明和頂層函數的引用是不正確的。ECMAScript規范要求在***次解析腳本時要檢測各種類型的變量沖突。例如,如果一個變量在同一作用域內被兩次聲明為詞法變量,則被認為是early SyntaxError。因為我們的預解析器只是跳過了變量聲明,所以在預解析過程中它將允許代碼錯誤地運行。此時我們認為性能上的勝利使對規范的違反情有可原。現在預解析器 能正確地跟蹤變量,盡管如此,我們還是應該在沒有明顯性能代價的情況下消除這類與變量解析相關的違反規范的行為。

跳過內部函數

如前所述,當***次調用一個預解析的函數時,我們將對其進行完全解析,并將生成的AST編譯為字節碼。

 

V8是如何快速地解析JavaScript: 延遲解析

 

該函數直接指向外部上下文,其中包含內部函數需要使用的變量聲明的值。為了允許函數的延遲編譯(并支持調試器),上下文會指向一個名為ScopeInfo的元數據對象。ScopeInfo對象描述了上下文中列出的變量。這意味著在編譯內部函數時,我們可以計算變量在上下文鏈中的位置。

但是,要計算延遲編譯的函數本身是否需要上下文,我們需要再次執行范圍解析: 我們需要知道嵌套在延遲編譯的函數中的函數是否引用了由延遲函數聲明的變量。我們可以通過重新解析這些函數來計算出來。這正是V8在升級到V8v6.3/Chrome63之前所做的。但是,這并不是理想的性能***的方法,因為它使資源大小和解析成本之間的關系變成非線性: 我們將盡可能多地解析嵌套函數。除了動態程序的自然嵌套之外,JavaScript打包器通常用“即時調用函數表達式”(IIFEs)的方式來包裝代碼,這使得大多數JavaScript程序具有多個嵌套層。

 

V8是如何快速地解析JavaScript: 延遲解析

 

每次重新解析至少會增加解析函數的成本。

為了避免非線性性能開銷,我們甚至在預解析過程中執行全作用域解析。我們存儲了足夠的元數據,這樣我們稍后就可以簡單地跳過內部函數,而不必重新解析它們。一種方法是存儲由內部函數引用的變量名。這樣做的存儲成本很高,并要求我們仍然進行重復工作:我們已經在預解析期間執行了變量解析。

相反,我們將在變量分配的地方將每一個變量序列化為它的一個密集標記數組。當我們延遲解析一個函數時,變量按照預解析器看到的順序被重新創建,我們可以簡單地將元數據應用于這些變量。現在函數已經編譯完成,已經不再需要變量分配元數據了,這樣它就可以被當做垃圾進行回收。由于我們只需要這個元數據來處理實際包含內部函數的函數,所以大部分函數甚至不需要這個元數據,從而顯著地降低了內存開銷。

 

V8是如何快速地解析JavaScript: 延遲解析

 

通過跟蹤預解析的函數的元數據,我們可以完全跳過內部函數。

跳過內部函數的性能影響是非線性的,就像重新預解析內部函數的開銷一樣。有些站點將它們的所有函數都提升到了頂層范圍。因為它們的嵌套層數總是0,所以開銷也總是0。然而,許多現代的站點實際上都有許多深層嵌套函數。當V8 v6.3 / Chrome 63啟動該特性時,我們就會在這些站點上看到顯著的改進。啟用該特性的主要優點是,現在代碼的嵌套深度已經無關緊要: 任何函數最多只預解析一次,完全解析一次[1]。

 

V8是如何快速地解析JavaScript: 延遲解析

 

主線程和非主線程的解析時間,以及運行“跳過內部函數”前后都得到了優化。

隨時調用函數表達式

如前所述,打包器通常通過將模塊代碼封裝在一個它們即時調用的閉包中,來將多個模塊組合到一個文件中。這為模塊提供了隔離,允許它們像腳本中唯一的代碼一樣運行。這些函數本質上是嵌套的腳本;腳本執行時這些函數會立即被調用。打包器通常以帶圓括號的函數,即 (function(){…})(),的形式提供即時調用函數表達式(IIFEs,發音為“iffies”)。

由于這些函數在腳本執行期間是立即需要的,所以預解析這些函數并不理想。在腳本的頂層執行過程中,我們急需這些函數被編譯,所以我們會完全解析和編譯這些函數。這意味著,我們在前期解析越快,代碼運行時啟動就越快,并且不會產生不必要的額外成本。

你可能會問,為什么不直接編譯調用的函數呢?雖然開發人員在一個函數被調用時能很容易注意到它,但是對于解析器情況則不同。解析器在開始解析函數之前需要決定該函數是需要立即編譯還是推遲編譯。語法中存在的歧義使得簡單地快速掃描到函數末尾變得很困難,而且成本很快就與常規預解析的成本一樣。

因此V8有兩個簡單的模式,它可以將函數識別為隨時調用函數表達式(PIFEs,發音為“piffies”),這樣它會快速解析并編譯一個函數:

如果一個函數是一個帶圓括號的函數表達式,即(function(){…}),我們假設它將被調用。我們一看到這個模式的開始,即(function,就立即做出這個假設。

在V8 v5.7 / Chrome 57中我們也檢測了由UglifyJS生成的模式!function(){…}(),function(){…}(),function(){…}()。一旦我們看到!function或者function后面如果緊跟著一個PIFE,那么這個檢測就起作用了。

由于V8會立即編譯PIFEs,所以它們可以被用作配置文件導向的反饋[2],通知瀏覽器啟動需要哪些函數。

當V8還在預解析內部函數時,一些開發人員已經注意到JS解析對啟動的影響相當大。optimize-js包會基于靜態啟發式將函數轉換為PIFEs。這個包的創建對V8的負載性能有很大的影響。通過在V8 v6.1上運行optimize-js提供的基準測試,我們復制了這些結果,你只需要查看縮小的腳本。

 

V8是如何快速地解析JavaScript: 延遲解析

 

急切地解析和編譯PIFEs會導致冷啟動和熱啟動稍微快一些 (***和第二頁加載,測量總的解析+編譯+執行時間)。但是,由于對解析器的顯著改進,這在V8 v7.5上的好處要比在V8 v6.1上使用的好處小得多。

盡管如此,但我們現在不再需要重新解析內部函數,而且由于解析器變得更快,通過optimize-js獲得的性能改進也大大降低。實際上,v7.5的默認配置已經比運行在v6.1上的優化版本快得多。即使在v7.5中,對于啟動期間需要的代碼,少量使用PIFEs仍然很有用: 我們避免了預解析,因為我們很早就知道會需要這個函數。

盡管如此,但我們現在不再需要重新解析內部函數,而且由于解析器變得更快,通過optimize-js獲得的性能改進也大大降低。實際上,v7.5的默認配置已經比運行在v6.1上的優化版本快得多。即使在v7.5中,對于啟動期間需要的代碼,少量使用PIFEs仍然很有用: 我們避免了預解析,因為我們很早就知道會需要這個函數。

optimize-js基準測試結果并不能準確地反映實際情況。腳本是同步加載的,整個解析+編譯時間都被計入加載時間。在實際環境中,你可能會使用<script>標記來加載腳本。這使得Chrome的預加載器能夠在腳本被計算之前就發現它,并在不阻塞主線程的情況下下載、解析和編譯該腳本。我們決定急切地編譯的所有東西都是在主線程之外自動編譯的,這樣就會確保計入啟動時間的值最小化。使用非主線程腳本編譯來運行會放大使用PIFEs的影響。

但是,這樣做仍然有成本,特別是內存成本,所以急切地編譯所有東西并不是一個好主意:

V8是如何快速地解析JavaScript: 延遲解析

急切地編譯所有JavaScript會付出巨大的內存代價。

雖然在啟動期間為需要的函數添加圓括號是一個好主意(例如,基于配置的啟動),但是使用像optimize-js這樣的包來應用簡單的靜態啟發式并不是一個好主意。例如,它假設一個函數在啟動期間被調用,如果它是一個函數調用的參數。但是,如果這樣一個函數實現了一個只需要很長時間的完整模塊,那么最終會編譯太多。過于急切地編譯對性能沒有好處: 沒有延遲編譯的V8會顯著地降低加載時間。此外,當UglifyJS和其它minifiers(最小化器)從不是IIFEs的PIFEs中刪除括號時,也就刪除了本可以應用于通用模塊定義樣式模塊的有用提示,這樣一來,optimize-js的一些好處就帶來了問題。這可能是minifiers應該修復的一個問題,以便在急切地編譯PIFEs的瀏覽器上獲得***的性能。

結論

延遲解析加快了啟動速度,并減少了應用程序的內存開銷,這些應用程序帶有的代碼比它們需要的多。能夠正確地跟蹤預解析中的變量聲明和引用對于正確(根據規范)快速地進行預解析是必要的。在預解析器中分配變量還允許我們序列化變量分配信息,以便后續在解析器中使用,這樣我們就可以完全避免必須重新預解析內部函數,避免深度嵌套函數的非線性解析行為。

解析器可以識別的PIFEs避免了啟動過程中立即需要的代碼的初始預解析的開銷。謹慎地使用配置文件導向的PIFEs,或使用打包器,可以提供一個有用的冷啟動減速帶。但是,應該避免為了觸發這種啟發式而將函數封裝在括號中這樣的沒必要的操作,因為這會導致更多的代碼被急切地編譯,從而導致更差的啟動性能和更大的內存使用量。

1.出于內存方面的原因,如果V8在一段時間內沒有被使用,它就會刷新字節碼。如果代碼運行結束后,稍后又需要重新運行,我們將重新解析并編譯它。由于我們允許變量元數據在編譯期間死亡,這就需要在延遲重新編譯的過程中重解析內部函數。此時,我們就需要為代碼的內部函數重新創建元數據,因此就不需要再一次重新預解析代碼內部函數中的內部函數。??(https://v8.dev/blog/preparser#fnref1 )

2.PIFEs也可以看作是基于配置文件通知的的函數表達式。??(https://v8.dev/blog/preparser#fnref2 )

英文原文:https://v8.dev/blog/preparser

 

譯者:天天向上

責任編輯:武曉燕 來源: 今日頭條
相關推薦

2023-06-05 16:38:51

JavaScript編程語言V8

2022-09-16 08:32:25

JavaC++語言

2020-10-30 10:15:21

Chrome V8JavaScript前端

2010-07-20 16:35:52

V8JavaScript瀏覽器

2020-10-12 06:35:34

V8JavaScript

2022-06-02 12:02:12

V8C++JavaScript

2019-02-26 13:00:11

JavaScriptURL前端

2021-08-11 22:50:53

JavaScript編程開發

2009-07-20 09:36:04

谷歌瀏覽器安全漏洞

2017-04-05 20:00:32

ChromeObjectJS代碼

2014-11-26 09:51:24

GithubGoogleV8

2023-10-10 10:23:50

JavaScriptV8

2023-02-28 07:56:07

V8內存管理

2011-10-19 13:47:57

ibmdwRationalWAS

2016-10-18 15:18:48

JEECMS V*javaCMS系統

2021-08-29 18:34:44

編譯V8C++

2020-09-27 07:32:18

V8

2016-11-02 08:42:13

火狐瀏覽器引擎

2011-09-09 17:31:45

Android WebJavascript

2011-03-14 09:51:32

DB2 V8數據庫系統
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品中文字幕一区二区三区 | 精品一区二区三区日本 | 国产精品一区二区无线 | 亚州综合一区 | 中文字幕视频在线观看 | 亚欧洲精品在线视频免费观看 | 久久久入口 | 免费视频一区二区 | 中文日韩在线 | 国产不卡一区 | 伊人网伊人 | 黄色大全免费看 | 99r在线| 欧美日韩国产高清 | 日本一区高清 | 亚洲精品一 | 紧缚调教一区二区三区视频 | 91 在线 | 久久另类视频 | 国产欧美精品一区二区色综合朱莉 | 免费观看国产视频在线 | 高清成人免费视频 | 精品视频在线观看 | 欧美美乳| 精品综合 | 天天综合久久 | 成人一区二区电影 | 一级网站 | 亚洲福利电影网 | 亚洲欧美视频 | 搞黄视频免费看 | 欧美成人免费在线 | 亚洲免费在线视频 | 欧美天天视频 | 91精品国产综合久久久密闭 | 亚洲 欧美 另类 综合 偷拍 | 欧美精品一区二区三区在线播放 | 91精品国产欧美一区二区 | 欧美日产国产成人免费图片 | 色婷婷一区二区三区四区 | 日韩精品久久久 |