2010-08-28 93 views
6

我一直在研究JavaScript的性能。我瞭解到,當訪問不止一次時,通常最好將閉包變量和類成員複製到本地範圍以加快速度。例如:微調性能時,多次調用JavaScript方法的最佳方式是什麼?

var i = 100; 
var doSomething = function() { 
    var localI = i; 
    // do something with localI a bunch of times 

    var obj = { 
     a: 100 
    }; 
    var objA = obj.a; 
    // do something with objA a bunch of times 
}; 

我明白這一點;它爲解釋者添加了一個按名稱查找價值的快捷方式。處理方法時,這個概念變得很不清楚。起初,我認爲它會以同樣的方式工作。例如:

var obj = { 
    fn: function() { 
     // Do something 
     return this.value; 
    }, 
    value: 100 
}; 
var objFn = obj.fn 
objFn(); 
// call objFn a bunch of times 

事實上,這根本不起作用。訪問像這樣的方法將其從其範圍中移除。當它到達this.value行時,這指的是窗口對象,this.value可能是未定義的。而不是直接調用objFn並丟失作用域,我可以通過objFn.call(obj)將它的作用域傳回給它,但是這樣做會比原來的obj.fn()更好或更差嗎?

我決定寫一個腳本來測試這個,我得到了非常混亂的結果。該腳本對多次循環上述函數調用的多次測試進行迭代。每次測試的平均時間輸出到身體。

一個對象是用很多簡單的方法創建的。額外的方法來確定解釋者是否必須更加努力地找到一個特定的方法。

測試1簡單地調用this.a();
測試2創建一個局部變量a = this.a然後調用a.call(this);
測試3使用YUI的綁定函數創建一個局部變量來保留範圍。我評論了這一點。由YUI創建的額外函數調用使這種方式變得更慢。

測試4,5和6是1,2,3的副本,除了使用z而不是a。

YUI的後期功能用於防止失控腳本錯誤。時間在實際的測試方法中完成,所以setTimeouts不應該影響結果。每個函數總共被稱爲10000000次。 (如果要運行測試,可以輕鬆配置)。

