Ⅰ 編譯原理
編譯原理是計算機科學中的一慎昌門重要課程,主要研究如段配何將高級程序語言轉化為機器語言寬燃扒的過程。它涉及到多個領域,如語言學、數學、計算機硬體和操作系統等。編譯器是實現這一過程的關鍵工具,它可以將程序源代碼轉化為可執行的機器代碼。
Ⅱ 編譯原理
C語言編譯過程詳解
C語言的編譯鏈接過程是要把我們編寫的一個C程序(源代碼)轉換成可以在硬體上運行的程序(可執行代碼),需要進行編譯和鏈接。編譯就是把文本形式源代碼翻譯為機器語言形式的目標文件的過程。鏈接是把目標文件、操作系統的啟動代碼和用到的庫文件進行組織形成最終生成可執行代碼的過程。過程圖解如下:
從圖上可以看到,整個代碼的編譯過程分為編譯和鏈接兩個過程,編譯對應圖中的大括弧括起的部分,其餘則為鏈接過程。
一、編譯過程
編譯過程又可以分成兩個階段:編譯和匯編。
1、編譯
編譯是讀取源程序(字元流),對之進行詞法和語法的分析,將高級語言指令轉換為功能等效的匯編代碼,源文件的編譯過程包含兩個主要階段:
第一個階段是預處理階段,在正式的編譯階段之前進行。預處理階段將根據已放置在文件中的預處理指令來修改源文件的內容。如#include指令就是一個預處理指令,它把頭文件的內容添加到.cpp文件中。這個在編譯之前修改源文件的方式提供了很大的靈活性,以適應不同的計算機和操作系統環境的限制。一個環境需要的代碼跟另一個環境所需的代碼可能有所不同,因為可用的硬體或操作系統是不同的。在許多情況下,可以把用於不同環境的代碼放在同一個文件中,再在預處理階段修改代碼,使之適應當前的環境。
主要是以下幾方面的處理:
(1)宏定義指令,如 #define a b。
對於這種偽指令,預編譯所要做的是將程序中的所有a用b替換,但作為字元串常量的 a則不被替換。還有 #undef,則將取消對某個宏的定義,使以後該串的出現不再被替換。
(2)條件編譯指令,如#ifdef,#ifndef,#else,#elif,#endif等。
這些偽指令的引入使得程序員可以通過定義不同的宏來決定編譯程序對哪些代碼進行處理。預編譯程序將根據有關的文件,將那些不必要的代碼過濾掉
(3) 頭文件包含指令,如#include "FileName"或者#include <FileName>等。
在頭文件中一般用偽指令#define定義了大量的宏(最常見的是字元常量),同時包含有各種外部符號的聲明。採用頭文件的目的主要是為了使某些定義可以供多個不同的C源程序使用。因為在需要用到這些定義的C源程序中,只需加上一條#include語句即可,而不必再在此文件中將這些定義重復一遍。預編譯程序將把頭文件中的定義統統都加入到它所產生的輸出文件中,以供編譯程序對之進行處理。包含到C源程序中的頭文件可以是系統提供的,這些頭文件一般被放在/usr/include目錄下。在程序中#include它們要使用尖括弧(<>)。另外開發人員也可以定義自己的頭文件,這些文件一般與C源程序放在同一目錄下,此時在#include中要用雙引號("")。
(4)特殊符號,預編譯程序可以識別一些特殊的符號。
例如在源程序中出現的LINE標識將被解釋為當前行號(十進制數),FILE則被解釋為當前被編譯的C源程序的名稱。預編譯程序對於在源程序中出現的這些串將用合適的值進行替換。
預編譯程序所完成的基本上是對源程序的「替代」工作。經過此種替代,生成一個沒有宏定義、沒有條件編譯指令、沒有特殊符號的輸出文件。這個文件的含義同沒有經過預處理的源文件是相同的,但內容有所不同。下一步,此輸出文件將作為編譯程序的輸出而被翻譯成為機器指令。
第二個階段編譯、優化階段。經過預編譯得到的輸出文件中,只有常量;如數字、字元串、變數的定義,以及C語言的關鍵字,如main,if,else,for,while,{,}, +,-,*,\等等。
編譯程序所要作得工作就是通過詞法分析和語法分析,在確認所有的指令都符合語法規則之後,將其翻譯成等價的中間代碼表示或匯編代碼。
優化處理是編譯系統中一項比較艱深的技術。它涉及到的問題不僅同編譯技術本身有關,而且同機器的硬體環境也有很大的關系。優化一部分是對中間代碼的優化。這種優化不依賴於具體的計算機。另一種優化則主要針對目標代碼的生成而進行的。
對於前一種優化,主要的工作是刪除公共表達式、循環優化(代碼外提、強度削弱、變換循環控制條件、已知量的合並等)、復寫傳播,以及無用賦值的刪除,等等。
後一種類型的優化同機器的硬體結構密切相關,最主要的是考慮是如何充分利用機器的各個硬體寄存器存放的有關變數的值,以減少對於內存的訪問次數。另外,如何根據機器硬體執行指令的特點(如流水線、RISC、CISC、VLIW等)而對指令進行一些調整使目標代碼比較短,執行的效率比較高,也是一個重要的研究課題。
2、匯編
匯編實際上指把匯編語言代碼翻譯成目標機器指令的過程。對於被翻譯系統處理的每一個C語言源程序,都將最終經過這一處理而得到相應的目標文件。目標文件中所存放的也就是與源程序等效的目標的機器語言代碼。目標文件由段組成。通常一個目標文件中至少有兩個段:
代碼段:該段中所包含的主要是程序的指令。該段一般是可讀和可執行的,但一般卻不可寫。
數據段:主要存放程序中要用到的各種全局變數或靜態的數據。一般數據段都是可讀,可寫,可執行的。
UNIX環境下主要有三種類型的目標文件:
(1)可重定位文件
其中包含有適合於其它目標文件鏈接來創建一個可執行的或者共享的目標文件的代碼和數據。
(2)共享的目標文件
這種文件存放了適合於在兩種上下文里鏈接的代碼和數據。
第一種是鏈接程序可把它與其它可重定位文件及共享的目標文件一起處理來創建另一個 目標文件;
第二種是動態鏈接程序將它與另一個可執行文件及其它的共享目標文件結合到一起,創建一個進程映象。
(3)可執行文件
它包含了一個可以被操作系統創建一個進程來執行之的文件。匯編程序生成的實際上是第一種類型的目標文件。對於後兩種還需要其他的一些處理方能得到,這個就是鏈接程序的工作了。
二、鏈接過程
由匯編程序生成的目標文件並不能立即就被執行,其中可能還有許多沒有解決的問題。
例如,某個源文件中的函數可能引用了另一個源文件中定義的某個符號(如變數或者函數調用等);在程序中可能調用了某個庫文件中的函數,等等。所有的這些問題,都需要經鏈接程序的處理方能得以解決。
鏈接程序的主要工作就是將有關的目標文件彼此相連接,也即將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,使得所有的這些目標文件成為一個能夠被操作系統裝入執行的統一整體。
根據開發人員指定的同庫函數的鏈接方式的不同,鏈接處理可分為兩種:
(1)靜態鏈接
在這種鏈接方式下,函數的代碼將從其所在地靜態鏈接庫中被拷貝到最終的可執行程序中。這樣該程序在被執行時這些代碼將被裝入到該進程的虛擬地址空間中。靜態鏈接庫實際上是一個目標文件的集合,其中的每個文件含有庫中的一個或者一組相關函數的代碼。
(2) 動態鏈接
在此種方式下,函數的代碼被放到稱作是動態鏈接庫或共享對象的某個目標文件中。鏈接程序此時所作的只是在最終的可執行程序中記錄下共享對象的名字以及其它少量的登記信息。在此可執行文件被執行時,動態鏈接庫的全部內容將被映射到運行時相應進程的虛地址空間。動態鏈接程序將根據可執行程序中記錄的信息找到相應的函數代碼。
對於可執行文件中的函數調用,可分別採用動態鏈接或靜態鏈接的方法。使用動態鏈接能夠使最終的可執行文件比較短小,並且當共享對象被多個進程使用時能節約一些內存,因為在內存中只需要保存一份此共享對象的代碼。但並不是使用動態鏈接就一定比使用靜態鏈接要優越。在某些情況下動態鏈接可能帶來一些性能上損害。
我們在linux使用的gcc編譯器便是把以上的幾個過程進行捆綁,使用戶只使用一次命令就把編譯工作完成,這的確方便了編譯工作,但對於初學者了解編譯過程就很不利了,下圖便是gcc代理的編譯過程:
從上圖可以看到:
預編譯
將.c 文件轉化成 .i文件
使用的gcc命令是:gcc –E
對應於預處理命令cpp
編譯
將.c/.h文件轉換成.s文件
使用的gcc命令是:gcc –S
對應於編譯命令 cc –S
匯編
將.s 文件轉化成 .o文件
使用的gcc 命令是:gcc –c
對應於匯編命令是 as
鏈接
將.o文件轉化成可執行程序
使用的gcc 命令是: gcc
對應於鏈接命令是 ld
總結起來編譯過程就上面的四個過程:預編譯、編譯、匯編、鏈接。了解這四個過程中所做的工作,對我們理解頭文件、庫等的工作過程是有幫助的,而且清楚的了解編譯鏈接過程還對我們在編程時定位錯誤,以及編程時盡量調動編譯器的檢測錯誤會有很大的幫助的。
Ⅲ vue3中的編譯器原理和優化策略
學習目標編譯器原理
vue3編譯過程剖析
vue3編譯優化策略
在初始化之前可能有編譯的過程,最終的產物是個渲染函數,我們知道渲染函數返回的值是一個虛擬DOM(vnode),那麼這個虛擬DOM在我們後續的更新過程中到底有什麼作用呢?我們今天就來探討一下。
編譯器原理1.概念廣義上的編譯原理:編譯器是將源代碼轉化成機器碼的軟體;所以編譯的過程則是將源代碼轉化成機器碼的過程,也就是cpu可執行的二進制代碼。例如使用高級語言java編寫的程序需要編譯成我們看不懂但計算機能看懂的的位元組碼。
如果了解過編譯器的工作流程的同學應該知道,一個完整的編譯器的工作流程會是這樣:
首先,parse解析原始代碼字元串,生成抽象語法樹AST。
其次,transform轉化抽象語法樹,讓它變成更貼近目標「DSL」的結構。
最後,codegen根據轉化後的抽象語法樹生成目標「DSL」的可執行代碼。
2.vue中的編譯在vue里也有編譯的過程,我們經常寫的那個HTML模版,在真正工作的時候,並不是那個HTML模版,它實際上是一個渲染函數,在這個過程中就發生了轉換,也就是編譯,也就是那個字元串的模版最終會變成一個JS函數,叫render函數。所以在這個過程中我們就需要引入編譯器的概念。在計算機中當一種東西從一種形態到另一種形態進行轉換的時候,就需要編譯。編譯器:用來將模板字元串編譯成為JavaScript渲染函數的代碼
那麼vue中的編譯發生在什麼時候呢?
這個時候我們就需要進一步了解vue包的不同版本的不同功能了。vue有攜帶編譯器和不攜帶編譯的包(對不同構建版本的解釋)。
3.運行時編譯在使用攜帶編譯器(compiler)的vue包的時候,vue編譯的時刻是發生在掛載($mount)的時候。
4.運行時不編譯如果使用未攜帶編譯器的vue包的時候,vue在運行時是不會進行編譯的。那麼它的編譯又發生在什麼時候呢?使用未攜帶編譯器的vue包的時候,需要進行預編譯,也就是基於構建工具使用,就是我們平時使用的vue-cli進行構建的項目,就是使用webpack調用vue-loader進行預編譯,將所有vue文件,就是SFC,將裡面的template模版部分轉換成render函數。這樣做的好處就是vue的包體積變小了,執行的時候速度更快了,因為不需要進行編譯了。
vue編譯器原理簡單來說就是:先將template模版轉換成ast抽象語法樹,ast再轉換成渲染函數render。
那麼什麼是是ast抽象語法樹呢?
1.ast抽象語法樹在template模版和render函數之間有一個中間產物叫做ast抽象語法樹。它就是個js對象,它能夠描述當前模版的結構信息,跟vnode很類似。注意,ast只是程序運行過程中編譯產生的,它跟我們最終程序的運行是沒有任何關系的。也就是當這個渲染函數生成之後,ast的生命周期就結束了,不再需要了,而那個虛擬DOM則伴隨整個程序的生命周期。這個就是ast和虛擬DOM的本質區別。
2.為什麼需要ast呢在ast轉換成render函數的過程中,需要進行特別的操作。第一次,將template轉成的ast是個非常粗糙的js對象,是一次非常粗糙的轉換,類似正則表達式的匹配,然後我們的template模版中還有很多表達式,指令,事件需要重新解析,經過這些具體的深加工的解析(transform)之後會得到一個終極ast,然後這個對這個終極ast進行generate,生成render函數
template=>ast=>transform=>ast=>render3.mini版vue編譯器下面我們來看一個mini版的vue編譯器,具體代碼已省略,具體代碼我已經放在Github上了:mini-vue-compiler
functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)大概有以上操作,其中parse函數就是發生在把template轉換成ast的這過程,具體是通過一些正則表達式的匹配template中的字元串。比如將
xxx轉成ast對象,那麼就是通過正則表達式匹配如果是
那麼就設置一個開始標記,再往後面匹配到xxx內容,然後就設置一個子元素,最後匹配到那麼就設置一個結束標記,以此類推。parse解析之後得到的是一個粗糙的ast對象。經過parse解析得到一個粗糙的ast對象之後,就用transform進行深加工,最後要經過generate生成代碼。
Vue3編譯過程剖析掛載的時候先把template編譯成render函數,在創建實例之後,直接調用組件實例的render函數創建這個組件的真實DOM,然後繼續向下做遞歸。
1.vue2.x和vue3.x的編譯對比Vue2.x中的Compile過程會是這樣:
parse詞法分析,編譯模板生成原始粗糙的AST。
optimize優化原始AST,標記ASTElement為靜態根節點或靜態節點。
generate根據優化後的AST,生成可執行代碼,例如_c、_l之類的。
在Vue3中,整體的Compile過程仍然是三個階段,但是不同於Vue2.x的是,第二個階段換成了正常編譯器都會存在的階段transform。
parse詞法分析,編譯模板生成原始粗糙的AST。
transform遍歷AST,對每一個ASTelement進行轉化,例如文本元素、指令元素、動態元素等等的轉化
generate根據優化後的AST,生成可執行代碼函數。
2.源碼編譯入口我們先從一個入口來開始我們的源碼閱讀,packages/vue/index.ts。
//web平台特有編譯函數functioncompileToFunction(template:string|HTMLElement,options?:CompilerOptions):RenderFunction{//省略...if(template[0]==='#'){//獲取模版內容constel=document.querySelector(template)//省略...template=el?el.innerHTML:''}//編譯const{code}=compile(template,extend({//省略...},options))constrender=(__GLOBAL__?newFunction(code)():newFunction('Vue',code)(runtimeDom))asRenderFunction//省略...return(compileCache[key]=render)}//注冊編譯函數registerRuntimeCompiler(compileToFunction)export{compileToFunctionascompile}這個入口文件的代碼比較簡單,只有一個compileToFunction函數,但函數體內的內容卻又比較關鍵,主要是經歷以下步驟:
依賴注入編譯函數至(compileToFunction)
runtime調用編譯函數compileToFunction
調用compile函數
返回包含code的編譯結果
將code作為參數傳入Function的構造函數將生成的函數賦值給render變數
將render函數作為編譯結果返回
3.template獲取app.mount()獲取了templatepackages/runtime-dom/src/index.ts
compile將傳?template編譯為render函數,packages/runtime-core/src/component.ts
實際執?的是baseCompile,packages/compiler-core/src/compile.ts
第?步解析-parse:解析字元串template為抽象語法樹ast
第?步轉換-transform:解析屬性、樣式、指令等
第三步?成-generate:將ast轉換為渲染函數
Vue3編譯器優化策略這是一個非常典型的用內存換時間的操作
1.靜態節點提升<div><div>{{msg}}</div><p>coboy</p><p>coboy</p><p>coboy</p></div>以上這個段template如果沒有開啟靜態節點提升它編譯後是這樣的:
import{toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",null,_toDisplayString(_ctx.msg),1/*TEXT*/),_createVNode("p",null,"coboy"),_createVNode("p",null,"coboy"),_createVNode("p",null,"coboy")]))}如果開啟了靜態節點提升之後它編譯後則是這樣的:
import{toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"const_hoisted_1=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)const_hoisted_2=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)const_hoisted_3=/*#__PURE__*/_createVNode("p",null,"coboy",-1/*HOISTED*/)exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",null,_toDisplayString(_ctx.msg),1/*TEXT*/),_hoisted_1,_hoisted_2,_hoisted_3]))}我們可以看到template里存在大量的不會變的p標簽,所以當這個組件重新渲染的時候,這些靜態的不會變的標簽就不應該再次創建了。所以vue3就把這些靜態的不會變的標簽的VNode放在了render函數作用域的外面,在下次render函數再次執行的時候,那些靜態標簽的VNode已經在內存里了,不需要重新創建了。相當於佔用當前機器的內存,避免重復創建VNode,用內存來換時間。大家仔細斟酌一番靜態提升的字眼,靜態二字我們可以不看,但是提升二字,直抒本意地表達出它(靜態節點)被提高了。
2.補丁標記和動態屬性記錄<div><div:title="title">coboy</div></div>意思就是在編譯的過程中,像人眼一樣對模版進行掃描看哪些東西是動態的,然後提前把這些動態的東西提前保存起來,作個標記和記錄,等下次更新的時候,只更新這些保存起來的動態的記錄。比如上面模版的title是動態的,提前做個標記和記錄,更新的時候就只更新title部分的內容。
import{createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",{title:_ctx.title},"coboy",8/*PROPS*/,["title"])]))}<div><div:title="title">{{text}}</div></div>import{toDisplayStringas_toDisplayString,createVNodeas_createVNode,openBlockas_openBlock,createBlockas_createBlock}from"vue"exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){return(_openBlock(),_createBlock("div",null,[_createVNode("div",{title:_ctx.title},_toDisplayString(_ctx.text),9/*TEXT,PROPS*/,["title"])]))}我們可以觀察到在_createVNode函數的第四個參數是個9,後面是一個注釋:/TEXT,PROPS/,這個是表示在當前的節點裡面有兩個東西是動態的,一個是內部的文本,一個是屬性,然後具體是哪個屬性,在第五個參數的數組裡面則記錄了下來["title"],有個title的屬性是動態的。
在將來進行patch更新的時候,就可以根據當前記錄的信息,進行更新,縮減更新過程和操作,可以非常精確地只進行title和文本的更新。
如果div標簽里是靜態文本的話,_createVNode函數的第四個參數則變成了8,後面的注釋變成了:/PROPS/,後面的第五個參數數據不變。
_createVNode函數的第四個參數的數字其實是一個二進制數字轉成十進制的數字。
8的二進制是1000,9的二進制是1001,很容易可以看出二進制的每一位的數字都代表著特殊的含義。這些數字就是patchFlag,那麼什麼是patchFlag呢?
什麼是patchFlagpatchFlag是complier時的transform階段解析ASTElement打上的補丁標記。它會為runtime時的patchVNode提供依據,從而實現靶向更新VNode和靜態提升的效果。
patchFlag被定義為一個數字枚舉類型,它的每一個枚舉值對應的標識意義是:
TEXT=1動態文本的元素
CLASS=2動態綁定class的元素
STYLE=4動態綁定style的元素
PROPS=8動態props的元素,且不含有class、style綁定
FULL_PROPS=16動態props和帶有key值綁定的元素
HYDRATE_EVENTS=32事件監聽的元素
STABLE_FRAGMENT=64子元素的訂閱不會改變的Fragment元素
KEYED_FRAGMENT=128自己或子元素帶有key值綁定的Fragment元素
UNKEYED_FRAGMENT=256沒有key值綁定的Fragment元素
NEED_PATCH=512帶有ref、指令的元素
DYNAMIC_SLOTS=1024動態slot的組件元素
HOISTED=-1靜態的元素
BAIL=-2不是render函數生成的一些元素,例如renderSlot
整體上patchFlag的分為兩大類:
當patchFlag的值大於0時,代表所對應的元素在patchVNode時或render時是可以被優化生成或更新的
當patchFlag的值小於0時,代表所對應的元素在patchVNode時,是需要被fulldiff,即進行遞歸遍歷VNodetree的比較更新過程。
以上就是vue3的一個非常高效的優化策略叫補丁標記和動態屬性記錄。
3.緩存事件處理程序functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)0將來框架會像react那樣把@click="onClick"變成@click="()=>onClick()",最後可能是這樣的一個箭頭函數。那就意味著每次onClick的函數都是一個全新的函數,那就會造成這個回調函數明明沒有變,都會被認為變了,那就必須進行一系列的更新,那麼如果能把這個回調函數緩存起來,更新的時候,就不要再創建了。
未進行緩存事件處理程序之前的編譯
functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)1進行緩存事件處理程序之後的編譯
functiontokenizer(input){...}functionparse(template){consttokens=tokenizer(template)...}functiontransform(ast){...}functiontraverse(ast,context){...}functiongenerate(ast){...}functioncompile(template){//1.解析constast=parse(template)console.log(JSON.stringify(ast,null,2))//2.轉換transform(ast)//3.生成constcode=generate(ast)console.log(code)//returnfunctionrender(ctx){//returnh("h3",{},//ctx.title//)}returnnewFunction(code)()}lettmpl=`<h3>{{title}}</h3>`compile(tmpl)24.塊block這是什麼意思呢?根據尤雨溪本人的解析,他說,根據他的統計那個動態的部分最多隻有三分之一,基本上都是靜態部分,所以在編譯的過程中,能不能發現那個比較小的動態部分,把它放到比較靠上