得物自研DScript2.0腳本能力從0到1演進(jìn)
一、前言
在高并發(fā)推薦引擎場(chǎng)景中,C++的極致性能往往以開發(fā)效率為妥協(xié),尤其在業(yè)務(wù)頻繁迭代時(shí),C++的開發(fā)效率流程成為顯著瓶頸。傳統(tǒng)嵌入式腳本(如Lua)雖支持動(dòng)態(tài)加載,但其與C++的交互成本(如虛擬棧數(shù)據(jù)中轉(zhuǎn)、類型轉(zhuǎn)換)仍會(huì)帶來(lái)額外性能損耗。
為此,我們探索設(shè)計(jì)DScript2.0——一種與C++內(nèi)存布局及調(diào)用約定深度兼容的動(dòng)態(tài)腳本語(yǔ)言,通過自研編譯器實(shí)現(xiàn)即時(shí)編譯與無(wú)縫嵌入,嘗試在保留腳本靈活性的同時(shí),盡可能貼近C++的原生性能,為性能與效率的平衡提供了輕量化解決方案。
二、動(dòng)態(tài)腳本在引擎中的引用
C++引擎的迭代效率瓶頸
在搜推引擎中的實(shí)踐中,出于對(duì)高并發(fā)場(chǎng)景下極致性能的追求,使用C++進(jìn)行引擎自研成為了一種業(yè)界常態(tài)。
眾所周知,C++通過開放底層控制權(quán)限(如內(nèi)存分配,指令優(yōu)化等),提升了可達(dá)的性能上限,但這種提升伴隨了大量底層細(xì)節(jié)的處理,消耗了更多的開發(fā)時(shí)間,追求性能優(yōu)先的同時(shí),卻又限制了開發(fā)效率。
我們希望能夠在保持性能的同時(shí),提升引擎的開發(fā)效率。
利用嵌入式腳本提升迭代效率
我們的目標(biāo)是尋求一種平衡性能與迭代效率的方案,一種主流方案是在C++中嵌入腳本語(yǔ)言。例如,在游戲引擎和Nginx開發(fā)中集成Lua,在C/C++代碼中實(shí)現(xiàn)性能需求,結(jié)合腳本代碼中實(shí)現(xiàn)控制邏輯,從而提升開發(fā)效率。
嵌入式腳本對(duì)迭代效率的提升
- 支持動(dòng)態(tài)加載,無(wú)需編譯部署。
- 無(wú)需C/C++經(jīng)驗(yàn),腳本學(xué)習(xí)成本低,提升參與迭代的人力總量。
引擎的迭代拆解
- 引擎內(nèi)部的技術(shù)性迭代
- 業(yè)務(wù)側(cè)的需求支持
業(yè)務(wù)側(cè)的需求非常適合引入嵌入式腳本,實(shí)現(xiàn)對(duì)易變需求的自迭代,提升開發(fā)效率,這也是一種業(yè)界主流方案。例如,一些搜索中臺(tái)中,對(duì)于相關(guān)性和粗排邏輯封裝為插件,業(yè)務(wù)側(cè)的算法工程師使用Lua開發(fā)計(jì)算邏輯,可以極大地提升迭代效率。
嵌入式腳本的額外性能開銷
在引擎中嵌入腳本,雖然可以提升迭代效率,但并非全無(wú)代價(jià),高階語(yǔ)言與低階語(yǔ)言的交互存在著額外的性能開銷。
例如,Lua和C++的交互機(jī)制基于Lua提供的虛擬棧來(lái)實(shí)現(xiàn),這個(gè)棧是兩者進(jìn)行數(shù)據(jù)交換的核心通道。
使用虛擬棧實(shí)現(xiàn)語(yǔ)言交互存在額外的開銷,包括但不限于壓棧和彈棧操作、棧空間管理、類型檢查和轉(zhuǎn)換、復(fù)雜數(shù)據(jù)結(jié)構(gòu)的處理等。
圖片
更加極致的方案
基于以上的瓶頸,我們期望一種更加極致的方案,實(shí)現(xiàn)性能與效率的平衡。
嵌入式腳本的額外性能開銷
(主要源于兩種語(yǔ)言在ABI層面的不一致)
- 函數(shù)調(diào)用約定不一致,需要一個(gè)虛擬棧進(jìn)行中轉(zhuǎn)。
- 數(shù)據(jù)類型內(nèi)存布局不一致,需要額外的檢查和轉(zhuǎn)換。
一個(gè)直觀的解決方案就是我們?cè)O(shè)計(jì)一種編程語(yǔ)言,在底層實(shí)現(xiàn)上與C++具有一致內(nèi)存布局與調(diào)用約定,從而消除額外的轉(zhuǎn)換開銷。
同時(shí),這種編程語(yǔ)言可以在C++嵌入,也支持即時(shí)編譯,提升效率的同時(shí),也擁有與原生C++近似的執(zhí)行性能。
以上是我們規(guī)劃DScript2.0項(xiàng)目初衷。
二、DScript2.0的編譯器實(shí)現(xiàn)
語(yǔ)法設(shè)計(jì)
DScript2.0被設(shè)計(jì)為一種輕量級(jí)面向過程的編程語(yǔ)言,同時(shí)它也是靜態(tài)類型的編譯語(yǔ)言。
在語(yǔ)法支持上,包含了基礎(chǔ)數(shù)據(jù)類型、變量、運(yùn)算符、控制流和函數(shù),額外支持了與C++的語(yǔ)言互操作。
數(shù)據(jù)類型 | int,long,bool,float,double,void |
變量 | 自定義變量,隱式類型轉(zhuǎn)換。 |
C++變量:支持訪問和操作外部注冊(cè)的C++變量,支持C++的結(jié)構(gòu)體部分操作。 | |
運(yùn)算符 | 算術(shù)運(yùn)算符:+,-,*,/,% |
關(guān)系運(yùn)算符:==,!=,>=,>,<=,< | |
邏輯運(yùn)算符:!,&&,|| | |
賦值運(yùn)算符:=,+= | |
自增自減運(yùn)算符:++i,--i | |
控制流 | 分支語(yǔ)句:if (...) else if (...) else |
循環(huán)語(yǔ)句: for循環(huán) | |
函數(shù) | 自定義函數(shù):基礎(chǔ)類型值傳遞,對(duì)象類型引用傳遞。 |
C++API:支持調(diào)用外部注冊(cè)的C++函數(shù)。 |
淺析編譯器架構(gòu)
(編譯器的三段結(jié)構(gòu))
一個(gè)完整的編譯器通常由三個(gè)主要部分組成:前端、優(yōu)化器和后端。
- 前端:負(fù)責(zé)詞法分析、語(yǔ)法分析、語(yǔ)義分析、生成中間代碼。
- 優(yōu)化器(中端):負(fù)責(zé)對(duì)中間代碼進(jìn)行優(yōu)化。
- 后端:負(fù)責(zé)將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器的的機(jī)器碼。
基于LLVM實(shí)現(xiàn)DScript2.0編譯器
圖片
LLVM 是一個(gè)模塊化且高度可重用的編譯器基礎(chǔ)設(shè)施項(xiàng)目。它提供了前端、優(yōu)化器和后端工具鏈,已支持多種編程語(yǔ)言和平臺(tái)。LLVM具有跨平臺(tái)性,允許開發(fā)者靈活定制編譯流程,提供高級(jí)優(yōu)化能力,支持即時(shí)編譯,被廣泛用于編譯器開發(fā)、虛擬機(jī)和代碼分析工具場(chǎng)景。
※ 采用LLVM實(shí)現(xiàn)DScript2.0的優(yōu)勢(shì)
- 提升開發(fā)效率:LLVM的前端、中端和后端采用了模塊化設(shè)計(jì),每個(gè)部分都可以獨(dú)立替換或擴(kuò)展,這種靈活性使得 LLVM 非常適合定制編譯器,我們可以復(fù)用LLVM的中端與后端,專注于前端開發(fā),減少開發(fā)成本。
- 支持高級(jí)優(yōu)化:LLVM 提供了一套強(qiáng)大的優(yōu)化工具,能夠?qū)Υa進(jìn)行靜態(tài)和動(dòng)態(tài)優(yōu)化。這些優(yōu)化不僅能夠提高代碼的執(zhí)行效率,還可以減少代碼體積。這是DScript2.0理論上可能提供接近原生C++性能的關(guān)鍵因素之一。
- 支持即時(shí)編譯:LLVM 支持即時(shí)編譯(JIT),通過 JIT 編譯,LLVM 能夠在運(yùn)行時(shí)生成和執(zhí)行代碼,大大提升了執(zhí)行效率。通過運(yùn)行時(shí)進(jìn)行編譯后運(yùn)行,這是DScript2.0理論上可能提供接近原生C++性能的關(guān)鍵因素之二。支持在線的即時(shí)編譯能力,同時(shí)也是算子開發(fā)與分發(fā)效率的保障。
DScript2.0編譯器架構(gòu)
圖片
- DScript2.0編譯器同樣包含前端、中端、后端三部分,前端能力自研,優(yōu)化器和后端基于LLVM的Pass和JIT實(shí)現(xiàn)。
- 編譯器最終輸出為x86_64平臺(tái)的可執(zhí)行二進(jìn)制,以JIT實(shí)例的方式常駐內(nèi)存,通過入口函數(shù)地址執(zhí)行。
- 編譯器支持注入C++類型與函數(shù)參與編譯,實(shí)現(xiàn)DScript2.0對(duì)C++的調(diào)用。
編譯器前端實(shí)現(xiàn)
前端的實(shí)現(xiàn)流程
編譯器前端的任務(wù)是將源碼轉(zhuǎn)換為優(yōu)化器可處理的中間代碼,這個(gè)轉(zhuǎn)換的流程通常包含4個(gè)步驟:
- 詞法分析
- 語(yǔ)法分析
- 語(yǔ)義分析
- 中間代碼生成
(編譯器前端架構(gòu))
詞法分析
原理:源代碼是一堆連續(xù)的字符,計(jì)算機(jī)要先識(shí)別出這些字符組成的基本單元,才能進(jìn)一步理解代碼含義。就像讀句子先得認(rèn)出單詞一樣,這是理解程序的第一步。詞法分析的本質(zhì)是將代碼的字符流,轉(zhuǎn)換為更易處理的token流。
輸入與輸出:字符流->記號(hào)流(Tokens)。
※ 詞法分析器
DScript2.0中了使用Flex,可以根據(jù)自定義的正則表達(dá)式規(guī)則,自動(dòng)生成詞法分析的掃描器,減少手工編寫詞法分析器的工作量。
Flex工作流程
圖片
Flex語(yǔ)法
在Flex的定義文件中包含三部分:
- 定義段:包含頭文件和全局變量,如輸入和輸出流的定義。
- 規(guī)則段:由模式和對(duì)應(yīng)的動(dòng)作組成。當(dāng)掃描器匹配到模式時(shí),執(zhí)行對(duì)應(yīng)的動(dòng)作。例如,匹配到"int"字符串時(shí),將其識(shí)別為INT標(biāo)識(shí)。
- 用戶代碼段:通常可以在此區(qū)域定義 main() 函數(shù),它調(diào)用 yylex() ,啟動(dòng)詞法分析過程。
示例:
/* 定義段段開始 */
/* 引入的c/c++代碼 */
%{
#include <string>
%}
/* 正則表達(dá)式的宏定義 */
LineTerminator \n|\r|\r\n
WhiteSpace [ \t\f]|{LineTerminator}
Identifier [a-zA-Z_][a-zA-Z0-9_]*
/* 定義段結(jié)束 */
%%
/* 規(guī)則段開始 */
/* 規(guī)則:正則表達(dá)式 { return 傳遞給語(yǔ)法分析器的記號(hào)類型 } */
"int" { return INT; }
"float" { return FLOAT; }
"void" { return VOID; }
{Identifier} {
yylval.identifier = new std::string(yytext);
return IDENTIFIER;
}
{LineTerminator} {}
{WhiteSpace} {}
<<EOF>> {
return END;
}
/* 規(guī)則段結(jié)束 */
%%
/* 用戶代碼段開始 */
/* 用戶代碼段結(jié)束 */
匹配規(guī)則
- 最長(zhǎng)匹配:當(dāng)多個(gè)規(guī)則可匹配時(shí),F(xiàn)lex選擇最長(zhǎng)匹配的詞素。
- 最先定義:若多個(gè)規(guī)則長(zhǎng)度相同,則選擇最先定義的規(guī)則。
語(yǔ)法分析
原理:語(yǔ)法分析的原理是根據(jù)上下文無(wú)關(guān)文法(CFG)對(duì)輸入的 tokens 序列進(jìn)行分析,驗(yàn)證其是否符合某種語(yǔ)言的語(yǔ)法規(guī)則,并構(gòu)建對(duì)應(yīng)的抽象語(yǔ)法樹。其核心在于建立程序的分層邏輯結(jié)構(gòu),并確保這種結(jié)構(gòu)符合語(yǔ)法約束。
輸入與輸出:記號(hào)流->抽象語(yǔ)法樹(AST)。
由語(yǔ)法分析原理拆分
- 結(jié)構(gòu)驗(yàn)證:檢查記號(hào)流的排列是否符合語(yǔ)法規(guī)則,DScript2.0的語(yǔ)法規(guī)則由上下文無(wú)關(guān)文法(CFG)描述,驗(yàn)證算法采用了自底向上的LR算法。
// 示例:分支語(yǔ)法規(guī)則:if (conditon) { stmts }
// 符合語(yǔ)法規(guī)則
if (a < 1) {
// 不符合語(yǔ)法規(guī)則
if a < 1 {
- 層次構(gòu)建:將線性的記號(hào)流轉(zhuǎn)換為樹狀或嵌套的語(yǔ)法結(jié)構(gòu),以抽象語(yǔ)法樹為例:
int func(int a) {
int b = a + 1;
return b;
}
FunctionDefinition
├── ReturnType: int
├── FunctionName: func
├── Parameters
│ └── Parameter
│ ├── Type: int
│ └── Name: a
└── Body
├── VariableDeclaration
│ ├── Type: int
│ ├── Name: b
│ └── InitialValue
│ └── +
│ ├── Variable: a
│ └── Constant: 1
└── ReturnStatement
└── Variable: b
※ 上下文無(wú)關(guān)文法(CFG)
上下文無(wú)關(guān)文法(CFG) 是編譯器語(yǔ)法分析的核心工具,用于形式化描述編程語(yǔ)言的語(yǔ)法結(jié)構(gòu)。
其核心要素包括:
- 終結(jié)符(如標(biāo)識(shí)符、運(yùn)算符),對(duì)應(yīng)詞法分析的 Token,不可再分解。
- 非終結(jié)符(如表達(dá)式、語(yǔ)句),需通過產(chǎn)生式規(guī)則展開為終結(jié)符或其他非終結(jié)符。
- 產(chǎn)生式規(guī)則(如 E → E + T) ,定義語(yǔ)法結(jié)構(gòu)的生成方式。
- 起始符號(hào)(如 Program ),代表語(yǔ)法分析的入口。
產(chǎn)生式規(guī)則定義示例:
/* 局部變量聲明 -> 類型 變量聲明 */
/* 例如 int a = 1 */
/* Type對(duì)應(yīng)int */
/* Variable_Declartor對(duì)應(yīng)a = 1 */
Local_Variable_Declartor ->
Type Variable_Declartor;
/* 變量聲明 -> 變量ID 或 變量ID = 變量初始化 */
Variable_Declartor ->
Variable_ID
| Variable_ID EQ Variable_Initializer;
/* 變量ID -> 標(biāo)識(shí)符 */
Variable_ID -> IDENTIFIER;
/* 變量初始化 -> 任意表達(dá)式 */
Variable_initializer -> expression;
示例中根據(jù)形式化的語(yǔ)法,描述了變量定義和變量初始化規(guī)則。
示例中包含4條產(chǎn)生式規(guī)則:
- 局部變量聲明規(guī)則
- 變量聲明表達(dá)式規(guī)則
- 變量ID規(guī)則
- 變量初始化規(guī)則
終止符:
- Type對(duì)應(yīng)一個(gè)C++的TypeNode
- IDENTIFIER對(duì)應(yīng)詞法定義的Token
※ 語(yǔ)法分析器
語(yǔ)法分析器采用Bison來(lái)實(shí)現(xiàn),Bison可以與Flex進(jìn)行協(xié)作,將詞法分析器生成的記號(hào)序列解析為語(yǔ)法樹,供編譯器進(jìn)一步處理。
通過與 Flex 協(xié)同工作,Bison 可以自動(dòng)化地處理復(fù)雜的語(yǔ)法分析任務(wù),使編譯器的開發(fā)更加高效和靈活。
語(yǔ)義分析
原理:通過遍歷抽象語(yǔ)法樹,實(shí)現(xiàn)上下文相關(guān)的文法檢查,對(duì)程序的類型、作用域和標(biāo)識(shí)符等進(jìn)行詳細(xì)檢查,確保程序在邏輯上符合編程語(yǔ)言的規(guī)則,同時(shí)生成中間表示代碼,作為優(yōu)化器或后端的輸入。
輸入與輸出:抽象語(yǔ)法樹->中間代碼。
語(yǔ)法分析與語(yǔ)義分析的區(qū)別:
- 輸出目標(biāo)不同:語(yǔ)法分析的主要任務(wù)是將記號(hào)流轉(zhuǎn)換為結(jié)構(gòu)化信息,語(yǔ)義分析是將結(jié)構(gòu)化信息翻譯為優(yōu)化器可以處理的中間表示語(yǔ)言。
- 語(yǔ)法正確的語(yǔ)句,語(yǔ)義未必正確:
- 例如,有函數(shù)原型 void echo(int a) ,在調(diào)用時(shí) int b = echo("a") ,這是符合語(yǔ)法的,但不符合語(yǔ)義。
- 再比如,語(yǔ)言要求使用變量前先定義,在未定義變量 a 的前提下,執(zhí)行賦值 a = 1; ,這樣也是符合語(yǔ)法但不符合語(yǔ)義的。
※ 語(yǔ)義分析的主要任務(wù)
符號(hào)表管理
- 作用域解析:追蹤變量/函數(shù)的作用域(如塊級(jí)作用域、全局作用域)。
- 符號(hào)綁定:將標(biāo)識(shí)符與其聲明關(guān)聯(lián)(如變量類型、函數(shù)簽名)。
- 重復(fù)定義檢查:禁止同一作用域內(nèi)同名符號(hào)的重復(fù)聲明。
類型系統(tǒng)校驗(yàn)
- 類型推斷與檢查:驗(yàn)證表達(dá)式和操作的合法性,如 int a = "str"; 類型不匹配。
- 隱式類型轉(zhuǎn)換:處理類型提升,如 int + float 自動(dòng)轉(zhuǎn)為浮點(diǎn)運(yùn)算。
- 函數(shù)簽名匹配:檢查實(shí)參與形參的個(gè)數(shù)、類型一致性。
控制流合法性
- 語(yǔ)句上下文檢查:確保 break 僅在循環(huán)內(nèi)、 return 與函數(shù)返回類型一致。
- 可達(dá)性分析:檢測(cè)不可達(dá)代碼(如 return 后的語(yǔ)句)。
常量表達(dá)式求值
- 優(yōu)化常量計(jì)算(如 const x = 2 + 3*4; 直接計(jì)算為 14 )。
- 用于數(shù)組長(zhǎng)度、條件編譯等需編譯期確定值的場(chǎng)景。
※ 中間代碼生成
中間代碼的生成流程是通過遞歸遍歷AST完成的,將語(yǔ)義檢查無(wú)誤的邏輯,轉(zhuǎn)換為中間表示語(yǔ)言,這是編譯器前端工作的最后一步。
DScript2.0中使用了LLVM IR作為中間代碼語(yǔ)言,它介于高級(jí)語(yǔ)言和目標(biāo)代碼之間,既能表達(dá)高級(jí)語(yǔ)言的抽象概念,又能適應(yīng)底層機(jī)器代碼的生成需求。
LLVM IR提供了豐富的指令集,涵蓋了從基本運(yùn)算到復(fù)雜控制流、內(nèi)存操作、同步操作等各種編程需求。
LLVM IR指令集示例
指令種類 | 指令/作用 |
算術(shù)和位操作指令 |
|
內(nèi)存訪問指令 |
|
比較指令 |
|
控制流指令 | br: 條件或無(wú)條件分支 |
函數(shù)管理指令 |
|
轉(zhuǎn)換示例:
int func(int a) {
int b = a + 1;
return b;
}
(源代碼)
; 函數(shù)定義: 函數(shù)名為 func,返回類型為 i32(32位整數(shù)),參數(shù)為 i32 類型的 a
define i32 @func(i32 %a) {
entry:
; 定義局部變量 b,并將其初始化為 a + 1 的結(jié)果
%b = add i32 %a, 1
; 返回 b 的值
ret i32 %b
}
(與之對(duì)應(yīng)的LLVM的中間代碼)
編譯器中端:中間代碼優(yōu)化
圖片
- 在DScript2.0中,優(yōu)化器是通過復(fù)用LLVM的中端優(yōu)化能力來(lái)實(shí)現(xiàn)的,通過一系列LLVM預(yù)置的優(yōu)化遍(Pass),對(duì)程序生成的中間代碼進(jìn)行優(yōu)化,以提高代碼的性能。
- 中端的輸出為優(yōu)化過后的IR指令,這些IR指令需要提供給后端進(jìn)行編譯。
在LLVM中,優(yōu)化遍是指按照一定順序執(zhí)行的一個(gè)或多個(gè)優(yōu)化算法。
以下是一些常用的優(yōu)化算法:
數(shù)據(jù)流分析 | 死代碼消除 (DCE) | 通過數(shù)據(jù)流分析,LLVM 能夠精確地識(shí)別和刪除這些無(wú)用的指令。 |
全局值編號(hào)(GVN) | 檢測(cè)并消除等價(jià)的冗余表達(dá)式,減少重復(fù)計(jì)算。 | |
循環(huán)優(yōu)化 | 循環(huán)展開 (Loop Unrolling) | 通過展開循環(huán)體中的指令,減少循環(huán)控制的開銷,并增加指令級(jí)并行性。 |
循環(huán)分割 (Loop Split) | 將復(fù)雜的循環(huán)拆分為多個(gè)更簡(jiǎn)單的循環(huán),以便更好地優(yōu)化每個(gè)循環(huán)。 | |
循環(huán)不變代碼外提 (LICM) | 將循環(huán)中不變的計(jì)算移出循環(huán)體,從而減少不必要的重復(fù)計(jì)算。 | |
控制流優(yōu)化 | 條件合并 (Conditional Merging) | 合并控制流中多余的條件判斷,從而簡(jiǎn)化分支結(jié)構(gòu)。 |
跳轉(zhuǎn)線程化 (Jump Threading) | 在控制流圖中,將多個(gè)條件判斷組合為一個(gè)單一的跳轉(zhuǎn),以減少不必要的分支。 | |
尾調(diào)用優(yōu)化 (TCO) | 優(yōu)化遞歸函數(shù)調(diào)用,使得尾遞歸調(diào)用能夠直接重用當(dāng)前棧幀,從而避免棧溢出。 | |
內(nèi)存訪問優(yōu)化 | 內(nèi)存別名分析 (Alias Analysis) | 確定不同指針是否指向相同的內(nèi)存位置,從而幫助優(yōu)化器在內(nèi)存訪問上進(jìn)行優(yōu)化,如消除冗余的內(nèi)存加載和存儲(chǔ)操作。 |
堆棧分配優(yōu)化 (Stack Allocation Optimization) | 通過分析棧上變量的生命周期,減少不必要的內(nèi)存分配和釋放,或者將棧分配的變量?jī)?yōu)化到寄存器中。 |
編譯器后端:即時(shí)編譯
圖片
DScript2.0 使用 LLVM 的 ORC JIT 作為即時(shí)編譯器的實(shí)現(xiàn),支持在程序運(yùn)行時(shí)編譯腳本,并通過查找函數(shù)地址的方式執(zhí)行腳本。
采用即時(shí)編譯器的優(yōu)勢(shì):
- 避免了開發(fā)調(diào)試過程中,頻繁的啟停程序,提升迭代效率。
- 且經(jīng)過編譯的代碼,在執(zhí)行時(shí)能夠顯著提升運(yùn)行性能。
語(yǔ)言互操作性
語(yǔ)言互操作性是指不同編程語(yǔ)言能夠相互調(diào)用、協(xié)同工作的能力。通過這種能力,開發(fā)者可以在同一項(xiàng)目中結(jié)合多種語(yǔ)言的優(yōu)勢(shì)。
例如,C++ 與 Lua 的結(jié)合是就互操作的經(jīng)典場(chǎng)景,常見于游戲開發(fā)、搜推引擎、嵌入式系統(tǒng)等領(lǐng)域。
在我們的需求中,要支持動(dòng)態(tài)腳本訪問引擎的表列資源,就需要DScript2.0也能具備與C++交互操作的能力。
DScript2.0與C++的語(yǔ)言互操作性體現(xiàn)在
- DScript2.0可以調(diào)用C++的函數(shù),并向C++傳遞數(shù)據(jù)。
- C++可以調(diào)用DScript2.0的函數(shù),并向DScript腳本傳遞數(shù)據(jù)。
- DScript2.0可以訪問和操作C++傳遞的基礎(chǔ)類型和結(jié)構(gòu)體類型變量。
調(diào)試能力
DScript2.0基于GDB實(shí)現(xiàn)了基本的調(diào)試能力:
- 支持通過Attach進(jìn)程進(jìn)行實(shí)時(shí)調(diào)試
- 支持在coredump中保留棧信息
調(diào)試能力的實(shí)現(xiàn)主要基于GDB的通用調(diào)試接口,在編譯DScript2.0源碼時(shí),生成調(diào)試信息,插入到LLVM IR的元數(shù)據(jù)中,然后通過JIT的監(jiān)聽器掛載GDB調(diào)試接口,并注入調(diào)試信息,最終實(shí)現(xiàn)調(diào)試能力。
圖片
異常處理
DScript2.0中也實(shí)現(xiàn)了異常處理能力,主要包括了硬件異常的主動(dòng)防御和跨C++與DScript2.0邊界的異常傳播。
硬件異常防御
程序異常可以劃分為硬件異常和主動(dòng)異常:
- 硬件異常是底層不可控錯(cuò)誤,硬件異常的處理需依賴信號(hào)鉤子或語(yǔ)言運(yùn)行時(shí)封裝。
典型例子:
- 段錯(cuò)誤(SIGSEGV):非法內(nèi)存訪問
- 浮點(diǎn)運(yùn)算錯(cuò)誤(SIGFPE):如整數(shù)除零或浮點(diǎn)運(yùn)算異常
- 非法指令(SIGILL):執(zhí)行未定義的機(jī)器指令
- 總線錯(cuò)誤 (SIGBUS):如未對(duì)齊的內(nèi)存訪問
- 主動(dòng)異常是代碼邏輯的一部分,用于可控的錯(cuò)誤處理與資源管理,主動(dòng)異常由開發(fā)者顯式拋出,也可由語(yǔ)言運(yùn)行時(shí)隱式轉(zhuǎn)換。
※ 硬件異常的主動(dòng)防御
DScript2.0在語(yǔ)言層面上,對(duì)代碼引發(fā)的硬件異常進(jìn)行了主動(dòng)防御。實(shí)現(xiàn)上,是在語(yǔ)義分析階段,對(duì)中間代碼添加防御邏輯,防御策略則采用了可被捕獲的主動(dòng)異常拋出。
例如下圖所示,在編譯階段,編譯器對(duì)于結(jié)構(gòu)體指針進(jìn)行了空引用檢查邏輯,將硬件異常轉(zhuǎn)換為了主動(dòng)異常,而主動(dòng)異常可以通過捕獲來(lái)進(jìn)行處理,避免了進(jìn)程崩潰。
跨語(yǔ)言邊界傳播
因?yàn)镈Script2.0的語(yǔ)言互操作性特性,會(huì)涉及到C++與DScript2.0的函數(shù)互相調(diào)用(如下圖所示),就會(huì)涉及到異常處理時(shí),異常在C++和DScript2.0之間傳播,即所謂跨語(yǔ)言邊界。
DScript2.0主要實(shí)現(xiàn)了如下的異常傳播機(jī)制:
- 腳本調(diào)用 C++ 函數(shù)時(shí)若拋出異常,在腳本端不進(jìn)行捕獲,但支持異常傳播到C++端,同時(shí)正常完成棧回退。
- C++ 調(diào)用腳本函數(shù)時(shí)若拋出異常,可以在 C++ 端捕獲。
四、DScript2.0在線開發(fā)工作流
圖片
DScript2.0通過平臺(tái)化實(shí)現(xiàn)了在線開發(fā)的工作流:
- 引擎集成:以SDK方式與引擎進(jìn)行集成,提供在線編譯和加載的能力。
- 在線IDE:實(shí)現(xiàn)編輯、編譯的在線開發(fā)環(huán)境。
- 在線工作流:通過平臺(tái)化支持腳本的在線分發(fā)與管理。
五、總結(jié)
DScript2.0的實(shí)踐為推薦引擎的敏捷迭代探索了一條新路徑。通過編譯器架構(gòu)與C++底層機(jī)制的高度兼容設(shè)計(jì),它在降低跨語(yǔ)言交互成本、支持動(dòng)態(tài)加載等方面展現(xiàn)出潛力,同時(shí)保持了接近原生C++的運(yùn)行時(shí)性能。
其即時(shí)編譯能力與在線開發(fā)流程,使業(yè)務(wù)團(tuán)隊(duì)能獨(dú)立完成邏輯更新,減少對(duì)傳統(tǒng)C++開發(fā)中編譯部署的依賴,初步驗(yàn)證了兼顧性能與效率的可能性。
未來(lái),我們計(jì)劃進(jìn)一步完善調(diào)試工具鏈與異常處理機(jī)制,并探索其在混合語(yǔ)言場(chǎng)景下的擴(kuò)展性,以更輕量的方式推動(dòng)引擎架構(gòu)的持續(xù)優(yōu)化。