Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 国产成人精品影视,亚洲欧美日韩三级,日本一区二区三区久久久久

          整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          探秘網(wǎng)頁(yè)性能提升利器之CSS硬件加速

          天我們將為大家介紹一個(gè)令網(wǎng)頁(yè)性能大幅提升的神奇技術(shù)——CSS硬件加速。隨著移動(dòng)互聯(lián)網(wǎng)的蓬勃發(fā)展和網(wǎng)頁(yè)設(shè)計(jì)越發(fā)復(fù)雜,如何優(yōu)化網(wǎng)頁(yè)性能成為了前端開(kāi)發(fā)者們亟待解決的問(wèn)題。在這篇文章中,我們將深入了解CSS硬件加速的原理,并通過(guò)一個(gè)生動(dòng)的案例來(lái)展示它如何幫助我們改善網(wǎng)頁(yè)的渲染性能。

          一、什么是CSS硬件加速

          在傳統(tǒng)的網(wǎng)頁(yè)渲染中,瀏覽器使用中央處理器(CPU)來(lái)處理CSS樣式和頁(yè)面渲染。然而,隨著網(wǎng)頁(yè)變得越來(lái)越復(fù)雜,例如包含大量動(dòng)畫(huà)、過(guò)渡效果或復(fù)雜的變換,CPU可能會(huì)承擔(dān)較重的負(fù)擔(dān),導(dǎo)致頁(yè)面加載緩慢或卡頓。CSS硬件加速是一種解決方案,它充分利用了計(jì)算機(jī)的圖形處理單元(GPU)來(lái)加快CSS樣式的處理和渲染,從而提高頁(yè)面性能和流暢度。

          1.1 CPU

          CPU 即中央處理器。

          CPU是計(jì)算機(jī)的大腦,它提供了一套指令集,我們寫(xiě)的程序最終會(huì)通過(guò) CPU 指令來(lái)控制的計(jì)算機(jī)的運(yùn)行。它會(huì)對(duì)指令進(jìn)行譯碼,然后通過(guò)邏輯電路執(zhí)行該指令。整個(gè)執(zhí)行的流程分為了多個(gè)階段,叫做流水線。指令流水線包括取指令、譯碼、執(zhí)行、取數(shù)、寫(xiě)回五步,這是一個(gè)指令周期。CPU會(huì)不斷的執(zhí)行指令周期來(lái)完成各種任務(wù)。

          1.2 GPU

          GPU 即圖形處理器。

          GPU,是Graphics ProcessingUnit的簡(jiǎn)寫(xiě),是現(xiàn)代顯卡中非常重要的一個(gè)部分,其地位與CPU在主板上的地位一致,主要負(fù)責(zé)的任務(wù)是加速圖形處理速度。GPU是顯卡的“大腦”,它決定了該顯卡的檔次和大部分性能,同時(shí)也是2D顯示卡和3D顯示卡的區(qū)別依據(jù)。2D顯示芯片在處理3D圖像和特效時(shí)主要依賴CPU的處理能力,稱為“軟加速”。3D顯示芯片是將三維圖像和特效處理功能集中在顯示芯片內(nèi),也即所謂的“硬件加速”功能。

          二、CSS硬件加速原理

          CSS硬件加速的原理涉及到瀏覽器的渲染引擎、GPU以及優(yōu)化渲染的過(guò)程。

          2.1 瀏覽器的渲染流程

          一個(gè)完整的渲染步驟大致可總結(jié)為如下:

          • 渲染進(jìn)程將HTML內(nèi)容轉(zhuǎn)換為能夠讀懂的DOM樹(shù)結(jié)構(gòu)。
          • 渲染引擎將CSS樣式表轉(zhuǎn)化為瀏覽器可以理解的 styleSheets ,計(jì)算出DOM節(jié)點(diǎn)的樣式。
          • 創(chuàng)建布局樹(shù),并計(jì)算元素的布局信息。
          • 對(duì)布局樹(shù)進(jìn)行分層,并生成分層樹(shù)。
          • 為每個(gè)圖層生成繪制列表,并將其提交到合成線程。
          • 合成線程將圖層分成圖塊,并在光柵化線程池中將圖塊轉(zhuǎn)換成位圖。
          • 合成線程發(fā)送繪制圖塊命令DrawQuad給瀏覽器進(jìn)程。
          • 瀏覽器進(jìn)程根據(jù)DrawQuad消息生成頁(yè)面,并顯示到顯示器上。

          2.2 CSS硬件加速觸發(fā)

          在傳統(tǒng)的渲染過(guò)程中,布局和繪制是由CPU來(lái)完成的,而在CSS硬件加速下,GPU參與了渲染的處理,從而提高了性能。

          CSS 中的以下幾個(gè)屬性能觸發(fā)硬件加速:

          1.transform屬性:該屬性用于應(yīng)用2D或3D變換效果,如旋轉(zhuǎn)、縮放、平移等。當(dāng)使用transform屬性時(shí),瀏覽器會(huì)將變換任務(wù)交給GPU處理,從而實(shí)現(xiàn)硬件加速。

          2.opacity屬性:該屬性用于設(shè)置元素的不透明度。雖然它主要用于控制透明度,但是一個(gè)不為1的值(例如0.99)也可以觸發(fā)硬件加速。

          3.will-change屬性:will-change屬性用于提示瀏覽器一個(gè)元素將要發(fā)生的變化,以便瀏覽器在渲染過(guò)程中做出優(yōu)化。

          一旦CSS硬件加速被觸發(fā),相關(guān)的渲染任務(wù)將被GPU處理。GPU在處理圖形和動(dòng)畫(huà)方面通常比CPU更快和更高效。對(duì)于復(fù)雜的CSS動(dòng)畫(huà)和變換,GPU可以并行處理多個(gè)任務(wù),從而提高性能和流暢度。

          請(qǐng)注意,CSS硬件加速并不是適用于所有情況。雖然它在許多情況下可以帶來(lái)顯著的性能提升,但有時(shí)也可能導(dǎo)致額外的GPU資源占用,從而影響其他應(yīng)用程序的性能。因此,在使用CSS硬件加速時(shí),我們應(yīng)該進(jìn)行性能測(cè)試和優(yōu)化,確保在特定情況下確實(shí)能獲得性能的提升。

          三、CSS硬件加速案例

          現(xiàn)在,我們來(lái)看一個(gè)實(shí)際的案例,通過(guò)啟用CSS硬件加速來(lái)改善網(wǎng)頁(yè)性能。

          <!DOCTYPE html>

          <html lang="en">

          <head>

          <meta charset="UTF-8" />

          <meta name="viewport" content="width=device-width, initial-scale=1.0" />

          <title>Document</title>

          <style>

          .app {

          position: relative;

          width: 400px;

          height: 400px;

          }

          .box {

          position: absolute;

          left: 0;

          top: 0;

          width: 100px;

          height: 100px;

          background-color: yellowgreen;

          }

          .box-run1 {

          -webkit-animation: run1 4s infinite;

          animation: run1 4s infinite;

          }

          .box-run2 {

          -webkit-animation: run2 4s infinite;

          animation: run2 4s infinite;

          }

          @keyframes run1 {

          0% {

          top: 0;

          left: 0;

          }

          25% {

          top: 0;

          left: 200px;

          }

          50% {

          top: 200px;

          left: 200px;

          }

          75% {

          top: 200px;

          left: 0;

          }

          }

          @keyframes run2 {

          0% {

          transform: translate(0, 0);

          }

          25% {

          transform: translate(200px, 0);

          }

          50% {

          transform: translate(200px, 200px);

          }

          75% {

          transform: translate(0, 200px);

          }

          }

          </style>

          </head>

          <body>

          <div class="app">

          <div class="box"></div>

          </div>

          <button class="btn1">循環(huán)轉(zhuǎn)換</button>

          <button class="btn2">硬件加速</button>

          <script>

          let box = document.querySelector(".box");

          let btn1 = document.querySelector(".btn1");

          let btn2 = document.querySelector(".btn2");

          btn1.addEventListener("click", function (e) {

          box.classList.remove("box-run2");

          box.classList.add("box-run1");

          });

          btn2.addEventListener("click", function (e) {

          box.classList.remove("box-run1");

          box.classList.add("box-run2");

          });

          </script>

          </body>

          </html>

          此時(shí)我們可以運(yùn)行代碼,在頁(yè)面上可以看到,2個(gè)按鈕均能使box在app當(dāng)中循環(huán)移動(dòng)。但對(duì)于這兩種方式的移動(dòng),他們的效率卻有著很大的差異。我們可以使用開(kāi)發(fā)者工具里的Performance去查看。

          當(dāng)我們點(diǎn)擊btn1時(shí),此時(shí)box盒子通過(guò)定位的left和top進(jìn)行循環(huán)移動(dòng)時(shí)。

          此時(shí)我們可以看到細(xì)節(jié)模塊的記錄詳情。

          藍(lán)色(Loading):網(wǎng)絡(luò)通信和HTML解析

          黃色(Scripting):Javascript執(zhí)行

          紫色(Rendering):樣式計(jì)算和布局,即重排

          綠色(Painting):重繪

          灰色(Other):其他事件花費(fèi)的時(shí)間

          白色(Idle):空閑時(shí)間

          細(xì)節(jié)模塊有4個(gè)面板,Summary面板每個(gè)事件都會(huì)有,其他三個(gè)只針對(duì)特定事件會(huì)有。

          當(dāng)我們點(diǎn)擊btn2時(shí),此時(shí)box盒子通過(guò)transform屬性進(jìn)行css硬件加速后進(jìn)行循環(huán)移動(dòng)時(shí)。

          通過(guò)對(duì)比我們不難發(fā)現(xiàn),當(dāng)啟用硬件加速時(shí),方塊的變換會(huì)更加流暢,其樣式計(jì)算和布局、重繪的時(shí)間都會(huì)減少。因?yàn)镚PU參與了渲染過(guò)程。

          總結(jié)

          CSS硬件加速是一個(gè)強(qiáng)大的前端技術(shù),可以顯著提高網(wǎng)頁(yè)的性能和流暢度。通過(guò)啟用硬件加速,我們可以將一些渲染任務(wù)交給GPU來(lái)處理,減輕CPU的負(fù)擔(dān),從而優(yōu)化網(wǎng)頁(yè)的渲染性能。然而,我們需要注意不要濫用硬件加速,避免觸發(fā)不必要的GPU渲染,以確保真正獲得性能提升。在日常的網(wǎng)頁(yè)開(kāi)發(fā)中,我們可以靈活運(yùn)用CSS硬件加速,為用戶帶來(lái)更好的瀏覽體驗(yàn)。

          數(shù)適配器機(jī)制不僅復(fù)雜,而且成本很高。

          本文最初發(fā)表于 v8.dev(Faster JavaScript calls),基于 CC 3.0 協(xié)議分享,由 InfoQ 翻譯并發(fā)布。

          JavaScript 允許使用與預(yù)期形式參數(shù)數(shù)量不同的實(shí)際參數(shù)來(lái)調(diào)用一個(gè)函數(shù),也就是傳遞的實(shí)參可以少于或者多于聲明的形參數(shù)量。前者稱為申請(qǐng)不足(under-application),后者稱為申請(qǐng)過(guò)度(over-application)。

          在申請(qǐng)不足的情況下,剩余形式參數(shù)會(huì)被分配 undefined 值。在申請(qǐng)過(guò)度的情況下,可以使用 rest 參數(shù)和 arguments 屬性訪問(wèn)剩余實(shí)參,或者如果它們是多余的可以直接忽略。如今,許多 Web/Node.js 框架都使用這個(gè) JS 特性來(lái)接受可選形參,并創(chuàng)建更靈活的 API。

          直到最近,V8 都有一種專門的機(jī)制來(lái)處理參數(shù)大小不匹配的情況:這種機(jī)制叫做參數(shù)適配器框架。不幸的是,參數(shù)適配是有性能成本的,但在現(xiàn)代的前端和中間件框架中這種成本往往是必須的。但事實(shí)證明,我們可以通過(guò)一個(gè)巧妙的技巧來(lái)拿掉這個(gè)多余的框架,簡(jiǎn)化 V8 代碼庫(kù)并消除幾乎所有的開(kāi)銷。

          我們可以通過(guò)一個(gè)微型基準(zhǔn)測(cè)試來(lái)計(jì)算移除參數(shù)適配器框架可以獲得的性能收益。

          console.time();
          function f(x, y, z) {}
          for (let i = 0; i <  N; i++) {
            f(1, 2, 3, 4, 5);
          }
          console.timeEnd();

          移除參數(shù)適配器框架的性能收益,通過(guò)一個(gè)微基準(zhǔn)測(cè)試來(lái)得出。

          上圖顯示,在無(wú) JIT 模式(Ignition)下運(yùn)行時(shí),開(kāi)銷消失,并且性能提高了 11.2%。使用 TurboFan 時(shí),我們的速度提高了 40%。

          這個(gè)微基準(zhǔn)測(cè)試自然是為了最大程度地展現(xiàn)參數(shù)適配器框架的影響而設(shè)計(jì)的。但是,我們也在許多基準(zhǔn)測(cè)試中看到了顯著的改進(jìn),例如我們內(nèi)部的 JSTests/Array 基準(zhǔn)測(cè)試(7%)和 Octane2(Richards 子項(xiàng)為 4.6%,EarleyBoyer 為 6.1%)。

          太長(zhǎng)不看版:反轉(zhuǎn)參數(shù)

          這個(gè)項(xiàng)目的重點(diǎn)是移除參數(shù)適配器框架,這個(gè)框架在訪問(wèn)棧中被調(diào)用者的參數(shù)時(shí)為其提供了一個(gè)一致的接口。為此,我們需要反轉(zhuǎn)棧中的參數(shù),并在被調(diào)用者框架中添加一個(gè)包含實(shí)際參數(shù)計(jì)數(shù)的新插槽。下圖顯示了更改前后的典型框架示例。

          移除參數(shù)適配器框架之前和之后的典型 JavaScript 棧框架。


          加快 JavaScript 調(diào)用

          為了講清楚我們?nèi)绾渭涌煺{(diào)用,首先我們來(lái)看看 V8 如何執(zhí)行一個(gè)調(diào)用,以及參數(shù)適配器框架如何工作。

          當(dāng)我們?cè)?JS 中調(diào)用一個(gè)函數(shù)調(diào)用時(shí),V8 內(nèi)部會(huì)發(fā)生什么呢?用以下 JS 腳本為例:

          function add42(x) {
            return x + 42;
          }
          add42(3);

          在函數(shù)調(diào)用期間 V8 內(nèi)部的執(zhí)行流程。


          Ignition

          V8 是一個(gè)多層 VM。它的第一層稱為 Ignition,是一個(gè)具有累加器寄存器的字節(jié)碼棧機(jī)。V8 首先會(huì)將代碼編譯為 Ignition 字節(jié)碼。上面的調(diào)用被編譯為以下內(nèi)容:

          0d              LdaUndefined              ;; Load undefined into the accumulator
          26 f9           Star r2                   ;; Store it in register r2
          13 01 00        LdaGlobal [1]             ;; Load global pointed by const 1 (add42)
          26 fa           Star r1                   ;; Store it in register r1
          0c 03           LdaSmi [3]                ;; Load small integer 3 into the accumulator
          26 f8           Star r3                   ;; Store it in register r3
          5f fa f9 02     CallNoFeedback r1, r2-r3  ;; Invoke call

          調(diào)用的第一個(gè)參數(shù)通常稱為接收器(receiver)。接收器是 JSFunction 中的 this 對(duì)象,并且每個(gè) JS 函數(shù)調(diào)用都必須有一個(gè) this。CallNoFeedback 的字節(jié)碼處理器需要使用寄存器列表 r2-r3 中的參數(shù)來(lái)調(diào)用對(duì)象 r1。

          在深入研究字節(jié)碼處理器之前,請(qǐng)先注意寄存器在字節(jié)碼中的編碼方式。它們是負(fù)的單字節(jié)整數(shù):r1 編碼為 fa,r2 編碼為 f9,r3 編碼為 f8。我們可以將任何寄存器 ri 稱為 fb - i,實(shí)際上正如我們所見(jiàn),正確的編碼是- 2 - kFixedFrameHeaderSize - i。寄存器列表使用第一個(gè)寄存器和列表的大小來(lái)編碼,因此 r2-r3 為 f9 02。

          Ignition 中有許多字節(jié)碼調(diào)用處理器。可以在此處查看它們的列表。它們彼此之間略有不同。有些字節(jié)碼針對(duì) undefined 的接收器調(diào)用、屬性調(diào)用、具有固定數(shù)量的參數(shù)調(diào)用或通用調(diào)用進(jìn)行了優(yōu)化。在這里我們分析 CallNoFeedback,這是一個(gè)通用調(diào)用,在該調(diào)用中我們不會(huì)積累執(zhí)行過(guò)程中的反饋。

          這個(gè)字節(jié)碼的處理器非常簡(jiǎn)單。它是用 CodeStubAssembler 編寫(xiě)的,你可以在此處查看。本質(zhì)上,它會(huì)尾調(diào)用一個(gè)架構(gòu)依賴的內(nèi)置 InterpreterPushArgsThenCall。

          這個(gè)內(nèi)置方法實(shí)際上是將返回地址彈出到一個(gè)臨時(shí)寄存器中,壓入所有參數(shù)(包括接收器),然后壓回該返回地址。此時(shí),我們不知道被調(diào)用者是否是可調(diào)用對(duì)象,也不知道被調(diào)用者期望多少個(gè)參數(shù),也就是它的形式參數(shù)數(shù)量。

          內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。

          最終,執(zhí)行會(huì)尾調(diào)用到內(nèi)置的 Call。它會(huì)在那里檢查目標(biāo)是否是適當(dāng)?shù)暮瘮?shù)、構(gòu)造器或任何可調(diào)用對(duì)象。它還會(huì)讀取共享 shared function info 結(jié)構(gòu)以獲得其形式參數(shù)計(jì)數(shù)。

          如果被調(diào)用者是一個(gè)函數(shù)對(duì)象,它將對(duì)內(nèi)置的 CallFunction 進(jìn)行尾部調(diào)用,并在其中進(jìn)行一系列檢查,包括是否有 undefined 對(duì)象作為接收器。如果我們有一個(gè) undefined 或 null 對(duì)象作為接收器,則應(yīng)根據(jù) ECMA 規(guī)范對(duì)其修補(bǔ),以引用全局代理對(duì)象。

          執(zhí)行隨后會(huì)對(duì)內(nèi)置的 InvokeFunctionCode 進(jìn)行尾調(diào)用。在沒(méi)有參數(shù)不匹配的情況下,InvokeFunctionCode 只會(huì)調(diào)用被調(diào)用對(duì)象中字段 Code 所指向的內(nèi)容。這可以是一個(gè)優(yōu)化函數(shù),也可以是內(nèi)置的 InterpreterEntryTrampoline。

          如果我們假設(shè)要調(diào)用的函數(shù)尚未優(yōu)化,則 Ignition trampoline 將設(shè)置一個(gè) IntepreterFrame。你可以在此處查看V8 中框架類型的簡(jiǎn)短摘要。

          接下來(lái)發(fā)生的事情就不用多談了,我們可以看一個(gè)被調(diào)用者執(zhí)行期間的解釋器框架快照。

          我們看到框架中有固定數(shù)量的插槽:返回地址、前一個(gè)框架指針、上下文、我們正在執(zhí)行的當(dāng)前函數(shù)對(duì)象、該函數(shù)的字節(jié)碼數(shù)組以及我們當(dāng)前正在執(zhí)行的字節(jié)碼偏移量。最后,我們有一個(gè)專用于此函數(shù)的寄存器列表(你可以將它們視為函數(shù)局部變量)。add42 函數(shù)實(shí)際上沒(méi)有任何寄存器,但是調(diào)用者具有類似的框架,其中包含 3 個(gè)寄存器。

          如預(yù)期的那樣,add42 是一個(gè)簡(jiǎn)單的函數(shù):

          25 02             Ldar a0          ;; Load the first argument to the accumulator
          40 2a 00          AddSmi [42]      ;; Add 42 to it
          ab                Return           ;; Return the accumulator

          請(qǐng)注意我們?cè)?Ldar(Load Accumulator Register)字節(jié)碼中編碼參數(shù)的方式:參數(shù) 1(a0)用數(shù)字 02 編碼。實(shí)際上,任何參數(shù)的編碼規(guī)則都是[ai] = 2 + parameter_count - i - 1,接收器[this] = 2 + parameter_count,或者在本例中[this] = 3。此處的參數(shù)計(jì)數(shù)不包括接收器。

          現(xiàn)在我們就能理解為什么用這種方式對(duì)寄存器和參數(shù)進(jìn)行編碼。它們只是表示一個(gè)框架指針的偏移量。然后,我們可以用相同的方式處理參數(shù)/寄存器的加載和存儲(chǔ)。框架指針的最后一個(gè)參數(shù)偏移量為 2(先前的框架指針和返回地址)。這就解釋了編碼中的 2。解釋器框架的固定部分是 6 個(gè)插槽(4 個(gè)來(lái)自框架指針),因此寄存器零位于偏移量-5 處,也就是 fb,寄存器 1 位于 fa 處。很聰明是吧?

          但請(qǐng)注意,為了能夠訪問(wèn)參數(shù),該函數(shù)必須知道棧中有多少個(gè)參數(shù)!無(wú)論有多少參數(shù),索引 2 都指向最后一個(gè)參數(shù)!

          Return 的字節(jié)碼處理器將調(diào)用內(nèi)置的 LeaveInterpreterFrame 來(lái)完成。該內(nèi)置函數(shù)本質(zhì)上是從框架中讀取函數(shù)對(duì)象以獲取參數(shù)計(jì)數(shù),彈出當(dāng)前框架,恢復(fù)框架指針,將返回地址保存在一個(gè)暫存器中,根據(jù)參數(shù)計(jì)數(shù)彈出參數(shù)并跳轉(zhuǎn)到暫存器中的地址。

          這套流程很棒!但是,當(dāng)我們調(diào)用一個(gè)實(shí)參數(shù)量少于或多于其形參數(shù)量的函數(shù)時(shí),會(huì)發(fā)生什么呢?這個(gè)聰明的參數(shù)/寄存器訪問(wèn)流程將失敗,我們?cè)撊绾卧谡{(diào)用結(jié)束時(shí)清理參數(shù)?

          參數(shù)適配器框架

          現(xiàn)在,我們使用更少或更多的實(shí)參來(lái)調(diào)用 add42:

          add42();
          add42(1, 2, 3);

          JS 開(kāi)發(fā)人員會(huì)知道,在第一種情況下,x 將被分配 undefined,并且該函數(shù)將返回 undefined + 42 = NaN。在第二種情況下,x 將被分配 1,函數(shù)將返回 43,其余參數(shù)將被忽略。請(qǐng)注意,調(diào)用者不知道是否會(huì)發(fā)生這種情況。即使調(diào)用者檢查了參數(shù)計(jì)數(shù),被調(diào)用者也可以使用 rest 參數(shù)或 arguments 對(duì)象訪問(wèn)其他所有參數(shù)。實(shí)際上,在 sloppy 模式下甚至可以在 add42 外部訪問(wèn) arguments 對(duì)象。

          如果我們執(zhí)行與之前相同的步驟,則將首先調(diào)用內(nèi)置的 InterpreterPushArgsThenCall。它將像這樣將參數(shù)推入棧:

          內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。


          繼續(xù)與以前相同的過(guò)程,我們檢查被調(diào)用者是否為函數(shù)對(duì)象,獲取其參數(shù)計(jì)數(shù),并將接收器補(bǔ)到全局代理。最終,我們到達(dá)了 InvokeFunctionCode。

          在這里我們不會(huì)跳轉(zhuǎn)到被調(diào)用者對(duì)象中的 Code。我們檢查參數(shù)大小和參數(shù)計(jì)數(shù)之間是否存在不匹配,然后跳轉(zhuǎn)到 ArgumentsAdaptorTrampoline。

          在這個(gè)內(nèi)置組件中,我們構(gòu)建了一個(gè)額外的框架,也就是臭名昭著的參數(shù)適配器框架。這里我不會(huì)解釋內(nèi)置組件內(nèi)部發(fā)生了什么,只會(huì)向你展示內(nèi)置組件調(diào)用被調(diào)用者的 Code 之前的框架狀態(tài)。請(qǐng)注意,這是一個(gè)正確的 x64 call(不是 jmp),在被調(diào)用者執(zhí)行之后,我們將返回到 ArgumentsAdaptorTrampoline。這與進(jìn)行尾調(diào)用的 InvokeFunctionCode 正好相反。

          我們創(chuàng)建了另一個(gè)框架,該框架復(fù)制了所有必需的參數(shù),以便在被調(diào)用者框架頂部精確地包含參數(shù)的形參計(jì)數(shù)。它創(chuàng)建了一個(gè)被調(diào)用者函數(shù)的接口,因此后者無(wú)需知道參數(shù)數(shù)量。被調(diào)用者將始終能夠使用與以前相同的計(jì)算結(jié)果來(lái)訪問(wèn)其參數(shù),即[ai] = 2 + parameter_count - i - 1。

          V8 具有一些特殊的內(nèi)置函數(shù),它們?cè)谛枰ㄟ^(guò) rest 參數(shù)或 arguments 對(duì)象訪問(wèn)其余參數(shù)時(shí)能夠理解適配器框架。它們始終需要檢查被調(diào)用者框架頂部的適配器框架類型,然后采取相應(yīng)措施。

          如你所見(jiàn),我們解決了參數(shù)/寄存器訪問(wèn)問(wèn)題,但是卻添加了很多復(fù)雜性。需要訪問(wèn)所有參數(shù)的內(nèi)置組件都需要了解并檢查適配器框架的存在。不僅如此,我們還需要注意不要訪問(wèn)過(guò)時(shí)的舊數(shù)據(jù)。考慮對(duì) add42 的以下更改:

          function add42(x) {
            x += 42;
            return x;
          }

          現(xiàn)在,字節(jié)碼數(shù)組為:

          25 02             Ldar a0       ;; Load the first argument to the accumulator
          40 2a 00          AddSmi [42]   ;; Add 42 to it
          26 02             Star a0       ;; Store accumulator in the first argument slot
          ab                Return        ;; Return the accumulator

          如你所見(jiàn),我們現(xiàn)在修改 a0。因此,在調(diào)用 add42(1, 2, 3)的情況下,參數(shù)適配器框架中的插槽將被修改,但調(diào)用者框架仍將包含數(shù)字 1。我們需要注意,參數(shù)對(duì)象正在訪問(wèn)修改后的值,而不是舊值。

          從函數(shù)返回很簡(jiǎn)單,只是會(huì)很慢。還記得 LeaveInterpreterFrame 做什么嗎?它基本上會(huì)彈出被調(diào)用者框架和參數(shù),直到到達(dá)最大形參計(jì)數(shù)為止。因此,當(dāng)我們返回參數(shù)適配器存根時(shí),棧如下所示:

          被調(diào)用者 add42 執(zhí)行之后的框架狀態(tài)。

          我們需要彈出參數(shù)數(shù)量,彈出適配器框架,根據(jù)實(shí)際參數(shù)計(jì)數(shù)彈出所有參數(shù),然后返回到調(diào)用者執(zhí)行。

          簡(jiǎn)單總結(jié):參數(shù)適配器機(jī)制不僅復(fù)雜,而且成本很高。

          移除參數(shù)適配器框架

          我們可以做得更好嗎?我們可以移除適配器框架嗎?事實(shí)證明我們確實(shí)可以。

          我們回顧一下之前的需求:


          1. 我們需要能夠像以前一樣無(wú)縫訪問(wèn)參數(shù)和寄存器。訪問(wèn)它們時(shí)無(wú)法進(jìn)行檢查。那成本太高了。
          2. 我們需要能夠從棧中構(gòu)造 rest 參數(shù)和 arguments 對(duì)象。
          3. 從一個(gè)調(diào)用返回時(shí),我們需要能夠輕松清理未知數(shù)量的參數(shù)。
          4. 此外,當(dāng)然我們希望沒(méi)有額外的框架!

          如果要消除多余的框架,則需要確定將參數(shù)放在何處:在被調(diào)用者框架中還是在調(diào)用者框架中。

          被調(diào)用者框架中的參數(shù)

          假設(shè)我們將參數(shù)放在被調(diào)用者框架中。這似乎是一個(gè)好主意,因?yàn)闊o(wú)論何時(shí)彈出框架,我們都會(huì)一次彈出所有參數(shù)!

          參數(shù)必須位于保存的框架指針和框架末尾之間的某個(gè)位置。這就要求框架的大小不會(huì)被靜態(tài)地知曉。訪問(wèn)參數(shù)仍然很容易,它就是一個(gè)來(lái)自框架指針的簡(jiǎn)單偏移量。但現(xiàn)在訪問(wèn)寄存器要復(fù)雜得多,因?yàn)樗鼤?huì)根據(jù)參數(shù)的數(shù)量而變化。

          棧指針總是指向最后一個(gè)寄存器,然后我們可以使用它來(lái)訪問(wèn)寄存器而無(wú)需知道參數(shù)計(jì)數(shù)。這種方法可能行得通,但它有一個(gè)關(guān)鍵缺陷。它需要復(fù)制所有可以訪問(wèn)寄存器和參數(shù)的字節(jié)碼。我們將需要 LdaArgument 和 LdaRegister,而不是簡(jiǎn)單的 Ldar。當(dāng)然,我們還可以檢查我們是否正在訪問(wèn)一個(gè)參數(shù)或寄存器(正或負(fù)偏移量),但這將需要檢查每個(gè)參數(shù)和寄存器訪問(wèn)。顯然這種方法太昂貴了!

          調(diào)用者框架中的參數(shù)

          好的,如果我們?cè)谡{(diào)用者框架中放參數(shù)呢?

          記住如何計(jì)算一個(gè)框架中參數(shù) i 的偏移量:[ai] = 2 + parameter_count - i - 1。如果我們擁有所有參數(shù)(不僅是形式參數(shù)),則偏移量將為[ai] = 2 + parameter_count - i - 1.也就是說(shuō),對(duì)于每個(gè)參數(shù)訪問(wèn),我們都需要加載實(shí)際的參數(shù)計(jì)數(shù)。

          但如果我們反轉(zhuǎn)參數(shù)會(huì)發(fā)生什么呢?現(xiàn)在可以簡(jiǎn)單地將偏移量計(jì)算為[ai] = 2 + i。我們不需要知道棧中有多少個(gè)參數(shù),但如果我們可以保證棧中至少有形參計(jì)數(shù)那么多的參數(shù),那么我們就能一直使用這種方案來(lái)計(jì)算偏移量。

          換句話說(shuō),壓入棧的參數(shù)數(shù)量將始終是參數(shù)數(shù)量和形參數(shù)量之間的最大值,并且在需要時(shí)使用 undefined 對(duì)象進(jìn)行填充。

          這還有另一個(gè)好處!對(duì)于任何 JS 函數(shù),接收器始終位于相同的偏移量處,就在返回地址的正上方:[this] = 2。

          對(duì)于我們的第 1 和第 4 條要求,這是一個(gè)干凈的解決方案。另外兩個(gè)要求又如何呢?我們?nèi)绾螛?gòu)造 rest 參數(shù)和 arguments 對(duì)象?返回調(diào)用者時(shí)如何清理?xiàng)V械膮?shù)?為此,我們?nèi)鄙俚闹皇菂?shù)計(jì)數(shù)而已。我們需要將其保存在某個(gè)地方。只要可以輕松訪問(wèn)此信息即可,具體怎么做沒(méi)那么多限制。兩種基本選項(xiàng)分別是:將其推送到調(diào)用者框架中的接收者之后,或被調(diào)用者框架中的固定標(biāo)頭部分。我們實(shí)現(xiàn)了后者,因?yàn)樗喜⒘?Interpreter 和 Optimized 框架的固定標(biāo)頭部分。


          如果在 V8 v8.9 中運(yùn)行前面的示例,則在 InterpreterArgsThenPush 之后將看到以下棧(請(qǐng)注意,現(xiàn)在參數(shù)已反轉(zhuǎn)):

          內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。

          所有執(zhí)行都遵循類似的路徑,直到到達(dá) InvokeFunctionCode。在這里,我們?cè)谏暾?qǐng)不足的情況下處理參數(shù),根據(jù)需要推送盡可能多的 undefined 對(duì)象。請(qǐng)注意,在申請(qǐng)過(guò)度的情況下,我們不會(huì)進(jìn)行任何更改。最后,我們通過(guò)一個(gè)寄存器將參數(shù)數(shù)量傳遞給被調(diào)用者的 Code。在 x64 的情況下,我們使用寄存器 rax。

          如果被調(diào)用者尚未進(jìn)行優(yōu)化,我們將到達(dá) InterpreterEntryTrampoline,它會(huì)構(gòu)建以下棧框架。

          沒(méi)有參數(shù)適配器的棧框架。

          被調(diào)用者框架有一個(gè)額外的插槽,其中包含的參數(shù)計(jì)數(shù)可用于構(gòu)造 rest 參數(shù)或 arguments 對(duì)象,并在返回到調(diào)用者之前清除棧中參數(shù)。

          返回時(shí),我們修改 LeaveInterpreterFrame 以讀取棧中的參數(shù)計(jì)數(shù),并彈出參數(shù)計(jì)數(shù)和形式參數(shù)計(jì)數(shù)之間的較大數(shù)字。

          TurboFan

          那么代碼優(yōu)化呢?我們來(lái)稍微更改一下初始腳本,以強(qiáng)制 V8 使用 TurboFan 對(duì)其進(jìn)行編譯:

          function add42(x) { return x + 42; }
          function callAdd42() { add42(3); }
          %PrepareFunctionForOptimization(callAdd42);
          callAdd42();
          %OptimizeFunctionOnNextCall(callAdd42);
          callAdd42();

          在這里,我們使用 V8 內(nèi)部函數(shù)來(lái)強(qiáng)制 V8 優(yōu)化調(diào)用,否則 V8 僅在我們的小函數(shù)變熱(經(jīng)常使用)時(shí)才對(duì)其進(jìn)行優(yōu)化。我們?cè)趦?yōu)化之前調(diào)用它一次,以收集一些可用于指導(dǎo)編譯的類型信息。在此處閱讀有關(guān) TurboFan 的更多信息(https://v8.dev/docs/turbofan)。

          這里,我只展示與主題相關(guān)的部分生成代碼。

          movq rdi,0x1a8e082126ad    ;; Load the function object <JSFunction add42>
          push 0x6                   ;; Push SMI 3 as argument
          movq rcx,0x1a8e082030d1    ;; <JSGlobal Object>
          push rcx                   ;; Push receiver (the global proxy object)
          movl rax,0x1               ;; Save the arguments count in rax
          movl rcx,[rdi+0x17]        ;; Load function object {Code} field in rcx
          call rcx                   ;; Finally, call the code object!

          盡管這段代碼使用了匯編來(lái)編寫(xiě),但如果你仔細(xì)看我的注釋?xiě)?yīng)該很容易能懂。本質(zhì)上,在編譯調(diào)用時(shí),TF 需要完成之前在 InterpreterPushArgsThenCall、Call、CallFunction 和 InvokeFunctionCall 內(nèi)置組件中完成的所有工作。它應(yīng)該會(huì)有更多的靜態(tài)信息來(lái)執(zhí)行此操作并發(fā)出更少的計(jì)算機(jī)指令。

          帶參數(shù)適配器框架的 TurboFan

          現(xiàn)在,讓我們來(lái)看看參數(shù)數(shù)量和參數(shù)計(jì)數(shù)不匹配的情況。考慮調(diào)用 add42(1, 2, 3)。它會(huì)編譯為:

          movq rdi,0x4250820fff1    ;; Load the function object <JSFunction add42>
          ;; Push receiver and arguments SMIs 1, 2 and 3
          movq rcx,0x42508080dd5    ;; <JSGlobal Object>
          push rcx
          push 0x2
          push 0x4
          push 0x6
          movl rax,0x3              ;; Save the arguments count in rax
          movl rbx,0x1              ;; Save the formal parameters count in rbx
          movq r10,0x564ed7fdf840   ;; <ArgumentsAdaptorTrampoline>
          call r10                  ;; Call the ArgumentsAdaptorTrampoline

          如你所見(jiàn),不難為 TF 添加對(duì)參數(shù)和參數(shù)計(jì)數(shù)不匹配的支持。只需調(diào)用參數(shù)適配器 trampoline 即可!

          然而這種方法成本很高。對(duì)于每個(gè)優(yōu)化的調(diào)用,我們現(xiàn)在都需要進(jìn)入?yún)?shù)適配器 trampoline,并像未優(yōu)化的代碼一樣處理框架。這就解釋了為什么在優(yōu)化的代碼中移除適配器框架的性能收益比在 Ignition 上大得多。

          但是,生成的代碼非常簡(jiǎn)單。從中返回非常容易(結(jié)尾):

          movq rsp,rbp   ;; Clean callee frame
          pop rbp
          ret 0x8        ;; Pops a single argument (the receiver)

          我們彈出框架并根據(jù)參數(shù)計(jì)數(shù)發(fā)出一個(gè)返回指令。如果實(shí)參計(jì)數(shù)和形參計(jì)數(shù)不匹配,則適配器框架 trampoline 將對(duì)其進(jìn)行處理。

          沒(méi)有參數(shù)適配器框架的 TurboFan

          生成的代碼本質(zhì)上與參數(shù)計(jì)數(shù)匹配的調(diào)用代碼相同。考慮調(diào)用 add42(1, 2, 3)。這將生成:

          movq rdi,0x35ac082126ad    ;; Load the function object <JSFunction add42>
          ;; Push receiver and arguments 1, 2 and 3 (reversed)
          push 0x6
          push 0x4
          push 0x2
          movq rcx,0x35ac082030d1    ;; <JSGlobal Object>
          push rcx
          movl rax,0x3               ;; Save the arguments count in rax
          movl rcx,[rdi+0x17]        ;; Load function object {Code} field in rcx
          call rcx                   ;; Finally, call the code object!

          該函數(shù)的結(jié)尾如何?我們不再回到參數(shù)適配器 trampoline 了,因此結(jié)尾確實(shí)比以前復(fù)雜了一些。

          movq rcx,[rbp-0x18]        ;; Load the argument count (from callee frame) to rcx
          movq rsp,rbp               ;; Pop out callee frame
          pop rbp
          cmpq rcx,0x0               ;; Compare arguments count with formal parameter count
          jg 0x35ac000840c6  <+0x86>
          ;; If arguments count is smaller (or equal) than the formal parameter count:
          ret 0x8                    ;; Return as usual (parameter count is statically known)
          ;; If we have more arguments in the stack than formal parameters:
          pop r10                    ;; Save the return address
          leaq rsp,[rsp+rcx*8+0x8]   ;; Pop all arguments according to rcx
          push r10                   ;; Recover the return address
          retl

          小結(jié)

          參數(shù)適配器框架是一個(gè)臨時(shí)解決方案,用于實(shí)際參數(shù)和形式參數(shù)計(jì)數(shù)不匹配的調(diào)用。這是一個(gè)簡(jiǎn)單的解決方案,但它帶來(lái)了很高的性能成本,并增加了代碼庫(kù)的復(fù)雜性。如今,許多 Web 框架使用這一特性來(lái)創(chuàng)建更靈活的 API,結(jié)果帶來(lái)了更高的性能成本。反轉(zhuǎn)棧中參數(shù)這個(gè)簡(jiǎn)單的想法可以大大降低實(shí)現(xiàn)復(fù)雜性,并消除了此類調(diào)用的幾乎所有開(kāi)銷。

          原文鏈接:

          https://v8.dev/blog/adaptor-frame

          延伸閱讀:

          Deno 2020 年大事記-InfoQ

          關(guān)注我并轉(zhuǎn)發(fā)此篇文章,即可獲得學(xué)習(xí)資料~若想了解更多,也可移步InfoQ官網(wǎng),獲取InfoQ最新資訊~

          家好,很高興又見(jiàn)面了,我是"高級(jí)前端進(jìn)階",由我?guī)е蠹乙黄痍P(guān)注前端前沿、深入前端底層技術(shù),大家一起進(jìn)步,也歡迎大家關(guān)注、點(diǎn)贊、收藏、轉(zhuǎn)發(fā)!

          高級(jí)前端進(jìn)階

          讓 JavaScript 在 WebAssembly 上疾速運(yùn)行

          與二十年前相比,如今 JavaScript 在瀏覽器中的運(yùn)行速度要快好多倍。而這多虧了瀏覽器廠商們?cè)诖似陂g堅(jiān)持不懈地加強(qiáng)性能優(yōu)化。

          而現(xiàn)在,我們又要開(kāi)始在完全不同的運(yùn)行環(huán)境中優(yōu)化 JavaScript 的性能 —— 這些新環(huán)境中的游戲規(guī)則是截然不同的。而讓 JavaScript 能夠適應(yīng)不同運(yùn)行環(huán)境的,正是 WebAssembly。

          這里我們要明確一點(diǎn) —— 如果你是在瀏覽器中運(yùn)行 JavaScript,那么直接部署 JavaScript 就行了。瀏覽器中的 JavaScript 引擎已經(jīng)被精心調(diào)校過(guò),可以很快速地運(yùn)行裝載進(jìn)來(lái)的 JavaScript 程序。

          但如果是在無(wú)服務(wù)器(Serverless)功能中運(yùn)行 JavaScript 呢?又或者說(shuō),如果想要在 iOS 或游戲機(jī)這類不支持通常的即時(shí)編譯的環(huán)境中運(yùn)行 JavaScript,又該如何把控性能?

          在這些使用場(chǎng)景中,你會(huì)需要關(guān)注這新一輪的 JavaScript 優(yōu)化。另外,若想要讓 Python、Ruby 或者 Lua 等其他運(yùn)行時(shí)語(yǔ)言在上述使用場(chǎng)景中提速,JavaScript 優(yōu)化也有參考價(jià)值。

          但在開(kāi)始探索如何在不同環(huán)境中進(jìn)行優(yōu)化前,我們需要了解一下其中的基本原理。

          原理是什么?

          不論你在何時(shí)運(yùn)行 Javascript 程序,JavaScript 代碼終歸要以機(jī)器編碼的形式執(zhí)行。 JavaScript 引擎通過(guò)一系列技術(shù)來(lái)實(shí)現(xiàn)這一轉(zhuǎn)換,例如各種解釋器和 JIT 編譯器。(詳情請(qǐng)參見(jiàn)即時(shí)(JIT)編譯器速成課。)

          但如果你想要運(yùn)行程序的平臺(tái)沒(méi)有 JavaScript 引擎怎么辦?那你就需要把 JavaScript 引擎和程序代碼一起部署。

          為了能讓 JavaScript 隨處運(yùn)行,我們把 JavaScript 引擎部署為一個(gè) WebAssembly 模塊,這樣就能夠跨越不同機(jī)器架構(gòu)之間的差異。而且,借助 WASI,跨操作系統(tǒng)也同樣成為可能。

          這意味著,整個(gè) JavaScript 運(yùn)行環(huán)境被集成進(jìn)了 WebAssembly 實(shí)例中。部署了 WebAssembly 后, 你只需把 JavaScript 代碼喂進(jìn)去就行了,WebAssembly 實(shí)例會(huì)自行消化代碼。

          JavaScript 引擎并不會(huì)直接在機(jī)器內(nèi)存中運(yùn)轉(zhuǎn),從二進(jìn)制碼到二進(jìn)制碼的垃圾回收對(duì)象,JavaScript 引擎把這一切都放到 Wasm 模塊的線性內(nèi)存中。

          對(duì)于 JavaScript 引擎,我們選用了 SpiderMonkey,就是 Firefox 瀏覽器中用到的那個(gè)。SpiderMonkey 是行業(yè)級(jí)別的 JavaScript 虛擬機(jī)(VM)之一,在瀏覽器領(lǐng)域里是久經(jīng)沙場(chǎng)的老將。當(dāng)你運(yùn)行不可信代碼,或者代碼會(huì)處理不可信輸入信息時(shí),這種皮實(shí)耐用、安全性高的特性就顯得尤為重要了。

          SpiderMonkey 還使用了一種叫做精確堆棧掃描的技術(shù),它對(duì)我下面將要說(shuō)到的部分優(yōu)化點(diǎn)極其重要。SpiderMonkey 還具有包容度極高的代碼庫(kù),這一點(diǎn)也很重要,因?yàn)閰f(xié)作開(kāi)發(fā)者們來(lái)自三個(gè)不同的組織 —— Fastly、Mozilla 和 Igalia。

          我剛剛描述的運(yùn)行方式并沒(méi)有顯得具有什么顛覆性特征。幾年前大家就已經(jīng)開(kāi)始這樣用 WebAssembly 運(yùn)行 JavaScript 了。

          但問(wèn)題在于,這樣運(yùn)行很慢。WebAssembly 并不支持動(dòng)態(tài)地生成新的機(jī)器編碼,然后在純 Wasm 代碼里運(yùn)行。這就意味著你無(wú)法使用即時(shí)編譯。你只能使用解釋器。

          知道了有這種局限性,你可能會(huì)問(wèn):

          那為何還要說(shuō)性能優(yōu)化?

          鑒于即時(shí)編譯讓瀏覽器能快速運(yùn)行 JavaScript(且鑒于在 WebAssembly 模塊中不能進(jìn)行即時(shí)編譯),還想提速似乎是反直覺(jué)的。

          但假如,即使不能用即時(shí)編譯,我們還有沒(méi)有辦法能讓 JavaScript 運(yùn)行提速呢?

          讓我們通過(guò)幾個(gè)案例來(lái)看看,如果 WebAssembly 可以快速運(yùn)行 JavaScript,將會(huì)產(chǎn)生多么大的效益。

          在 iOS(以及其他 JIT 受限的環(huán)境)中運(yùn)行 JavaScript

          在有些環(huán)境下,由于安全原因,無(wú)法使用即時(shí)編譯,舉例來(lái)說(shuō),無(wú)特殊權(quán)限的 iOS 應(yīng)用、部分智能電視以及游戲機(jī)設(shè)備都屬于此范疇。

          在這些平臺(tái)上,必須要使用解釋器才行。但想在這些平臺(tái)上運(yùn)行的,都是那種運(yùn)行周期長(zhǎng)、代碼量大的應(yīng)用。正是這些條件讓你不想用解釋器,因?yàn)榻忉屍鲿?huì)嚴(yán)重拖慢執(zhí)行速度

          如果能讓 JavaScript 在這樣的環(huán)境中提速,那么開(kāi)發(fā)者們就可以在不支持即時(shí)編譯的平臺(tái)使用 JavaScript 而無(wú)需顧慮性能了。

          讓無(wú)服務(wù)器即刻冷啟動(dòng)

          在另外一些場(chǎng)景中,即時(shí)編譯不成問(wèn)題,但啟動(dòng)時(shí)間卻拖了后腿,比如在使用無(wú)服務(wù)器功能時(shí)。這就是冷啟動(dòng)延遲的問(wèn)題,你可能已經(jīng)有所耳聞。

          即使用精簡(jiǎn)到極致的 JavaScript 環(huán)境 , 一個(gè)僅啟動(dòng)純 JavaScript 引擎的隔離環(huán)境,最低延遲也有 5 毫秒左右,還沒(méi)有把初始化應(yīng)用的時(shí)間算進(jìn)去。

          倒是有一些辦法可以把收到的請(qǐng)求的啟動(dòng)延遲隱藏起來(lái)。但隨著 QUIC 這類提案在網(wǎng)絡(luò)層中對(duì)連接時(shí)長(zhǎng)的優(yōu)化,想要隱藏延遲越來(lái)越困難。而當(dāng)你鏈?zhǔn)綀?zhí)行多個(gè)無(wú)服務(wù)器功能等這類操作時(shí),要隱藏延遲更是難上加難。

          使用這些技術(shù)去隱藏延遲的平臺(tái)頁(yè),常常會(huì)在多個(gè)請(qǐng)求間復(fù)用實(shí)例。某些情況下,這意味著在不同請(qǐng)求中都可以觀察到全局狀態(tài),這就是拿安全當(dāng)兒戲了。

          正是由于這個(gè)冷啟動(dòng)問(wèn)題,開(kāi)發(fā)者們常常無(wú)法遵循最佳實(shí)踐來(lái)開(kāi)發(fā)。他們會(huì)在一次無(wú)服務(wù)器部署中,塞入大量功能。這就導(dǎo)致了另一個(gè)安全問(wèn)題 ,一處暴雷,全盤(pán)完蛋。如果這次部署中的一部分破防了,那么攻擊者就有了整個(gè)部署的訪問(wèn)權(quán)限。

          但如果能把上述場(chǎng)景中 JavaScript 的啟動(dòng)時(shí)間降到足夠低,那自然就無(wú)需再費(fèi)盡心思去隱藏啟動(dòng)時(shí)間了,因?yàn)槟茉趲孜⒚胫g就啟動(dòng)一個(gè)實(shí)例。

          如果能做到這種程度,就能為每個(gè)請(qǐng)求提供一個(gè)新實(shí)例,于是不會(huì)再有全局狀態(tài)橫穿多個(gè)請(qǐng)求。而且,由于這些實(shí)例足夠輕量,開(kāi)發(fā)者能夠任意把代碼拆分成粒度更細(xì)的片段,把每一段代碼的故障范圍壓縮到最小。

          這種實(shí)現(xiàn)還有另外一個(gè)安全方面的優(yōu)點(diǎn)。除了實(shí)例能保持輕量、代碼隔離粒度更優(yōu)之外,Wasm 引擎能提供的安全壁壘也更堅(jiān)固了。

          JavaScript 引擎過(guò)去用來(lái)創(chuàng)建隔離的代碼庫(kù)龐大無(wú)比,包含著大量用來(lái)進(jìn)行極其復(fù)雜的優(yōu)化工作的底層代碼,所以很容易產(chǎn)生 Bug,從而使得攻擊者跳出虛擬機(jī)、獲取到虛擬機(jī)所在系統(tǒng)的訪問(wèn)權(quán)限。這就是為何像 Chrome 和 Firefox 這樣的瀏覽器要竭盡全力確保網(wǎng)站運(yùn)行在完全隔離的進(jìn)程中。

          相反的是,Wasm 引擎需要的代碼極少,因此便于檢查,而且它們中有許多是用 Rust 這種內(nèi)存無(wú)害語(yǔ)言寫(xiě)的。而由 WebAssembly 模塊生成的原生二進(jìn)制碼,其內(nèi)存隔離的安全性是可以驗(yàn)證的。

          通過(guò)在 Wasm 引擎中運(yùn)行 JavaScript 代碼,構(gòu)筑起了這座安全性更高的外部沙盒堡壘,以此作為另一道防線。

          因此,在上述這些場(chǎng)景中,讓 JavaScript 在 Wasm 引擎上運(yùn)行得更快,是裨益良多的。那我們?cè)趺磥?lái)實(shí)現(xiàn)呢?要回答這個(gè)問(wèn)題,需要弄清楚 JavaScript 引擎把時(shí)間都消磨在哪里了。

          JavaScript 的兩個(gè)耗時(shí)之處

          可以粗略地把 JavaScript 引擎所做的工作拆分為兩個(gè)部分:初始化和運(yùn)行時(shí)。

          把 JavaScript 看作是一個(gè)包工頭。這位包工頭被雇用來(lái)完成這樣一份工作,即運(yùn)行 JavaScript 代碼,并得出結(jié)果。

          初始化階段

          在這位包工頭真正開(kāi)始運(yùn)作項(xiàng)目之前,它需要做一點(diǎn)預(yù)備工作。此初始化階段包括了在執(zhí)行之初所有那些只需運(yùn)行一次的操作

          應(yīng)用初始化

          不論是什么項(xiàng)目,合同工都需要了解一下客戶的需求,然后配置要完成任務(wù)所需的資源。

          例如,合同工瀏覽一遍項(xiàng)目概要以及其他支持文檔,然后把它們轉(zhuǎn)化成自己能處理的東西,比如搭建一個(gè)項(xiàng)目管理系統(tǒng),把所有文檔存儲(chǔ)并整理起來(lái)。

          在 JavaScript 引擎看來(lái),這個(gè)任務(wù)更像是通讀頂層源碼并把各項(xiàng)功能解析為字節(jié)碼、為聲明的變量分配內(nèi)存、給已經(jīng)定義過(guò)的變量賦值。

          引擎初始化

          在無(wú)服務(wù)器等特定場(chǎng)景中,還有另一個(gè)需要初始化的部分,發(fā)生在應(yīng)用初始化之前。

          那就是引擎初始化。引擎本身需要率先啟動(dòng)起來(lái),內(nèi)置函數(shù)需要添加到環(huán)境當(dāng)中。可以把這個(gè)過(guò)程看作在開(kāi)始工作之前要先把辦公室布置好 ,組裝桌椅之類的事。

          這個(gè)過(guò)程也可能花費(fèi)一定量的時(shí)間,也是導(dǎo)致冷啟動(dòng)成為無(wú)服務(wù)器使用場(chǎng)景的大問(wèn)題的原因之一。

          運(yùn)行時(shí)階段

          一旦初始化階段結(jié)束,JavaScript 引擎就能開(kāi)始運(yùn)行代碼了。

          把這部分工作的完成速度稱為吞吐量(Throughput),能影響吞吐量的因素有很多。比如:

          • 功能使用哪種語(yǔ)言開(kāi)發(fā)
          • JavaScript 引擎是否能預(yù)測(cè)代碼行為
          • 使用哪種數(shù)據(jù)結(jié)構(gòu)
          • 代碼的運(yùn)行周期是否足夠長(zhǎng)到能從 JavaScript 引擎的優(yōu)化編譯中獲益

          那么這就是 JavaScript 消耗時(shí)間的兩個(gè)階段。

          那該如何讓這兩個(gè)階段運(yùn)行得更快呢?

          大幅壓縮初始化耗時(shí)

          先使用 Wizer 這個(gè)工具來(lái)加快初始化過(guò)程。稍后我會(huì)解釋如何操作,但為了讓心急的讀者一睹為快,下面先給出運(yùn)行一個(gè)非常簡(jiǎn)單的 JavaScript 應(yīng)用時(shí)的加速情況。

          當(dāng)用 Wizer 運(yùn)行這個(gè)小應(yīng)用時(shí),只消耗了 0.36 毫秒(等于 360 微秒)。這要比純 JavaScript 的方式快了不止 13 倍。

          啟動(dòng)能如此迅速,是因?yàn)榻柚丝煺眨⊿napshot)。Nick Fitzgerald 在 WebAssembly 峰會(huì)上關(guān)于 Wizer 的演講中進(jìn)行了更為詳盡的解釋。

          那么其中的原理是什么?在部署代碼之前,作為構(gòu)建步驟的一部分,用 JavaScript 引擎運(yùn)行 JavaScript 代碼,直到初始化結(jié)束。

          在此處,JavaScript 引擎把所有的 JavaScript 代碼解析成了字節(jié)碼,并存儲(chǔ)在了線性內(nèi)存中。在這一階段,引擎還會(huì)進(jìn)行大量的內(nèi)存分配和初始化工作。

          由于線性內(nèi)存的獨(dú)立完備性非常強(qiáng),當(dāng)所有的數(shù)據(jù)值被存進(jìn)來(lái)后,直接把這塊內(nèi)存綁定為 Wasm 模塊的數(shù)據(jù)區(qū)塊即可。

          當(dāng) JavaScript 引擎模塊被實(shí)例化后,它就能訪問(wèn)數(shù)據(jù)區(qū)塊中的所有數(shù)據(jù)了。當(dāng)引擎需要使用這塊內(nèi)存時(shí),它可以復(fù)制所需的區(qū)塊(或者內(nèi)存頁(yè))到自己的線性內(nèi)存中去。這樣,JavaScript 引擎在啟動(dòng)時(shí)就無(wú)需再做配置工作了。所有的預(yù)初始化的數(shù)據(jù)就都已經(jīng)準(zhǔn)備就緒、聽(tīng)?wèi){差遣了。

          眼下,把這個(gè)數(shù)據(jù)區(qū)塊和 JavaScript 引擎綁在了一起。但在將來(lái),一旦模塊鏈接(Module linking)可用了,就能把數(shù)據(jù)區(qū)塊裝載為一個(gè)單獨(dú)的模塊了,也就能讓 JavaScript 引擎被多個(gè)不同的 JavaScript 應(yīng)用復(fù)用了。

          這樣就實(shí)現(xiàn)了真正干凈清爽的解耦。

          JavaScript 引擎模塊只包含引擎本身的代碼。這意味著一經(jīng)編譯完成,這部分代碼就可以高效率地被多個(gè)不同實(shí)例緩存和復(fù)用了。

          另一方面,特定的應(yīng)用模塊不包含 Wasm 代碼。它只含有線性內(nèi)存,而線性內(nèi)存只含有 JavaScript 代碼字節(jié)碼,以及初始化生成的 JavaScript 引擎狀態(tài)數(shù)據(jù)。這讓內(nèi)存整理和分配十分便利。

          就好像是包工頭 JavaScript 引擎根本不需要再去布置辦公室了。它直接可以拎包入住了。它的包里裝下了整個(gè)辦公室,所有器具一應(yīng)俱全,全部都調(diào)校就緒,就等 JavaScript 引擎破土動(dòng)工了。

          而最酷的就是,這不是特地為 JavaScript 實(shí)現(xiàn)的 —— 只需要使用 WebAssembly 現(xiàn)有的屬性即可。所以你也可以把這個(gè)辦法用在 Python、Ruby、Lua 或其他運(yùn)行時(shí)環(huán)境中。

          下一步:提升吞吐量

          通過(guò)這種方式,可以讓啟動(dòng)時(shí)長(zhǎng)超級(jí)短了,那如何優(yōu)化吞吐量呢?

          對(duì)于某些情況來(lái)說(shuō),吞吐量其實(shí)不算差。如果你的 JavaScript 應(yīng)用運(yùn)行周期非常短,它怎么也輪不到即時(shí)編譯來(lái)處理 —— 它的全程都在解釋器中完成。在這種情況中,吞吐量就和在瀏覽器中一樣了,在傳統(tǒng)的 JavaScript 引擎初始化完成之前,程序就已經(jīng)運(yùn)行完了。

          但是對(duì)于運(yùn)行周期更長(zhǎng)的 JavaScript 代碼,即時(shí)編譯用不了多久就得開(kāi)始介入了。一旦發(fā)生這種情況,吞吐量的差異就開(kāi)始變得懸殊了。

          如上面所言,在純 WebAssembly 環(huán)境中是不可能使用即時(shí)編譯的。但事實(shí)上,可以把即時(shí)編譯的一些想法應(yīng)用到提前編譯模型中。

          快速 AOT 編譯 JavaScript 代碼(無(wú)分析)

          即時(shí)編譯用到的一個(gè)優(yōu)化技術(shù)是內(nèi)聯(lián)緩存(Inline caching)。通過(guò)內(nèi)聯(lián)緩存,即時(shí)編譯創(chuàng)建一個(gè)存根鏈表,其中包含了機(jī)器編碼的快捷路徑,指向曾經(jīng)運(yùn)行過(guò)的 JavaScript 字節(jié)碼的所有運(yùn)行方式。(詳情請(qǐng)參閱文章:即時(shí)編譯器速成課)

          之所以需要用鏈表,是因?yàn)?JavaScript 是動(dòng)態(tài)類型語(yǔ)言。每當(dāng)一行代碼變換了不同的類型,就需要生成一個(gè)新的存根,添加到鏈表中。但如果之前就處理過(guò)這個(gè)類型,那就可以直接使用已經(jīng)生成好的存根。

          由于內(nèi)聯(lián)緩存(IC)在即時(shí)編譯中比較常用,人們會(huì)認(rèn)為它們是非常動(dòng)態(tài)化的,并且專用于特定程序。但實(shí)際上,它們也可以用于 AOT 場(chǎng)景。

          即使還沒(méi)有看到 JavaScript 代碼,也對(duì)要生成的 IC 存根比較熟悉了。這是因?yàn)?JavaScript 中有一些模式是經(jīng)常被使用到的。

          訪問(wèn)對(duì)象屬性就是一個(gè)有力佐證。訪問(wèn)對(duì)象屬性在 JavaScript 中非常常見(jiàn),而使用 IC 存根就能為這個(gè)操作提速。對(duì)于那些有確定“形狀”或者“隱藏類”(即屬性的存儲(chǔ)位置相對(duì)固定)的對(duì)象來(lái)說(shuō),當(dāng)你讀取這類對(duì)象的某個(gè)屬性,該屬性總在同樣的偏移位置(Offset)上。

          按照傳統(tǒng),即時(shí)編譯中的這種 IC 存根會(huì)硬編碼為兩種值:一個(gè)是指向形狀的指針,一個(gè)是屬性的偏移量。而這所需的信息,是提前預(yù)知不到的。但能做的是把 IC 存根參數(shù)化。可以把形狀和屬性偏移量看作是傳到存根里的變量。

          這樣,就能創(chuàng)建出一個(gè)單獨(dú)的存根,它從內(nèi)存中加載值,然后可以到處使用這個(gè)存根。可以把屬于常見(jiàn)模式的所有存根合成一個(gè) AOT 編譯模塊,不去關(guān)心 JavaScript 代碼的具體功能細(xì)節(jié)。即使在瀏覽器設(shè)置中,這種 IC 共享也是有益處的,因?yàn)檫@讓 JavaScript 引擎生成更少的機(jī)器編碼,提升啟動(dòng)速度,優(yōu)化本地指令緩存。

          對(duì)于我們的使用場(chǎng)景來(lái)說(shuō),IC 共享尤其重要。它意味著可以把屬于常見(jiàn)模式的所有存根合成一個(gè) AOT 編譯模塊,不去關(guān)心 JavaScript 代碼的具體實(shí)現(xiàn)細(xì)節(jié)。

          我們發(fā)現(xiàn),僅需幾 KB 的 IC 存根,就能覆蓋全部 JavaScript 代碼中的絕大部分。例如,只需 2 KB 的 IC 存根,就足以覆蓋 Google Octane 基準(zhǔn)測(cè)試中 95% 的 JavaScript 代碼。從初步測(cè)試結(jié)果來(lái)看,通常的網(wǎng)頁(yè)瀏覽場(chǎng)景似乎都能保持這個(gè)比率。

          因此,使用這種優(yōu)化手段,我們應(yīng)該能夠達(dá)到早期即時(shí)編譯的吞吐量水平。一旦我們做到這個(gè)程度,我們就將加入更細(xì)粒度的優(yōu)化,進(jìn)一步打磨性能,正如各個(gè)瀏覽器廠商的 JavaScript 引擎開(kāi)發(fā)團(tuán)隊(duì)在早期即時(shí)編譯中所做的那樣。

          下一步:或許該加一點(diǎn)分析?

          以上是能提前做的,無(wú)需知道程序是做什么的,也無(wú)需知道它都使用了什么類型的數(shù)據(jù)。但要是能像即時(shí)編譯一樣訪問(wèn)到分析數(shù)據(jù)呢?那就可以全面優(yōu)化代碼了。

          但這會(huì)引出一個(gè)問(wèn)題 ,開(kāi)發(fā)者分析起自己的代碼來(lái)往往十分困難。要想提取出有代表性的代碼樣本,實(shí)非易事。因此沒(méi)法確定是否能得到優(yōu)質(zhì)的分析數(shù)據(jù)。

          如果能找合適的工具來(lái)進(jìn)行分析,那么還是有可能讓 JavaScript 代碼運(yùn)行得像如今的即時(shí)編譯一樣快速(連熱身的時(shí)間都不需要!)的。

          如今該如何上手?

          這種新的方式讓我們激動(dòng)不已,期盼著能更上一層樓。也很激動(dòng)地看到,其他動(dòng)態(tài)類型語(yǔ)言可以用這種方式擁抱 WebAssembly 了。

          因此,下面是有幾種上手的方式,如果有任何問(wèn)題,可以在 Zulip 中提問(wèn)。

          對(duì)于其他想支持 JavaScript 的平臺(tái)

          要想在自己的平臺(tái)運(yùn)行 JavaScript,你需要嵌入一個(gè)支持 WASI 的 WebAssembly 引擎,比如Wasmtime。

          然后需要 JavaScript 引擎。在這一步里,我們?yōu)?Mozilla 的構(gòu)建系統(tǒng)添加了對(duì)編譯 SpiderMonkey 到 WASI 的完全支持。Mozilla 將把 SpiderMonkey 的 WASI 構(gòu)建添加到用于構(gòu)建和測(cè)試 Firefox 的 CI 設(shè)置中。這讓 WASI 成為了 SpiderMonkey 的線上質(zhì)量目標(biāo),確保了 WASI 構(gòu)建能夠一直保持運(yùn)轉(zhuǎn)。這意味著可以如文中所講的那樣使用 SpiderMonkey。

          最后,需要讓用戶提供預(yù)先初始化的 JavaScript 代碼。為了能助你一臂之力,我們還開(kāi)源了 Wizer,可以集成到構(gòu)建工具中,產(chǎn)出針對(duì)特定應(yīng)用的 WebAssembly 模塊,以適用于 JavaScript 引擎模塊所用的預(yù)先初始化內(nèi)存。

          對(duì)于其他想要使用這種方法的語(yǔ)言

          如果是 Python、Ruby、Lua 等語(yǔ)言的使用者,可以針對(duì)該語(yǔ)言構(gòu)建出一個(gè)自己的版本。

          首先,需要把運(yùn)行時(shí)編譯成 WebAssembly,使用 WASI 作為系統(tǒng)調(diào)用,可參考我們對(duì) SpiderMonkey 的處理。然后,可以按照上文所說(shuō),把 Wizer 集成到構(gòu)建工具中,生成內(nèi)存快照,這樣就能用快照來(lái)加速啟動(dòng)。

          參考資料

          原文鏈接:https://bytecodealliance.org/articles/making-javascript-run-fast-on-webassembly

          原文作者:Lin Clark

          中文參考翻譯:https://juejin.cn/post/6981685894470172679


          主站蜘蛛池模板: 国产福利电影一区二区三区久久久久成人精品综合 | 一区二区三区免费在线视频 | 美女免费视频一区二区三区| 国产精品毛片一区二区三区| 亚洲熟女www一区二区三区 | 麻豆国产在线不卡一区二区 | 无码av免费毛片一区二区| 真实国产乱子伦精品一区二区三区| 精品国产一区二区三区久久影院 | 亚洲国产精品一区二区九九| 国产精品无码一区二区三区毛片| 日韩精品无码中文字幕一区二区| 丝袜人妻一区二区三区| 一区二区三区免费在线视频| 免费国产在线精品一区| 国产丝袜一区二区三区在线观看| 无码中文人妻在线一区| 日本无码一区二区三区白峰美| 精品人妻码一区二区三区| 人妻无码一区二区三区免费| 文中字幕一区二区三区视频播放| 亚洲乱色熟女一区二区三区丝袜| 综合无码一区二区三区| 亚洲一区二区三区AV无码| 日韩亚洲一区二区三区| 无码国产精品一区二区免费3p | 日本免费一区尤物| 国产在线一区视频| 国产福利电影一区二区三区久久老子无码午夜伦不 | 精品视频一区二区三区在线观看| 国产美女口爆吞精一区二区| 亚洲一区二区视频在线观看| 视频一区精品自拍| 久久综合亚洲色一区二区三区| 亚洲午夜一区二区三区| 欧美激情国产精品视频一区二区| 国模一区二区三区| V一区无码内射国产| 国产亚洲福利精品一区| 2020天堂中文字幕一区在线观 | 色系一区二区三区四区五区|