這是我用來測試的整個XHTML文檔。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xml:lang="en" dir="ltr"> 
    <head> 
     <script type="text/javascript" src="http://yui.yahooapis.com/combo?3.1.2/build/yui/yui-min.js"></script> 
     <script> 
      YUI().use('node', function (Y) { 
       var o = { 
        value: '', 
        a: function() { 
         this.value += 'a'; 
        }, 
        b: function() { 
         this.value += 'b'; 
        }, 
        c: function() { 
         this.value += 'c'; 
        }, 
        d: function() { 
         this.value += 'd'; 
        }, 
        e: function() { 
         this.value += 'e'; 
        }, 
        f: function() { 
         this.value += 'f'; 
        }, 
        g: function() { 
         this.value += 'g'; 
        }, 
        h: function() { 
         this.value += 'h'; 
        }, 
        i: function() { 
         this.value += 'i'; 
        }, 
        j: function() { 
         this.value += 'j'; 
        }, 
        k: function() { 
         this.value += 'k'; 
        }, 
        l: function() { 
         this.value += 'l'; 
        }, 
        m: function() { 
         this.value += 'm'; 
        }, 
        n: function() { 
         this.value += 'n'; 
        }, 
        o: function() { 
         this.value += 'o'; 
        }, 
        p: function() { 
         this.value += 'p'; 
        }, 
        q: function() { 
         this.value += 'q'; 
        }, 
        r: function() { 
         this.value += 'r'; 
        }, 
        s: function() { 
         this.value += 's'; 
        }, 
        t: function() { 
         this.value += 't'; 
        }, 
        u: function() { 
         this.value += 'u'; 
        }, 
        v: function() { 
         this.value += 'v'; 
        }, 
        w: function() { 
         this.value += 'w'; 
        }, 
        x: function() { 
         this.value += 'x'; 
        }, 
        y: function() { 
         this.value += 'y'; 
        }, 
        z: function() { 
         this.value += 'z'; 
        }, 
        reset: function() { 
         this.value = ''; 
        }, 
        test1: function (length) { 
         var time = new Date().getTime(); 

         while ((length -= 1)) { 
          this.a(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test2: function (length) { 
         var a = this.a, 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          a.call(this); 
         } 
         return new Date().getTime() - time; 
        }, 
        test3: function (length) { 
         var a = Y.bind(this.a, this), 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          a(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test4: function (length) { 
         var time = new Date().getTime(); 

         while ((length -= 1)) { 
          this.z(); 
         } 
         return new Date().getTime() - time; 
        }, 
        test5: function (length) { 
         var z = this.z, 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          z.call(this); 
         } 
         return new Date().getTime() - time; 
        }, 
        test6: function (length) { 
         var z = Y.bind(this.z, this), 
         time = new Date().getTime(); 

         while ((length -= 1)) { 
          z(); 
         } 
         return new Date().getTime() - time; 
        } 
       }, 
       iterations = 100, iteration = iterations, length = 100000, 
       t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, body = Y.one('body'); 

       body.set('innerHTML', '<span>Running ' + iterations + ' Iterations&hellip;</span>'); 
       while ((iteration -= 1)) { 
        Y.later(1, null, function (iteration) { 
         Y.later(1, null, function() { 
          o.reset(); 
          t1 += o.test1(length); 
         }); 
         Y.later(1, null, function() { 
          o.reset(); 
          t2 += o.test2(length); 
         }); 
         /*Y.later(1, null, function() { 
          o.reset(); 
          t3 += o.test3(length); 
         });*/ 
         Y.later(1, null, function() { 
          o.reset(); 
          t4 += o.test4(length); 
         }); 
         Y.later(1, null, function() { 
          o.reset(); 
          t5 += o.test5(length); 
         }); 
         /*Y.later(1, null, function() { 
          o.reset(); 
          t6 += o.test6(length); 
         });*/ 
         if (iteration === 1) { 
          Y.later(10, null, function() { 
           t1 /= iterations; 
           t2 /= iterations; 
           //t3 /= iterations; 
           t4 /= iterations; 
           t5 /= iterations; 
           //t6 /= iterations; 

           //body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 3: a();</dt><dd>' + t3 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd><dt>Test 6: z();</dt><dd>' + t6 + '</dd></dl>'); 
           body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd></dl>'); 
          }); 
         } 
        }, iteration); 
       } 
      }); 
     </script> 
    </head> 
    <body> 
    </body> 
</html> 

我已經在三個不同的瀏覽器中使用Windows 7運行此操作。這些結果以毫秒爲單位。

火狐3.6.8

Test 1: this.a(); 
    9.23 
Test 2: a.call(this); 
    9.67 
Test 4: this.z(); 
    9.2 
Test 5: z.call(this); 
    9.61 

的Chrome 7.0.503.0

Test 1: this.a(); 
    5.25 
Test 2: a.call(this); 
    4.66 
Test 4: this.z(); 
    3.71 
Test 5: z.call(this); 
    4.15 

的Internet Explorer 8

Test 1: this.a(); 
    168.2 
Test 2: a.call(this); 
    197.94 
Test 4: this.z(); 
    169.6 
Test 5: z.call(this); 
    199.02 

Firefox和Internet Explorer所產生如何我預期的結果。測試1和測試4相對接近,測試2和測試5相對接近,測試2和測試5比測試1和測試4需要更長的時間,因爲需要額外的函數調用來處理。 Chrome我一點也不懂,但速度要快得多,也許對亞毫秒性能的調整是不必要的。

有沒有人對結果有很好的解釋?什麼是多次調用JavaScript方法的最佳方式?

+2

可以添加此鏈接在你的問題,以便其他用戶可以運行測試本身? - http://jsfiddle.net/Lbbx5/ – Anurag 2010-08-28 21:34:42

+1

相關閱讀:[JavaScript Widgets Without「this」](http://michaux.ca/articles/javascript-widgets-without-this) – 2010-08-28 21:41:38

+1

僅供參考,使用Chromium(Linux)在我的(相對較慢的)系統上,經過幾次測試後,結果與Firefox中的結果大致相同(僅更快):組合1/4和2/5或多或少接近,2和5比1和4. – 2010-08-28 21:50:24

回答

1

那麼,只要你的網站有IE8用戶作爲訪問者,這是非常不相干的。使用1或3(用戶不會看到區別)。

「爲什麼」問題可能沒有一個好的答案。當涉及到優化時,這些腳本引擎可能將重點放在優化他們在現實生活中發生的場景,在這些場景中,優化可以被證明是正確的,並且它在哪裏產生了變化,並且使其失效最少量的測試。

+0

在任何必須迭代數百萬次的實際應用程序中,它可能會調用比value + ='a'更復雜的函數,並且每次迭代可能會調用多個函數。在這個測試中,Internet Explorer上大約三十毫秒的優化可能是無關緊要的,但是當應用程序需要更多時間來實際執行某些操作並且30毫秒乘以幾十個函數調用時,它會加起來非常明顯的延遲。 – Killthesand 2010-08-29 15:01:36

2

只是理論化,所以藉此與一粒鹽...

Chrome的JavaScript引擎V8,使用稱爲隱類的優化技術。基本上它構造了靜態對象,這些靜態對象會影響動態JavaScript對象,其中每個屬性/方法都映射到一個固定的內存地址,該地址可以被immedital引用而不需要昂貴的查表操作。每當Javascript對象添加/刪除屬性時,都會創建一個新的隱藏類。

我對Chrome測試結果的理論是,在免費的局部變量中引用函數可以打破隱藏的類關係。雖然引用局部變量可能也不需要查找表,但現在必須執行額外的步驟來重新分配「this」變量。對於一個隱藏類的方法,'this'是一個固定值,所以可以在沒有這個步驟的情況下調用它。

再次只是理論。可能值得進行一次測試,以測試Chrome中局部變量引用與object.member引用之間的差異,以查看後者對性能的影響是否顯着低於其他瀏覽器,可能是因爲隱藏類。

+0

V8引擎中的隱藏類的概念使這更加陌生。爲什麼調用this.a()比調用this.z()更加昂貴?調用a.call(this)比調用this.a()要便宜,但調用z.call(this)比調用this.z()更昂貴。他們是否反向遍歷成員而不是解引用內存指針?我想知道這些結果是否可靠。我們的規模爲3-5毫秒;我大概可以在屏幕上移動鼠標並扭曲這些結果。我會在chrome上嘗試大量的迭代,看看它是如何工作的。 – Killthesand 2010-08-29 15:12:33

+0

我在Chrome上運行了另一個測試,1000次迭代,每次有100萬次調用。運行需要相當長的時間,但提供了更加一致的結果。 測試1:this.a(); 168.475 測試2:a.call(this); 172.069 測試4:this.z(); 168.936 測試5:z.call(this); 173.012 – Killthesand 2010-08-29 17:07:40

相關問題