簡體   English   中英

為什么使用window.variable訪問變量的速度較慢?

[英]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 )之后,這些訪問方法中哪一種訪問速度最快,為什么?

  1. 直接在全球范圍內

     x = x + 1; 
  2. 直接在全局范圍內,但以window為前綴

     window.x = window.x + 1; 
  3. 函數內部,不合格

     function accessUnqualified() { x = x + 1; } 
  4. 在函數內部,帶有window前綴

     function accessWindowPrefix() { window.x = window.x + 1; } 
  5. 在函數內部,將緩存窗口作為變量進行前綴訪問(模擬IIFE的本地參數)。

     function accessCacheWindow() { var global = window; global.x = global.x + 1; } 
  6. 在IIFE(作為參數的窗口)內,有前綴訪問。

      (function(global){ global.x = global.x + 1; })(window); 
  7. 在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)。 但是為什么呢?
  • 在函數中訪問全局變量(可能是額外的作用域查找)更快(#3 vs#1)。 為什么
  • 為什么在兩個瀏覽器中#5,#6,#7結果如此不同?

我了解有些人認為這樣的測試對性能調整毫無意義,這很可能是正確的。 但是,請您出於知識的考慮而幽默,並幫助我更好地理解這些簡單的概念,例如變量訪問和作用域鏈。

如果您已經閱讀了本文,則感謝您的耐心配合。 很長的道歉,或者可能將多個問題歸納為一個道歉-我認為它們都有些相關。


編輯:根據要求共享我的基准代碼。

 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.xx慢,甚至比其他任何東西都慢。

這使我想到了x = 1;前提x = 1; window.x = 1;相同window.x = 1; 很抱歉告訴您這是錯誤的。 FWIW window不直接是全局對象,它既是其屬性,又是對其的引用。 嘗試window.window.window.window ...

環境記錄

每個變量都必須在環境記錄中 “注冊”,並且有兩種主要類型:聲明性和對象性。

功能范圍使用聲明性環境記錄。

全局范圍使用對象環境記錄。 這意味着該范圍內的每個變量也是對象的屬性,在這種情況下為全局對象。

此相反,它也可以工作:可以通過具有相同名稱的標識符訪問該對象的每個屬性。 但這並不意味着您正在處理一個變量。 with語句是使用對象環境記錄的另一個示例。

x = 1和window.x = 1之差

創建變量與向對象添加屬性不同,即使該對象是環境記錄也是如此。 在兩種情況下均嘗試使用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的變量時,它需要確定該變量是否在:

  • 當地范圍
  • 關閉范圍(例如IIFE創建的范圍)
  • 全球范圍

“作用域鏈”實質上是這些范圍的鏈接列表。 查找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並獲得相同的結果。 但是,當xwindow.location.xlocation也是一個大型本機對象)時,時間發生了巨大變化:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM