[英]Why is accessing a variable using window.variable slower?
JS性能提示的多種來源鼓勵開發人員減少“范圍鏈查找”。 例如,當您訪問全局變量時,IIFE被吹捧為具有“減少范圍鏈查找”的額外好處。 這聽起來很合乎邏輯,甚至是理所當然的,所以我沒有質疑它的智慧。 像許多其他人一樣,我一直很高興地使用IIFE,認為除了避免全局名稱空間污染之外,與任何全局代碼相比,性能都將得到提高。
我們今天的期望:
(function($, window, undefined) {
// apparently, variable access here is faster than outside the IIFE
})(jQuery, window);
將其簡化/擴展為一般情況,人們會期望:
var x = 0;
(function(window) {
// accessing window.x here should be faster
})(window);
根據我對JS的了解, x = 1;
之間沒有區別x = 1;
和window.x = 1;
在全球范圍內。 因此,期望他們表現出色是合乎邏輯的,對嗎? 錯誤。 我進行了一些測試,發現訪問時間存在顯着差異。
好吧,也許如果我把window.x = 1;
在IIFE中,它應該運行得更快(即使只是一點點),對嗎? 又錯了。
好的,也許是Firefox; 讓我們嘗試使用Chrome(V8是JS速度的基准,是嗎?),它應該在Firefox方面勝於Firefox,例如直接訪問全局變量,對嗎? 再次錯誤 。
因此,我着手確切地找出在兩種瀏覽器中哪種訪問方法最快。 假設我們從一行代碼開始: var x = 0;
。 在聲明了x
(並愉快地將其附加到window
)之后,這些訪問方法中哪一種訪問速度最快,為什么?
直接在全球范圍內
x = x + 1;
直接在全局范圍內,但以window
為前綴
window.x = window.x + 1;
函數內部,不合格
function accessUnqualified() { x = x + 1; }
在函數內部,帶有window
前綴
function accessWindowPrefix() { window.x = window.x + 1; }
在函數內部,將緩存窗口作為變量進行前綴訪問(模擬IIFE的本地參數)。
function accessCacheWindow() { var global = window; global.x = global.x + 1; }
在IIFE(作為參數的窗口)內,有前綴訪問。
(function(global){ global.x = global.x + 1; })(window);
在IIFE(作為參數的窗口)中,沒有資格的訪問。
(function(global){ x = x + 1; })(window);
請假定瀏覽器上下文,即window
是全局變量。
我編寫了一個快速的時間測試來循環執行增量操作一百萬次,並對結果感到驚訝。 我發現:
Firefox Chrome
------- ------
1. Direct access 848ms 1757ms
2. Direct window.x 2352ms 2377ms
3. in function, x 338ms 3ms
4. in function, window.x 1752ms 835ms
5. simulate IIFE global.x 786ms 10ms
6. IIFE, global.x 791ms 11ms
7. IIFE, x 331ms 655ms
我重復了幾次測試,這些數字似乎是指示性的。 但是他們似乎使我感到困惑:
window
前綴要慢得多(#2 vs#1,#4 vs#3)。 但是為什么呢? 我了解有些人認為這樣的測試對性能調整毫無意義,這很可能是正確的。 但是,請您出於知識的考慮而幽默,並幫助我更好地理解這些簡單的概念,例如變量訪問和作用域鏈。
如果您已經閱讀了本文,則感謝您的耐心配合。 很長的道歉,或者可能將多個問題歸納為一個道歉-我認為它們都有些相關。
編輯:根據要求共享我的基准代碼。
var x, startTime, endTime, time; // Test #1: x x = 0; startTime = Date.now(); for (var i=0; i<1000000; i++) { x = x + 1; } endTime = Date.now(); time = endTime - startTime; console.log('access x directly - Completed in ' + time + 'ms'); // Test #2: window.x x = 0; startTime = Date.now(); for (var i=0; i<1000000; i++) { window.x = window.x + 1; } endTime = Date.now(); time = endTime - startTime; console.log('access window.x - Completed in ' + time + 'ms'); // Test #3: inside function, x x =0; startTime = Date.now(); accessUnqualified(); endTime = Date.now(); time = endTime - startTime; console.log('accessUnqualified() - Completed in ' + time + 'ms'); // Test #4: inside function, window.x x =0; startTime = Date.now(); accessWindowPrefix(); endTime = Date.now(); time = endTime - startTime; console.log('accessWindowPrefix()- Completed in ' + time + 'ms'); // Test #5: function cache window (simulte IIFE), global.x x =0; startTime = Date.now(); accessCacheWindow(); endTime = Date.now(); time = endTime - startTime; console.log('accessCacheWindow() - Completed in ' + time + 'ms'); // Test #6: IIFE, window.x x = 0; startTime = Date.now(); (function(window){ for (var i=0; i<1000000; i++) { window.x = window.x+1; } })(window); endTime = Date.now(); time = endTime - startTime; console.log('access IIFE window - Completed in ' + time + 'ms'); // Test #7: IIFE x x = 0; startTime = Date.now(); (function(global){ for (var i=0; i<1000000; i++) { x = x+1; } })(window); endTime = Date.now(); time = endTime - startTime; console.log('access IIFE x - Completed in ' + time + 'ms'); function accessUnqualified() { for (var i=0; i<1000000; i++) { x = x+1; } } function accessWindowPrefix() { for (var i=0; i<1000000; i++) { window.x = window.x+1; } } function accessCacheWindow() { var global = window; for (var i=0; i<1000000; i++) { global.x = global.x+1; } }
由於eval
(可以訪問本地框架!),因此Javascript難以優化。
但是,如果編譯器足夠聰明,可以檢測到eval
那么事情就會變得更快。
如果只有局部變量,捕獲的變量和全局變量,並且可以假設不對eval
進行任何改動,那么理論上:
原因是,如果在查找時x
導致局部或全局,則x
始終是局部或全局,因此可以直接使用mov rax, [rbp+0x12]
(當為局部)或mov rax, [rip+0x12345678]
全局時為mov rax, [rip+0x12345678]
。 沒有任何查找。
對於捕獲的變量,由於存在生命周期的問題,事情會稍微復雜一些。 在一個非常常見的實現中(捕獲的變量包裝在單元格中,並在創建閉包時復制單元格),這將需要兩個額外的間接步驟……例如
mov rax, [rbp] ; Load closure data address in rax
mov rax, [rax+0x12] ; Load cell address in rax
mov rax, [rax] ; Load actual value of captured var in rax
再一次在運行時不需要“查找”。
所有這些意味着您正在觀察的時間是其他因素的結果。 對於單純的變量訪問,與諸如緩存或實現細節(例如,垃圾回收器的實現方式;例如移動一個垃圾回收器)之類的其他問題相比,局部變量,全局變量和捕獲變量之間的差異非常小。 )。
當然,使用window
對象訪問全局對象是另一回事...而且我並不感到驚訝,因為它需要更長的時間( window
也必須是常規對象)。
當我在Chrome中運行您的代碼段時,除了直接訪問window.x
之外,其他所有方法都需要花費幾毫秒的window.x
。 毫不奇怪,使用對象屬性比使用變量要慢。 因此,唯一要回答的問題是為什么window.x
比x
慢,甚至比其他任何東西都慢。
這使我想到了x = 1;
前提x = 1;
與window.x = 1;
相同window.x = 1;
。 很抱歉告訴您這是錯誤的。 FWIW window
不直接是全局對象,它既是其屬性,又是對其的引用。 嘗試window.window.window.window ...
每個變量都必須在環境記錄中 “注冊”,並且有兩種主要類型:聲明性和對象性。
功能范圍使用聲明性環境記錄。
全局范圍使用對象環境記錄。 這意味着該范圍內的每個變量也是對象的屬性,在這種情況下為全局對象。
與此相反,它也可以工作:可以通過具有相同名稱的標識符訪問該對象的每個屬性。 但這並不意味着您正在處理一個變量。 with
語句是使用對象環境記錄的另一個示例。
創建變量與向對象添加屬性不同,即使該對象是環境記錄也是如此。 在兩種情況下均嘗試使用Object.getOwnPropertyDescriptor(window, 'x')
。 當x
是變量時,則屬性x
是不可configurable
。 結果之一是您無法刪除它。
當我們僅看到window.x
我們不知道它是變量還是屬性。 因此,如果沒有進一步的知識,我們根本無法將其視為變量。 您可以在堆棧中的范圍內使用變量。 編譯器可以檢查是否還有變量x
但是檢查可能比僅僅執行window.x = window.x + 1
要花費更多。 並且不要忘記該window
僅存在於瀏覽器中。 JavaScript引擎也可以在其他環境中工作,這些環境可能具有不同的名稱屬性,甚至根本沒有屬性。
現在,為什么window.x
在Chrome上這么慢? 有趣的是,Firefox並非如此。 在我的測試運行中,FF的速度要快得多,並且window.x
的性能與其他所有對象訪問都相當。 Safari也是如此。 因此,這可能是Chrome的問題。 通常,訪問環境記錄對象的速度很慢,而在這種特定情況下,其他瀏覽器僅會進行更好的優化。
需要注意的一件事是,測試微優化不再容易,因為JS引擎的JIT編譯器將優化代碼。 您的某些測試時間極短,可能是由於編譯器刪除了“未使用”的代碼並展開了循環。
因此,實際上有兩件事情需要擔心“作用域鏈查找”和妨礙JIT編譯器編譯或簡化代碼的代碼。 (后者非常復雜,因此您最好閱讀一些技巧,然后再留意。)
范圍鏈的問題是,當JS引擎遇到類似x
的變量時,它需要確定該變量是否在:
“作用域鏈”實質上是這些范圍的鏈接列表。 查找x
需要首先確定它是否是局部變量。 如果不是,則走任何封閉處並在每個封閉處尋找它。 如果沒有關閉,則在全局上下文中查看。
在以下代碼示例中, console.log(a);
首先嘗試在innerFunc()的本地范圍內解析a
。 它找不到局部變量a
因此它在其封閉的閉包中查找,也找不到變量a
。 (如果還有其他嵌套的回調導致更多的關閉,則必須檢查它們中的每個)在任何關閉中均未找到a
之后,它最終會在全局范圍內查找並在此處找到它。
var a = 1; // global scope
(function myIife(window) {
var b = 2; // scope in myIife and closure due to reference within innerFunc
function innerFunc() {
var c = 3;
console.log(a);
console.log(b);
console.log(c);
}
// invoke innerFunc
innerFunc();
})(window);
恕我直言(不幸的是,我找不到任何方法來證明它的正確與否),這與以下事實有關: window
不僅是全局范圍,而且還是具有大量屬性的本機對象。
我觀察到,在一次通過該引用訪問的循環中,一次又一次地存儲對window
引用的情況下,情況更快。 在window
參與左側(LHS)查找的情況下,循環中的每次迭代都慢得多。
為什么所有情況的時機都不一樣的問題仍然存在,但這顯然是由於js引擎優化所致。 一種說法是不同的瀏覽器顯示不同的時間比例。 可以通過以下假設來解釋最奇怪的贏家#3:由於這種用法得到了很好的優化。
我對測試進行了一些修改,得到了以下結果。 將window.x
移至window.obj.x
並獲得相同的結果。 但是,當x
在window.location.x
( location
也是一個大型本機對象)時,時間發生了巨大變化:
1. access x directly - Completed in 4278ms
2. access window.x - Completed in 6792ms
3. accessUnqualified() - Completed in 4109ms
4. accessWindowPrefix()- Completed in 6563ms
5. accessCacheWindow() - Completed in 4489ms
6. access IIFE window - Completed in 4326ms
7. access IIFE x - Completed in 4137ms
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.