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
湖南客戶端6月17日(記者 蔣茜)6月16日,由湖南省安委辦、湖南省應(yīng)急管理廳聯(lián)合制作的安全生產(chǎn)警示片《不可逾越的紅線》,在長(zhǎng)沙市“6.16”安全生產(chǎn)宣傳咨詢?nèi)栈顒?dòng)上正式發(fā)布。湖南省應(yīng)急管理廳黨委委員、政治部主任劉站出席咨詢?nèi)栈顒?dòng)并正式發(fā)布該片。
劉站指出, 制作安全生產(chǎn)警示片《不可逾越的紅線》,開展警示教育,是推動(dòng)各級(jí)各部門和企業(yè)深入學(xué)習(xí)貫徹落實(shí)習(xí)近平總書記關(guān)于安全生產(chǎn)重要論述,落實(shí)省委、省政府決策部署,以及安全生產(chǎn)專項(xiàng)整治三年行動(dòng)工作要求的重要舉措。各級(jí)各部門和企業(yè)要以案為鑒,舉一反三,警鐘長(zhǎng)鳴,壓實(shí)責(zé)任,樹牢安全發(fā)展理念,堅(jiān)守安全發(fā)展紅線,錨定“三堅(jiān)決兩確?!蹦繕?biāo),切實(shí)保障人民群眾生命財(cái)產(chǎn)安全。
劉站要求,各級(jí)各部門要根據(jù)今年“安全生產(chǎn)月”活動(dòng)安排,認(rèn)真組織觀看安全生產(chǎn)警示片《不可逾越的紅線》。要確保達(dá)到觀看“四個(gè)全覆蓋”要求,即企業(yè)員工、安委會(huì)成員單位領(lǐng)導(dǎo)同志、市州和縣市區(qū)黨政領(lǐng)導(dǎo)干部、負(fù)有安全生產(chǎn)監(jiān)管職責(zé)干部實(shí)現(xiàn)觀看全覆蓋。
《不可逾越的紅線》選取了湖南近年發(fā)生的滬昆高速“7.19”特別重大道路交通危化品爆燃事故、湘潭縣花石鎮(zhèn)“9.22”重大道路交通事故、瀏陽(yáng)市碧溪煙花制造有限公司“12.4”重大爆炸事故、衡陽(yáng)耒陽(yáng)市源江山煤礦“11.29”重大透水事故、常德安鄉(xiāng)眾鑫紙業(yè)有限責(zé)任公司“8.28”較大中毒窒息事故、婁底雙峰縣一心電器服務(wù)中心“6.17”較大火災(zāi)事故、岳陽(yáng)華容縣“1.23”較大塔式起重機(jī)坍塌事故為案例,用畫面還原了事故發(fā)生經(jīng)過,分析了事故發(fā)生的原因、事故危害和事故應(yīng)吸取的教訓(xùn)。
[責(zé)編:蔣茜]
[來源:湖南日?qǐng)?bào)·新湖南客戶端]
1世紀(jì)資管研究院研究員 唐曜華 隨著上市房企陸續(xù)披露2021年中報(bào),上市房企的最新“三道紅線”數(shù)據(jù)以及達(dá)標(biāo)情況也正式出爐。
為方便投資者查詢, 21世紀(jì)經(jīng)濟(jì)報(bào)道、21世紀(jì)資管研究院“南財(cái)債市通”產(chǎn)品組,特聯(lián)合中國(guó)指數(shù)研究院推出上市房企“三道紅線”查詢器工具(簡(jiǎn)介見后文)。
據(jù)中國(guó)指數(shù)研究院數(shù)據(jù),截至今年6月末,有19家上市房企今年6月末仍然處于三道紅線“全踩”的狀態(tài)。
除了債券已經(jīng)違約的華夏幸福、藍(lán)光發(fā)展、泰禾集團(tuán)、泛??毓伞⑻旆堪l(fā)展、新華聯(lián)外,還有華遠(yuǎn)地產(chǎn)、京投發(fā)展、中天金融、嘉凱城、棲霞建設(shè)、光明地產(chǎn)、恒盛地產(chǎn)等房企三道紅線“全踩”。在房企普遍降負(fù)債的努力之下,半年的時(shí)間“紅色檔”房企數(shù)量已經(jīng)有所減少。
三道紅線全踩房企融資難度大
根據(jù)“三道紅線”規(guī)定,房企將視踩線情況分為“紅、橙、黃、綠”四檔。如踩三道紅線,為“紅色檔”,不得新增有息負(fù)債;如踩兩道紅線,為“橙色檔”,有息負(fù)債規(guī)模年增速不得超過5%;如踩一道紅線,為“黃色檔”,有息負(fù)債規(guī)模年增速不得超過10%;三道紅線一道未踩為“綠色檔”,有息負(fù)債規(guī)模年增速不得超過15%。
據(jù)中國(guó)指數(shù)研究院數(shù)據(jù),截止2021年6月末,處于“紅色檔”即三道紅線全踩的上市房企有19家,“橙色檔”的上市房企有24家,“黃色檔”有76家,“綠色檔”有73家。大部分房企處在“綠色檔”和“黃色檔”。
除了三道紅線政策外,去年底人民銀行會(huì)同銀保監(jiān)會(huì)聯(lián)合發(fā)布 《關(guān)于建立銀行業(yè)金融機(jī)構(gòu)房地產(chǎn)貸款集中度管理制度的通知》,建立房地產(chǎn)貸款集中度管理制度,分檔設(shè)置房地產(chǎn)貸款余額占比上限和個(gè)人住房貸款余額占比上限,給銀行發(fā)放房地產(chǎn)貸款設(shè)限。
在政策整體收緊之下,三道紅線全踩雖然只是規(guī)定不得新增有息負(fù)債,但據(jù)了解,在實(shí)際融資過程中,包括通過金融機(jī)構(gòu)融資以及債券發(fā)行都會(huì)受三道紅線影響,金融機(jī)構(gòu)對(duì)三道紅線全踩的“紅檔”房企普遍采取謹(jǐn)慎的態(tài)度。這就使得一些三道紅線全踩的“紅檔”房企融資難度加大,不管主動(dòng)還是被動(dòng)均在壓降有息負(fù)債。
20家上市房企上半年實(shí)現(xiàn)降檔
通過壓降有息負(fù)債,今年上半年有22家上市房企實(shí)現(xiàn)降檔,包括財(cái)信發(fā)展、港龍中國(guó)地產(chǎn)、祥生控股集團(tuán)、天譽(yù)置業(yè)等房企實(shí)現(xiàn)降兩檔,其中財(cái)信發(fā)展、港龍中國(guó)地產(chǎn)成功進(jìn)入綠檔房企行列,天譽(yù)置業(yè)、祥生控股集團(tuán)進(jìn)入黃檔房企行列。需要說明的是,本文統(tǒng)計(jì)現(xiàn)金短債比采取的是寬口徑,即在統(tǒng)計(jì)現(xiàn)金及現(xiàn)金等價(jià)物時(shí)A股以(貨幣資金-受限資金)計(jì)算,港股則統(tǒng)計(jì)現(xiàn)金及現(xiàn)金等價(jià)物(不包含受限資金部分)。
從降檔房企實(shí)現(xiàn)降檔的具體指標(biāo)來看,大部分房企通過提高現(xiàn)金短債比實(shí)現(xiàn)降檔,有15家上市房企將現(xiàn)金短債比提高到1以上實(shí)現(xiàn)達(dá)標(biāo),剔除預(yù)收賬款的資產(chǎn)負(fù)債率降到紅線以下的房企也不少。
也有部分房企踩線條數(shù)增加,比如建發(fā)國(guó)際集團(tuán)、城投控股等今年上半年新增踩線2條,從原來的“綠檔”掉入“橙檔”行列。建發(fā)國(guó)際今年上半年拿地頗為激進(jìn),在今年上半年房企拿地金額排行榜中排名居前。保利置業(yè)集團(tuán)、融信中國(guó)、中渝置地、恒達(dá)集團(tuán)控股、嘉華國(guó)際等房企則今年上半年新增踩線一條。
21世紀(jì)經(jīng)濟(jì)報(bào)道、21世紀(jì)資管研究院“南財(cái)債市通”產(chǎn)品組推出的“三道紅線”查詢工具,可查詢上市房企2021年6月末的三道紅線數(shù)據(jù)以及上半年變化情況,了解房企踩線條數(shù)以及所處檔位,掃下方二維碼或者點(diǎn)擊鏈接即可查詢:
https://app.21jingji.com/html/2021/ssfqhx/
隨著融資環(huán)境收緊,不少房企經(jīng)營(yíng)風(fēng)險(xiǎn)出現(xiàn)上升,我們即將發(fā)布前50強(qiáng)房企健康度測(cè)評(píng)報(bào)告之經(jīng)營(yíng)風(fēng)險(xiǎn)篇,敬請(qǐng)期待~
更多內(nèi)容請(qǐng)下載21財(cái)經(jīng)APP
者:ecznlai@騰訊文檔
前段時(shí)間通過優(yōu)化業(yè)務(wù)里的相關(guān)實(shí)現(xiàn),將高頻調(diào)用場(chǎng)景性能優(yōu)化到原來的十倍,使文檔核心指標(biāo)耗時(shí)達(dá)到 10~15% 的下降。本文將從 V8 整體架構(gòu)出發(fā),深入淺出 V8 對(duì)象模型,從匯編細(xì)節(jié)點(diǎn)出其 ICs 優(yōu)化細(xì)節(jié)以及原理,最后根據(jù)這些優(yōu)化原理來編寫超快的 JS 代碼
js 代碼從源碼到執(zhí)行 —— v8 編譯器管線:
parser 將源碼編譯為 AST,并在 AST 基礎(chǔ)上編譯為「字節(jié)碼 bytecode」
ignition 是 v8 的字節(jié)碼解釋器,可以運(yùn)行字節(jié)碼,并在運(yùn)行過程中持續(xù)收集「feedback」即綠線,給到 turbofan 做最終的機(jī)器碼編譯優(yōu)化。
而由于 js 是相當(dāng)動(dòng)態(tài)的語(yǔ)言,編譯出來的「機(jī)器指令」未必能正確,因此其運(yùn)行過程中有可能要回滾到 ignition 解釋器來運(yùn)行,這些問題通過「紅線」反饋給 ignition 解釋器,這個(gè)過程叫做「反優(yōu)化」。
—— 更具體來說:
將源碼一段線性 buffer string 解析為 Token 流,最后依據(jù) Token 流生成 AST 樹狀構(gòu)造,這是所有語(yǔ)言都會(huì)有的過程。
運(yùn)行過程中產(chǎn)生并持續(xù)收集的反饋信息,比如多次調(diào)用 add(1, 2) 就會(huì)產(chǎn)生「add 函數(shù)的兩個(gè)參數(shù) “大概率” 是整數(shù)」的反饋,v8 會(huì)收集這類信息,并在后續(xù) TurboFan codegen 的時(shí)候根據(jù)這些反饋來做假設(shè),并依據(jù)這些假設(shè)做深度優(yōu)化,后文將從匯編的角度討論這個(gè)細(xì)節(jié)。
前面提到 「add 函數(shù)的兩個(gè)參數(shù) “大概率” 是整數(shù)」 的假設(shè),當(dāng)假設(shè)被打破的時(shí)候會(huì)觸發(fā)所謂的「deoptimize」反優(yōu)化,比如你在運(yùn)行了很久的 add(number, number) 上突然來一個(gè) add("123", "abc") 那么此時(shí)就會(huì)降級(jí)重新回到 ignition bytecode 執(zhí)行。
前者生成 byte code,后者根據(jù)執(zhí)行過程中收集的 feedback 來生成深度優(yōu)化的 machine code
世界上能執(zhí)行代碼的地方有很多,數(shù)軸上的兩個(gè)極端: 左邊是抽象程度最高的人腦,右邊是抽象程度最低的 CPU:
上圖中三個(gè)實(shí)體以不同的角度理解下面這樣的代碼,從源碼到字節(jié)碼再到機(jī)器碼其實(shí)就是不斷編譯為另外一個(gè)語(yǔ)言的過程
const a=3 + 4;
計(jì)算 3+4 存儲(chǔ)到 js 變量 const a 中
將代碼解析為 AST 樹(一種 JSON 結(jié)構(gòu))
iginition 會(huì)將代碼理解編譯為 bytecode :
...
LdaSmi [3] // 加載字面量 3 到棧頂
Star0 // 將棧頂 3 pop 到寄存器 r0
Add r0, [4] // 計(jì)算 r0 + 4
...
TurboFan 會(huì)將代碼理解為匯編:
...
mov ax 3 # 將 3 賦值到寄存器 ax
add ax 4 # 計(jì)算 ax=ax + 4
...
本質(zhì)上來說 v8 bytecode 和 x86 匯編是一樣的,只是世界上沒有裸機(jī)能跑出 v8 所理解的 bytecode 而已,機(jī)器碼為什么快是因?yàn)?CPU 能在硬件層面上裸跑匯編,因此速度特別快。
總之為了充分表達(dá) js 動(dòng)態(tài)特性以及方便優(yōu)化為 CPU 能直接裸跑的匯編,v8 引入了 bytecode 這個(gè)層次,它比 AST 更接近物理機(jī),因?yàn)樗鼪]有層次嵌套,是一種基于寄存器的指令集。
JIT 指的是邊運(yùn)行邊優(yōu)化為機(jī)器碼的編譯技術(shù),其中的代表有 jvm / lua jit / v8,這類優(yōu)化技術(shù)會(huì)在運(yùn)行過程中持續(xù)收集執(zhí)行信息并優(yōu)化程序性能。AOT 指的是傳統(tǒng)的編譯行為,在靜態(tài)類型語(yǔ)言(如 C、C++、Rust)和某些動(dòng)態(tài)類型語(yǔ)言(如 Go、Swift)中得到了廣泛應(yīng)用,由于能提前看到完整代碼,編譯器/語(yǔ)言運(yùn)行時(shí)可以在編譯階段進(jìn)行充分的優(yōu)化,從而提高程序的性能。
由于 JIT 語(yǔ)言并不能提前分析代碼并優(yōu)化執(zhí)行,因此 JIT 語(yǔ)言的「編譯期」很薄,而「運(yùn)行時(shí)」相當(dāng)厚實(shí),諸多編譯優(yōu)化都是在代碼運(yùn)行的過程中實(shí)現(xiàn)的。
ignition 負(fù)責(zé)解釋執(zhí)行 V8 引入的中間層次字節(jié)碼,上接人腦里的 js 規(guī)范,下承底層 CPU 機(jī)器指令
TurboFan 可以將字節(jié)碼編譯為最快的機(jī)器碼,讓裸機(jī)直接運(yùn)行,達(dá)到最快的執(zhí)行速度。
利用這個(gè)參數(shù)開啟 v8 注入的 runtime call,幫助分析和調(diào)試 v8
# node 下開啟
$ node --allow-natives-syntax
# chrome 下開啟
$ open -a Chromium --args --js-flags="--allow-natives-syntax"
下面是一些常用指令說明。
可以打印對(duì)象在 v8 的內(nèi)部信息,比如打印一個(gè)函數(shù):
告訴 v8 下次調(diào)用主動(dòng)觸發(fā)優(yōu)化函數(shù) fn
獲取函數(shù)當(dāng)前的優(yōu)化 status,后文會(huì)詳細(xì)介紹:
對(duì)應(yīng)的是 V8 源碼里的這個(gè)枚舉:
從開發(fā)視角來看,一個(gè)函數(shù)最佳的 status 應(yīng)該是 00000000000001010001 (81) 即:
%HasFastProperties 可以用來打印對(duì)象是否是 Fast Properties 模式
后文會(huì)介紹這個(gè) Fast Properties 和與之對(duì)立的 Slow Properties。
首先 Tagged Pointer 是 C/C++ 里常用的優(yōu)化技術(shù),不只在 V8 里有用,具體來說就是依據(jù) pointer 自身的數(shù)值的某些位來決定 pointer 的行為,也就是說這類指針的特點(diǎn)是「其指針數(shù)值上的某些位有特殊含義」。
比如在 v8 里,js 堆指針和 SMI 小整數(shù)類型(small intergers)是通過 Tagged Pointer 來表達(dá)和引用的,區(qū)別就在于最低一位是不是 0 來決定其指針類型:
對(duì)象指針(32 位):
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx1
SMI 小整數(shù)(32 位)其中 xxx 部分為數(shù)值部分:
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx0
用 C 表達(dá)就是這樣:
#include <stdio.h>
void printTaggedPointer(void * p) {
// 強(qiáng)轉(zhuǎn)一下, 關(guān)注 p 本身的數(shù)值
unsigned int tp=((unsigned int) p);
if ((tp & 0b1)==0b0) {
printf("p 是 SMI, 數(shù)值大小為 0x%x \n", tp >> 1);
return;
}
printf("p 是堆對(duì)象指針, Object<0x%x> \n", tp);
// printObject(*p); // 假設(shè)有個(gè)方法可以打印堆對(duì)象
}
int main() {
printTaggedPointer(0x1234 << 1); // smi
printTaggedPointer(17); // object
return 0;
}
運(yùn)行效果:
備注:
我們先來看這個(gè)例子,一個(gè) add(x,y) 函數(shù),如果運(yùn)行期間出現(xiàn)了多種類型的傳參,那么會(huì)導(dǎo)致代碼變慢:
我們可以看到,L15 速度慢了非常多,比一開始的 66ms 慢了幾倍。
原因:
比如一開始傳的是 number,走到了優(yōu)化過的代碼,里面走的是匯編指令 add;當(dāng)傳入 string 或者 其他什么合法的 JSValue 后,編譯為匯編的 add 函數(shù)的執(zhí)行真的沒問題嗎?—— 不會(huì)有問題,因?yàn)?TurboFan 在編譯后的「機(jī)器碼」里會(huì)帶上很多 checkpoint,其實(shí)這些 checkpoint 就是在做類型檢查 type guard,如果類型對(duì)不上立刻就會(huì)終止這次調(diào)用并執(zhí)行「反優(yōu)化」讓 ignition 走字節(jié)碼解釋執(zhí)行。
上述說法可能會(huì)比較含糊,我們可以具體看看打出來的匯編是咋樣的,可以通過以下方式打印出優(yōu)化后的 x86 匯編(m1 芯片的蘋果電腦應(yīng)該是 arm 指令)。
$ node --print-opt-code --allow-natives-syntax --trace-opt --trace-deopt ./a.js
如下圖所示,這個(gè) test 函數(shù)實(shí)現(xiàn)是將第一個(gè)入?yún)⒓由?0x1234 并返回,而這個(gè)核心邏輯對(duì)應(yīng) L37 那行匯編,而其他的部分除了 v8 自身的「調(diào)用約定」外,其他的就是 checkpoint 檢查類型,以及一些 debug 斷點(diǎn)了:
從前面的 Tagged Pointer 的相關(guān)討論可知,L19 ~ L22 其實(shí)就是在判斷入?yún)⑹遣皇?SMI,具體來說是 [rbx+0xf] 與 0x1 做按位與操作([rbx+0xf] 是通過棧傳遞的參數(shù),是 v8 里 js 的調(diào)用約定)如果結(jié)果是 0 則跳轉(zhuǎn) 0x10b7cc34f 即后續(xù)的正常流程,否則走到 CompileLazyDeoptimizedCode 走反優(yōu)化流程用字節(jié)碼解釋器去執(zhí)行了,我這里大概寫了一個(gè)反匯編偽碼對(duì)照:
另外我們也可以看到,核心邏輯對(duì)應(yīng)到匯編也就一行,剩余的指令要么是 checkpoint 要么是 v8/js 的調(diào)用約定,在這么多冗余指令的情況下執(zhí)行性能依然很快,可見匯編的執(zhí)行效率比起 line-by-line 的解釋器要高得多了。
通過 %DebugPrint 可以看到
當(dāng)打破這個(gè) assumption 后,會(huì)變成 Any:
不會(huì)
根據(jù)前面提到的 checkpoint,上面三個(gè) mono 的 checkpoint 最少,而最后的 mega 將會(huì)非常多,優(yōu)化性能最差,或者 V8 干脆就不會(huì)對(duì)這類函數(shù)做更深度的機(jī)器碼優(yōu)化了(比如后文會(huì)提到的 ICs)
從 JS AST / bytecode 編譯到機(jī)器碼也需要開銷,毫秒級(jí)。
根據(jù)這篇文章 V8 function optimization - Blog by Kemal Erdem 如果某個(gè)函數(shù)「反優(yōu)化」超過 5 次后,v8 以后就不再會(huì)對(duì)這個(gè)函數(shù)做優(yōu)化了,不過我無(wú)法復(fù)現(xiàn)他說的這個(gè)情況,可能是老版本的 v8 的表現(xiàn),node16 不會(huì)這樣,不管怎樣只要 run 了足夠多次都turbofanned,只是如果「曾經(jīng)傳的參數(shù)類型太 union typed」會(huì)導(dǎo)致優(yōu)化效果出現(xiàn)非常大的折損。
前面我們已經(jīng)知道了「運(yùn)行足夠多次」會(huì)觸發(fā)優(yōu)化,而這只是其中一種情況,具體可以參考 v8 里 ShouldOptimize 的實(shí)現(xiàn),里面有詳細(xì)定義何時(shí)啟動(dòng)優(yōu)化:
作為開發(fā)視角來看:
備注:maglev 是去年 chrome v8 團(tuán)隊(duì)搞的新特性 —— 編譯層次優(yōu)化,總的來說就是根據(jù) feedback 對(duì)機(jī)器碼的編譯層次做精細(xì)控制來達(dá)到更好的優(yōu)化效果,下圖是 v8 團(tuán)隊(duì)發(fā)布的 benchmark 對(duì)比:
具體可參考 v8.dev/blog/maglev
會(huì)的,而且有時(shí)候這部分內(nèi)存占用非常多,這也是 Chrome 經(jīng)常被調(diào)侃為內(nèi)存殺手的重要原因之一,以 qq.com 為例,具體對(duì)應(yīng)是 heapdump 里的 (compiled code) 包含了編譯后的代碼內(nèi)存占用:
本節(jié)開始是本文的重點(diǎn)部分,因?yàn)橹挥辛私?V8 對(duì)象的內(nèi)存構(gòu)造,才能真正理解 V8 諸多優(yōu)化的理由。
在正式進(jìn)入之前,我們先看看 C 里面 struct 的「點(diǎn)讀」是怎么做的。
C 會(huì)將 struct 理解為一段連續(xù)的線性 buffer 結(jié)構(gòu),并在上面根據(jù)字段的類型來劃分好從下標(biāo)的哪里到哪里是哪個(gè)字段(對(duì)齊),因此在編譯 point.x 的時(shí)候會(huì)改成 base+4 的方式進(jìn)行屬性訪問,如下圖所示,時(shí)間復(fù)雜度是 O(1) 的:
也因此 C 里面沒提供從字段 key 名的方式去取 struct value 的方法,也就是不支持 point['x']這樣,需要你自己寫 getter 才能實(shí)現(xiàn)類似操作。
這類根據(jù) string value 來從對(duì)象取值的技術(shù)通常在現(xiàn)代編程語(yǔ)言里都是自帶了的,通常稱為反射,可以在運(yùn)行時(shí)訪問源碼信息。
但在 JS 里,對(duì)象是動(dòng)態(tài)的,可以有任意多的 key-values,而且這些 kv 鍵值對(duì)還可能在運(yùn)行時(shí)期間動(dòng)態(tài)發(fā)生變化,比如我可以隨時(shí) p.xxx=123 又或者 delete p.xxx 去刪掉它,這意味著一個(gè) object 的 “shapes” 及其「內(nèi)存結(jié)構(gòu)」是無(wú)法被靜態(tài)分析出來的,而且這種內(nèi)存結(jié)構(gòu)必然不是「定長(zhǎng)固定」的,是需要?jiǎng)討B(tài) malloc 變長(zhǎng)的。
假設(shè)現(xiàn)在是 2008 年,你是 google 的工程師,正在 chrome v8 項(xiàng)目組開發(fā),你會(huì)怎樣設(shè)計(jì) JS 的對(duì)象的內(nèi)存結(jié)構(gòu)?
const obj={ x: 3, y: 5 }
// obj 的內(nèi)存結(jié)構(gòu)可以設(shè)計(jì)成怎樣?
一眼丁真,開搞:
一個(gè) key 定義加一個(gè)值,然后將這個(gè)結(jié)構(gòu)數(shù)組化就可以表達(dá)對(duì)象的 kv 結(jié)構(gòu),增加屬性就在后面繼續(xù)擴(kuò)增,查找算法則是從頭查到尾,時(shí)間復(fù)雜度為 O(n)
但是如果按這個(gè)設(shè)計(jì),下面兩個(gè) obj 就會(huì)有重復(fù)的 key 定義內(nèi)存消耗了:
const obj1={ x: 11, y: 22 } // "x" 11 "y" 22
const obj1={ x: 33, y: 44 } // "x" 33 "y" 44
// 會(huì)重復(fù) "x" 和 "y"
好了就上面這樣簡(jiǎn)單弄一下就搞出了好多問題了。從下面開始正式進(jìn)入,V8 是如何描述對(duì)象,參見下文。
在 js 標(biāo)準(zhǔn)里 Array 是一類特殊的 Object,但出于性能考慮 V8 底層針對(duì)對(duì)象和數(shù)組的處理是不同的:
如下圖所示,JSObject:
在 V8 里:
嗯?對(duì)象的 Shapes?那是什么?
所謂對(duì)象的 shapes,其實(shí)就是對(duì)象上有什么 key,前面提到過 V8 的優(yōu)化需要在運(yùn)行時(shí)不斷收集 feedback,比如當(dāng)執(zhí)行下面這段代碼的時(shí)候,引擎就可以知道「obj 有兩個(gè) key,一個(gè)是 a 一個(gè)是 b」:
const obj={}
obj.a=123;
obj.b=124;
doSomething(obj);
V8 通過 Hidden Class 結(jié)構(gòu)來記錄 JSObject 在運(yùn)行時(shí)的時(shí)候有哪些 key,也就是記錄對(duì)象的 shapes,由于 JSObject 是動(dòng)態(tài)的,后續(xù)也可以隨意設(shè)置 obj.xxx=123,也就是對(duì)象的 shapes 會(huì)變,也因此對(duì)象持有的 Hidden Class 會(huì)隨著特定代碼的運(yùn)行而變化
Hidden Class 是比較學(xué)術(shù)的說法,在 V8 源碼里的「工程命名」是 Map,在微軟 Edge Chakra (edge) 里叫做 Types,在 JavaScriptCore (WebKit Safari) 里叫做 Structure,在 SpiderMonkey (FireFox) 里叫做 Shapes .... 總之各個(gè)主流引擎都有實(shí)現(xiàn)追蹤「對(duì)象 shapes 變化」
后文可能會(huì)混淆上面幾個(gè)用語(yǔ),它們都是指 Hidden Class,用來描述對(duì)象的 shapes。
前面提到除了 *properties 和 *elements 可以用來存儲(chǔ)對(duì)象成員之外,JSObject 還提供了所謂 in-object properties 的方式來存儲(chǔ)對(duì)象成員,也就是將對(duì)象成員保存在「JSObject 結(jié)構(gòu)體」上,并配合 Hidden Class 進(jìn)行鍵值描述:
上圖里 Hidden Class 里底下有個(gè)叫做 DescriptorArrays 的子結(jié)構(gòu),這個(gè)結(jié)構(gòu)會(huì)記錄對(duì)象成員 key 以及其對(duì)應(yīng)存儲(chǔ)的 in-object 下標(biāo),也就是上面的紫框。
或許你會(huì)問:
如果 Hidden Class 是靜態(tài)的,那么這圖就足夠描述 Hidden Class 了:
但是對(duì)象的 shapes 會(huì)變,也因此對(duì)象持有的 Hidden Class 會(huì)隨著特定代碼的運(yùn)行而變化,V8 使用了 Transition Chain,一種基于鏈表構(gòu)造的方式來描述「變化中的 Hidden Class」:
備注:為了方便討論,后文可能不會(huì)將 Hidden Class 畫成鏈表,而是畫成一起并且省略空對(duì)象的 shapes,另外 Hidden Class Node 上還有其他字段,相對(duì)不那么重要,就忽略了
由于鏈表的特性,顯然可以比較容易地讓具有相同 shapes 的對(duì)象能復(fù)用同一個(gè) Hidden Class ,比如下面這個(gè) case,o1 o2 均復(fù)用了地址為 0xABCD 的 Hidden Class 節(jié)點(diǎn):
當(dāng)出現(xiàn)不同走向的時(shí)候,此時(shí)會(huì)單獨(dú)開一個(gè) branch 來描述這種情況,此時(shí) o1 和 o2 就不再一樣了:
從前文的討論,可以得到的結(jié)論:
懸而未決的問題:
請(qǐng)帶著這兩個(gè)問題到下一章 Inline Caches 繼續(xù)閱讀。
引入 Hidden Class 后,為了讀取某個(gè)成員,那不還得查一次 Hidden Class 拿到 in-object 的下標(biāo),這個(gè)過程不還是 O(n) 嗎?
是的,如果事先不知道 JSObject 的 shapes 的情況下去讀取成員確實(shí)是 O(n) 的,但前面我已經(jīng)提過了:
V8 的諸多優(yōu)化是基于 assumption 的,那么在已知 obj 的 Shapes 的情況下,你會(huì)怎么優(yōu)化下面這個(gè) distance 函數(shù)?
如此優(yōu)化就可以將「通過遍歷 *properties訪問成員的O(n) 過程」直接優(yōu)化為「直接按下標(biāo)偏移直接讀取 `in-object` 的 O(1)過程」了,這種優(yōu)化手段就叫做 Inline Caches (ICs),有點(diǎn)類似 C 語(yǔ)言的 struct 將字段點(diǎn)讀編譯為偏移訪問,只不過這個(gè)過程是 JIT 的,不是 C 那樣 AOT 靜態(tài)編譯確定的,是 V8 在函數(shù)執(zhí)行多次收集了足夠多的 feedback 后實(shí)現(xiàn)的。
你可能還會(huì)問:在調(diào)用優(yōu)化后的 distance2 的時(shí)候具體要怎么確定傳入的 p1 p2 的 shapes 是否有變化?還記得前面那個(gè) 0xABCD 嗎?沒錯(cuò),編譯后的匯編 checkpoint 就是直接判斷傳入對(duì)象的 hidden classs 指針數(shù)值是不是 *0xABCD*,如果不是就觸發(fā)「反優(yōu)化」兜底解釋器模式運(yùn)行即可。
—— 下面這個(gè)實(shí)例將手把手介紹 ICs 的真實(shí)場(chǎng)景以及匯編細(xì)節(jié)
從前面 Inline Cache 的討論中可以得知,必須要確定了訪問的 key 才能做 ICs 優(yōu)化,因此寫代碼的過程中,如有可能請(qǐng)盡量避免下面這樣通過 key string 動(dòng)態(tài)查找對(duì)象屬性:
function test(obj: any, key: string) {
return obj[key];
}
如果能明確知道 key 的具體值,此時(shí)建議寫為:
function test(obj: any, key: 'a' | 'b') {
if (key==='a') return obj.a;
if (key==='b') return obj.b;
}
即使確實(shí)不得不動(dòng)態(tài)查詢,但是你知道某個(gè)子 case 占了 99% 的調(diào)用次數(shù),此時(shí)也可以這樣優(yōu)化:
function test(obj, key: 'a' | 'b') {
// 為 'a' 的調(diào)用次數(shù)占了 99% 可以這樣提前優(yōu)化
if (key==='a') return obj.a;
return obj[key];
}
靜態(tài)和動(dòng)態(tài)兩種寫法風(fēng)格可能會(huì)有幾倍甚至上百倍的差距,如果業(yè)務(wù)里有大幾百萬(wàn)次的調(diào)用 test,優(yōu)化后能省不少毫秒,比如下面這個(gè)「簡(jiǎn)化的服務(wù)發(fā)現(xiàn)」例子有近百倍的差距:
原因是 s2.js 里那些屬性訪問都被 ICs 技術(shù)優(yōu)化成 O(1) 訪問了,速度很快 —— 為了探究?jī)?nèi)部的 ICs 相關(guān)匯編邏輯,嘗試輸出 serviecMap 的 Hidden Class (V8 里 hidden class 別名是 Map) 以及匯編源碼:
首先 %DebugPrint 出 serviceMap 的 Hidden Class 的物理地址,可以看到是 0x3a8d76b74971 然后看后續(xù)編譯優(yōu)化的 arm machine code 是怎么利用這個(gè)地址實(shí)現(xiàn) ICs 技術(shù)優(yōu)化的:(筆者這會(huì)的電腦是 mac m1 因此是 arm 匯編,不是 x86 匯編)。
可以看到,ICs 優(yōu)化后匯編的 checkpoint 其實(shí)就是將 Hidden Map 的指針物理地址直接 Inline 到匯編里了,通過判等的方式來驗(yàn)證假設(shè),然后就可以直接將屬性訪問優(yōu)化為 O(1) 的 in-object properties 訪問了,這也是這個(gè)技術(shù)為什么叫做 Inline Cahce (ICs) 了。
(這幾乎是 V8 里效果最好的優(yōu)化了,也因此部分 benchmark 里 nodejs 對(duì)象可能比 Java 對(duì)象還快,因?yàn)?Java 里有可能濫用反射導(dǎo)致對(duì)象性能非常差)。
如果知道 ICs 技術(shù)內(nèi)涵的話,理解 Fast Properties 和 Slow Properties (或者稱字典模式) 就不會(huì)有困難了。
下圖描述了 JSObject 的主要構(gòu)造:當(dāng)把對(duì)象成員存儲(chǔ)到 in-object properties 的時(shí)候,此時(shí)稱對(duì)象是 Fast Properties 模式,這意味著對(duì)象訪問 V8 會(huì)在合適的時(shí)候?qū)⑵?Inline Cache 到優(yōu)化后的匯編里;反之,當(dāng)成員存儲(chǔ)到 *properties 的時(shí)候,此時(shí)稱為 Slow Properties,此時(shí)就不會(huì)對(duì)這類對(duì)象做 inline cache 優(yōu)化了,此時(shí)對(duì)象訪問性能最差(因?yàn)橐闅v *properties字典,通常慢幾十到幾百倍,取決于對(duì)象成員數(shù)量)。
我們可以用 %HasFastProperties 來打印對(duì)象是否是 Fast Properties 模式,如下圖所示:
delete 會(huì)將對(duì)象轉(zhuǎn)為 slow properties 模式,為什么呢?因?yàn)?delete 帶來的問題可太多了,緩存技術(shù)最怕的就是 delete,如圖所示:
我拍腦子就能想到上面四個(gè)問題,要完整的確保 delete 的安全性可太難了,因此維護(hù) delete 后的 hidden class 非常麻煩,V8 采取的方式是直接將 in-object 釋放掉,然后將對(duì)象屬性都復(fù)制存儲(chǔ)到 *properties 里了,以后這個(gè)對(duì)象就不再開啟 ICs 優(yōu)化了,此時(shí)這種退化后的對(duì)象就稱為 slow properties (或者稱字典模式)。
Hidden Class 是比較學(xué)術(shù)的名字,在 V8 里對(duì)應(yīng)的「工程命名」是 Map,可以在 heapdump 里看到:
利用查找 Hidden Class 的方式可以快速定位大批量相同 shapes 的對(duì)象哦,很方便查找內(nèi)存溢出問題。
跟 C++ 里的 inline 關(guān)鍵字一樣,將函數(shù)直接提前展開,少一次調(diào)用棧和函數(shù)作用域開銷。
基于 Sea Of Nodes 的 PL 理論進(jìn)行優(yōu)化,分析對(duì)象生命周期,如果對(duì)象是一次性的,那么就可以做編譯替換提升性能,比如下圖里對(duì)象 o 只用到了 a,那么就可以優(yōu)化成右邊那樣,減少對(duì)象內(nèi)存分配并提升尋址速度:
通過打 heapdump 的方式可以發(fā)現(xiàn)下面第二行的空對(duì)象的 shallow size 是 28 字節(jié),而后一個(gè)是 16 字節(jié):
window.arr=[]; // 打一次 heapdump
arr.push({}); // 打一次 heapdump
arr.push({ ggg: undefined });
原因:V8 假設(shè)空對(duì)象后面都會(huì)設(shè)置新的 key 上去,因此會(huì)預(yù)先 malloc 了一些 in-object 字段到 JSObject 上,最后就是 28,比 16 要大;而第三行這樣固定就只會(huì) malloc 一個(gè) in-object 字段了(其實(shí)看圖里還有一個(gè) proto 字段)。
那么 new Object() 呢?一樣會(huì);如果是 Object.create(null) 呢?這種情況就不會(huì)申請(qǐng)了,shallow size 此時(shí)最小,為 12 字節(jié)。
28 - 12=16 字節(jié),而一個(gè)指針占 4 字節(jié),因此 V8 對(duì)一個(gè)空對(duì)象會(huì)默認(rèn)為其多創(chuàng)建 4 個(gè) in-object 字段以備后續(xù)使用,而這類預(yù)分配的內(nèi)存空間,會(huì)在下次 GC 的時(shí)候?qū)]用到的回收掉,這項(xiàng)技術(shù)叫做 「Slack Tracking 松弛追蹤」。
v8 里還有很多針對(duì) string / Array 的優(yōu)化技術(shù),本次技術(shù)優(yōu)化主要涉及 ICs 相關(guān)優(yōu)化,就不展開寫了,參見后文鏈接(其實(shí)大部分對(duì)象優(yōu)化技術(shù)都是圍繞 V8 對(duì)象模型來進(jìn)行的)。
Safari 的 WebKit JSCore 引擎也有基于 LLVM 后端的 JIT 技術(shù),因此很多優(yōu)化手段是共通的,比如 safari 也有 type feedback 和屬性追蹤,也有自己的 hidden class / ICs 實(shí)現(xiàn),可以打開 safari 的調(diào)試工具看到運(yùn)行時(shí)的 type feedback:(macOS、iOS、iPadOS 上都有 JIT,在 chrome 上優(yōu)化后全平臺(tái)都能受益)。
在這些優(yōu)化技術(shù)的加持上,safari jscore 某些情況下甚至?xí)?chrome v8 還要快:
大部分業(yè)務(wù)場(chǎng)景里更關(guān)心可維護(hù)性,性能不是最重要的,另外就是面向引擎/底層優(yōu)化邏輯寫的 js 未必是符合最佳實(shí)踐的,有時(shí)候會(huì)顯得非常臟,這里總結(jié)一下個(gè)人遇到的常見實(shí)例對(duì)照,供參考:
熱點(diǎn)函數(shù)會(huì)優(yōu)先走 turbofan 編譯為機(jī)器碼,性能會(huì)更好,要如何利用好這個(gè)特性?將項(xiàng)目里的一些高頻原子操作拆成獨(dú)立函數(shù),人為制造熱點(diǎn)代碼,比如計(jì)算點(diǎn)距離,單位換算等等這些需要高性能的地方:
除了前面提到的熱區(qū)之外,拆解后的函數(shù)如果足夠短,那么 V8 在調(diào)用的時(shí)候會(huì)做 inline 展開優(yōu)化,節(jié)省一次調(diào)用棧開銷。
從前面的 add 的例子我們可以知道,V8 TurboFan 優(yōu)化是基于 assumption 的,應(yīng)該盡量保持函數(shù)的單態(tài)性 (Monomorphic),或者說減少函數(shù)的狀態(tài),具體來說高頻函數(shù)不要傳 Union Types 作為參數(shù)。(這個(gè)不夠準(zhǔn)確,最好是不要打破參數(shù)的 V8 內(nèi)部類型表示以及匯編 checkpoint,比如一會(huì)傳浮點(diǎn)數(shù)、一會(huì)傳 SMI 這樣即使都是 number 也會(huì)打破 v8 的假設(shè),因?yàn)?v8 內(nèi)部實(shí)現(xiàn)的浮點(diǎn)數(shù)會(huì)裝箱,而小整數(shù) SMI 不會(huì),兩者的匯編邏輯不一樣)。
推薦使用 TypeScript 來寫 js 應(yīng)用,限制函數(shù)的入?yún)㈩愋涂梢杂行ПWC函數(shù)的單態(tài)性質(zhì),更容易編寫高性能的 js 代碼
賦值順序的不同會(huì)產(chǎn)生不同的 Hidden Class 鏈,不同的鏈不能做 ICs 優(yōu)化。
class A {
a?: number
}
class A {
a=undefined // 或 null
}
理由跟前一點(diǎn)一樣,前者 A 有 shapes 鏈?zhǔn)?空對(duì)象+a,而后者就是確定的 a 了。
但是,賦值會(huì)多消耗一點(diǎn)內(nèi)存,內(nèi)存敏感型場(chǎng)景慎用。
delete 后會(huì)將對(duì)象轉(zhuǎn)為 Slow Properties 模式,這種模式下的對(duì)象不會(huì)被 inline cache 到優(yōu)化后的匯編機(jī)器碼里,對(duì)性能影響比較大,另外這樣的對(duì)象如果到處傳的話就會(huì)到處觸發(fā)「反優(yōu)化」將污染已經(jīng)優(yōu)化過的代碼。
前面的例子里提到,反優(yōu)化后的函數(shù)再優(yōu)化性能不會(huì)比最開始要好,換言之被「feedback 污染」了,我們應(yīng)當(dāng)盡量避免反優(yōu)化的出現(xiàn)(即 checkpoint 被打破的情況)。
前面已經(jīng)討論過這類情況了,靜態(tài)種寫法 V8 可以做 ICs 優(yōu)化,將屬性訪問直接改為 in-object 訪問,速度可以比動(dòng)態(tài) key 查找快近百倍。
const obj={ a: 1, b: 2 };
const obj={};
obj.a=1;
obj.b=2;
從 Hidden Class 的角度來看,第二種會(huì)使 Hidden Class 變化三次,而第一種直接聲明其實(shí)就隱含了 Hidden Class 了,V8 可以直接提前靜態(tài)分析得出。
v8 會(huì)分析 ast,將左側(cè)優(yōu)化成右側(cè)。
在 React / Vue 里有這種 Ref 構(gòu)造來實(shí)現(xiàn)訪問同一個(gè)實(shí)例的操作(類似指針)
type Ref<T>={
ref: T
}
// React 的是 current 作為 key
type ReactRef<T>={ current: T }
前面提到過的 ICs 優(yōu)化,因此上述這樣的構(gòu)造并不會(huì)造成嚴(yán)重的性能損失,會(huì)多消耗一點(diǎn)內(nèi)存,大多數(shù)情況下可以放心使用(多消耗 16 字節(jié))。
這塊參考了大量資料,有的地方只有源碼里才有,這里簡(jiǎn)單列一下:
另外特別感謝元寶對(duì)我工作的大力支持 ??
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。