1 回答

TA貢獻(xiàn)1817條經(jīng)驗 獲得超6個贊
有趣的!從代碼來看,很明顯 IIFE 包裝的版本應(yīng)該更慢,而不是更快:在每次循環(huán)迭代中,它都會創(chuàng)建一個新的函數(shù)對象并調(diào)用它(優(yōu)化編譯器最終會避免這種情況,但這并不會)不會立即啟動),所以通常只是做更多的工作,這應(yīng)該花費(fèi)更多的時間。
本例中的解釋是內(nèi)聯(lián)。
一點(diǎn)背景知識:將一個函數(shù)內(nèi)聯(lián)到另一個函數(shù)中(而不是調(diào)用它)是優(yōu)化編譯器為了實(shí)現(xiàn)更好的性能而執(zhí)行的標(biāo)準(zhǔn)技巧之一。不過,它是一把雙刃劍:從好的方面來說,它避免了調(diào)用開銷,并且通??梢詫?shí)現(xiàn)進(jìn)一步的優(yōu)化,例如恒定傳播或消除重復(fù)計算(請參閱下面的示例)。不利的一面是,它會導(dǎo)致編譯時間更長(因為編譯器做了更多工作),并且會導(dǎo)致生成更多代碼并將其存儲在內(nèi)存中(因為內(nèi)聯(lián)函數(shù)實(shí)際上會重復(fù)它),并且在像 JavaScript 這樣的動態(tài)語言中,優(yōu)化的代碼通常依賴于受保護(hù)的假設(shè),
一般來說,做出完美的內(nèi)聯(lián)決策(不要太多,也不要太少)需要預(yù)測未來:提前知道代碼執(zhí)行的頻率和參數(shù)。當(dāng)然,這是不可能的,因此優(yōu)化編譯器使用各種規(guī)則/“啟發(fā)式”來猜測什么可能是一個相當(dāng)好的決定。
V8 當(dāng)前的一項規(guī)則是:不要內(nèi)聯(lián)遞歸調(diào)用。
這就是為什么在代碼的簡單版本中,add
不會內(nèi)聯(lián)到自身中。IIFE 版本本質(zhì)上有兩個相互調(diào)用的函數(shù),這被稱為“相互遞歸”——事實(shí)證明,這個簡單的技巧足以欺騙 V8 的優(yōu)化編譯器并使其回避“不要內(nèi)聯(lián)遞歸調(diào)用”規(guī)則。相反,它愉快地將未命名的 lambda 內(nèi)聯(lián)到add
,然后add
內(nèi)聯(lián)到未命名的 lambda 中,依此類推,直到大約 30 輪后其內(nèi)聯(lián)預(yù)算用完。(旁注:“內(nèi)聯(lián)多少”是有點(diǎn)復(fù)雜的啟發(fā)法之一,特別是考慮到函數(shù)大小,因此我們在這里看到的任何特定行為確實(shí)是針對這種情況的。)
在這種特定場景中,所涉及的函數(shù)非常小,內(nèi)聯(lián)很有幫助,因為它避免了調(diào)用開銷。因此,在這種情況下,內(nèi)聯(lián)提供了更好的性能,即使它是遞歸內(nèi)聯(lián)的(偽裝的)情況,這通常通常對性能不利。它確實(shí)是有代價的:在簡單版本中,優(yōu)化編譯器只花費(fèi) 3 毫秒進(jìn)行編譯add
,為其生成 562 字節(jié)的優(yōu)化代碼。在 IIFE 版本中,編譯器花費(fèi) 30 毫秒并生成 4318 字節(jié)的優(yōu)化代碼add
。這就是為什么它不像“V8 應(yīng)該總是內(nèi)聯(lián)更多”那么簡單的原因之一:編譯的時間和電池消耗很重要,內(nèi)存消耗也很重要,以及簡單的 10 行代碼中可接受的成本(并顯著提高性能)在 100,000 行應(yīng)用程序中,演示很可能會產(chǎn)生不可接受的成本(甚至可能會影響整體性能)。
現(xiàn)在,了解了發(fā)生了什么之后,我們可以回到“IIFE 有開銷”的直覺,并制作一個更快的版本:
function add(n,m) {
return add_inner(n, m);
};
function add_inner(n, m) {
return n === 0 ? m : add(n - 1, m) + 1;
}
在我的機(jī)器上,我看到:
簡單版本:1650 毫秒
IIFE 版本:720 毫秒
add_inner 版本:460 毫秒
當(dāng)然,如果您add(n, m)
簡單地實(shí)現(xiàn)為return n + m
,那么它會在 2 毫秒內(nèi)終止——算法優(yōu)化勝過優(yōu)化編譯器可能完成的任何事情:-)
附錄:優(yōu)化好處的示例??紤]這兩個函數(shù):
function Process(x) {
return (x ** 2) + InternalDetail(x, 0, 2);
}
function InternalDetail(x, offset, power) {
return (x + offset) ** power;
}
(顯然,這是愚蠢的代碼;但我們假設(shè)它是在實(shí)踐中有意義的東西的簡化版本。)
當(dāng)天真地執(zhí)行時,會發(fā)生以下步驟:
評價
temp1 = (x ** 2)
帶
InternalDetail
參數(shù)調(diào)用x
,0
,2
評價
temp2 = (x + 0)
評價
temp3 = temp2 ** 2
返回
temp3
給調(diào)用者評價
temp4 = temp1 + temp3
返回
temp4
。
如果優(yōu)化編譯器執(zhí)行內(nèi)聯(lián),那么第一步它將得到:
function Process_after_inlining(x) { return (x ** 2) + ( (x + 0) ** 2 ); }
它允許兩種簡化:x + 0
可以折疊為x
,然后x ** 2
計算發(fā)生兩次,因此可以通過重用第一次的結(jié)果來替換第二次:
function Process_with_optimizations(x) { let temp1 = x ** 2; return temp1 + temp1; }
因此,與簡單的執(zhí)行相比,我們從 7 個步驟減少到了 3 個步驟:
評價
temp1 = (x ** 2)
評價
temp2 = temp1 + temp1
返回
temp2
我并不是預(yù)測實(shí)際性能會從 7 個時間單位變?yōu)?3 個時間單位;這只是為了直觀地說明為什么內(nèi)聯(lián)可以幫助減少一定量的計算負(fù)載。
腳注:為了說明所有這些東西是多么棘手,請考慮在 JavaScript 中用x + 0
just替換x
并不總是可能的,即使編譯器知道它x
總是一個數(shù)字:如果x
碰巧是-0
,那么添加0
到它會將其更改為+0
,這很可能是可觀察的程序行為;-)
添加回答
舉報