JavaScript 可能會讓人感覺與其運行的硬體非常脫離,但低階思考在有限的情況下仍然有用。
Kafeel Ahmad 最近發表的關於循環優化的文章詳細介紹了許多循環性能改進技術。那篇文章讓我思考了這個主題。
為了解決這個問題,這是一種很少有人在 Web 開發中需要考慮的技術。此外,過早關注優化可能會使程式碼更難編寫、更難維護。了解底層技術可以讓我們深入了解我們的工具和一般工作,即使我們無法直接應用這些知識。
循環展開基本上複製了循環內的邏輯,因此您可以在每個循環期間執行多個操作。在特定情況下,使循環中的程式碼更長可以使其更快。
透過有意分組而不是逐一執行某些操作,電腦可能能夠更有效地運作。
讓我們舉一個很簡單的例子:對數組中的值求和。
// 1-to-1 looping const simpleSum = (data) => { let sum = 0; for(let i=0; i < data.length; i += 1) { sum += data[i]; } return sum; }; const parallelSum = (data) => { let sum1 = 0; let sum2 = 0; for(let i=0; i < data.length; i += 2) { sum1 += data[i]; sum2 += data[i + 1]; } return sum1 + sum2; };
乍看之下這可能看起來很奇怪。我們正在管理更多變數並執行簡單範例中不會發生的其他操作。這怎麼可能更快? !
我對各種資料大小和多次運行以及順序或交錯測試進行了一些比較。 parallelSum 的性能各不相同,但幾乎總是更好,除了非常小的資料大小的一些奇怪結果之外。我使用 RunJS 對此進行了測試,它基於 Chrome 的 V8 引擎構建。
不同的資料大小給出了非常粗略的這些結果:
然後我建立了一個包含 100 萬筆記錄的 JSPerf 來嘗試跨不同的瀏覽器。自己嘗試吧!
Chrome 運行 parallelSum 的速度是 simpleSum 的兩倍,正如 RunJS 測試所預期的那樣。
Safari 在百分比和每秒操作數方面幾乎與 Chrome 相同。
同一系統上的 Firefox 對於 simpleSum 的效能幾乎相同,但 parallelSum 只快了 15% 左右,而不是快兩倍。
這種變化讓我尋找更多資訊。雖然這還不是確定的,但我發現了 2016 年的 StackOverflow 評論,討論了循環展開的一些 JS 引擎問題。這是對引擎和優化如何以我們意想不到的方式影響程式碼的有趣觀察。
我也嘗試了第三個版本,它在一次操作中添加了兩個值,以查看一個變數和兩個變數之間是否存在明顯差異。
const parallelSum = (data) => { let sum = 0 for(let i=0; i < data.length; i += 2) { sum += data[i] + data[i + 1]; } return sum; };
簡短回答:不。兩個「並行」版本在彼此報告的誤差範圍內。
雖然 JavaScript 是單執行緒的,但是當滿足某些條件時,底層的解釋器、編譯器和硬體可以為我們執行最佳化。
在簡單的範例中,操作需要 i 值來知道要取得哪些數據,並且需要更新 sum 的最新值。由於這兩者在每個循環中都會發生變化,因此電腦必須等待循環完成才能獲取更多資料。雖然 i += 1 的作用對我們來說似乎是顯而易見的,但計算機大多理解“值會改變,稍後再檢查”,因此它很難優化。
我們的並行版本為 i 的每個值載入多個資料條目。我們仍然依賴每個循環的總和,但每個週期我們可以載入和處理兩倍的資料。但這並不意味著它的運行速度兩倍。
為了理解為什麼循環展開有效,我們研究電腦的低階操作。具有超標量架構的處理器可以有多個管道來執行同時操作。它們可以支援無序執行,因此彼此不依賴的操作可以盡快發生。對於某些操作,SIMD 可以同時對多個資料執行一項操作。除此之外,我們開始討論快取、資料取得和分支預測......
但這是一篇 JavaScript 文章!我們不會走得那麼深。如果您想了解有關處理器架構的更多信息,Anandtech 有一些出色的深度潛水。
循環展開並不是魔法。由於程式或資料大小、操作複雜性、電腦體系結構等原因,會出現限制和收益遞減。但我們只測試了一兩個操作,現代電腦通常支援四個或更多執行緒。
為了嘗試一些更大的增量,我製作了另一個包含1、2、4 和10 條記錄的JSPerf,並在運行macOS 14.5 Sonoma 的Apple M1 Max MacBook Pro 和運行Windows 11 的AMD Ryzen 9 3950X PC 上運作。
一次 10 筆記錄比基本循環快 2.5-3.5 倍,但僅比在 Mac 上處理 4 筆記錄快 12-15%。在 PC 上,我們仍然看到 1 到 2 筆記錄之間的效能提高了 2 倍,但 10 筆記錄僅比 4 筆記錄快 2%,這對於 16 核心處理器來說是我無法預測的。
這些不同的結果提醒我們要小心最佳化。針對電腦進行最佳化可能會在功能較差或不同的硬體上帶來更糟糕的體驗。當開發人員在快速、強大的機器上工作時,較舊或入門級硬體的效能或功能問題是一個常見問題,而且我在職業生涯中多次遇到這個問題。
對於某些效能規模,HP 目前提供的入門級 Chromebook 配備 Intel Celeron N4120 處理器。這大致相當於我的 2013 Core i5-4250U MacBook Air。在綜合基準測試中,它的表現僅為 M1 Max 的九分之一。在運行最新版本 Chrome 的 2013 年 MacBook Air 上,4 記錄功能比 10 記錄功能快,但仍然只比單記錄功能快 60%!
瀏覽器和標準也在不斷變化。例行的瀏覽器更新或不同的處理器架構可能會使最佳化的程式碼比常規循環慢。當您發現自己進行深度最佳化時,您可能需要確保您的最佳化與消費者相關,並且保持相關性。
這讓我想起了 Nicholas Zakas 寫的《高效能 JavaScript》一書,我在 2012 年讀過這本書。這是一本很棒的書,包含了許多見解。然而,到 2014 年,書中指出的許多重大效能問題已透過瀏覽器引擎更新得到解決或大幅減少,我們能夠將更多精力集中在編寫可維護的程式碼上。
如果您想保持效能最佳化的領先地位,請為變更和定期驗證做好準備。
在研究這個主題時,我遇到了 2000 年的 Linux 核心郵件列表主題,內容涉及刪除一些循環展開優化,最終提高了應用程式效能。它包括這個仍然相關的點(強調我的):
最重要的是,我們對什麼快、什麼慢的直觀假設常常是錯誤的,特別是考慮到過去幾年 CPU 發生了多大的變化。
– Theodore Ts'o
有時您可能需要從循環中擠出效能,如果您正在處理足夠的項目,這可能是您這樣做的方法之一。了解這類最佳化固然很好,但對於大多數工作來說,您並不需要它™。
不過,我希望您喜歡我的漫談,也許將來您會記住有關效能優化注意事項的資訊。
感謝您的閱讀!
以上是JavaScript 中的循環展開?的詳細內容。更多資訊請關注PHP中文網其他相關文章!