當前位置:
首頁 > 科技 > 2 行代碼,將.NET 執行時間降低 87%!

2 行代碼,將.NET 執行時間降低 87%!

作者 | STEVE GORDON

譯者 | 彎月,責編 | 屠敏

頭圖 | CSDN 下載自東方 IC

出品 | CSDN(ID:CSDNnews)

以下為譯文:

長期以來,我一直在致力於提高性能,並且努力避免在關鍵代碼路徑中進行內存分配。例如,使用Span在解析數據時避免內存分配,以及使用ArrayPool避免為臨時緩衝區分配數組。這樣的修改雖然對性能有好處,但會增加新版本代碼的維護難度。

在本文中,我想展示的性能優化並不需要大量複雜的代碼修改。有時候,有些簡單的修改也能在提升性能上有出色的表現。下面我們就來看一個這樣的例子。

找出優化的對象

最近,我在研究Elasticsearch.NET客戶端代碼庫。我對庫中某些熱路徑的性能感到好奇。

給應用程序性能分析方面的新手解釋一下,熱路徑就是在正常的使用過程中被頻繁調用的一系列方法。例如,Web應用程序中可能有一個端點,與所有其他端點相比,該端點在生產環境中被調用的頻率更高。那麼,該端點對應的方法很可能是應用程序中熱路徑的開始。相應地,它調用的各種方法也可能位於熱路徑上。再舉一個例子,循環內的代碼,如果循環執行數百或數千次,則可能會對其他方法產生大量調用。

在優化應用程序性能時,通常首先應該關注熱路徑,由於被調用的頻率很高,因此對它們做出的改進能夠給性能帶來最顯著的影響。改進調用次數僅佔10%的代碼,產生的收益也要小得多。

.NET有兩個相關的Elasticsearch客戶端。NEST是支持強類型查詢的高級客戶端,位於底層客戶端Elasticsearch.NET之上。

NEST命名空間內有一個抽象的RequestBase類,該類派生出的子類都是強類型的請求類型。每個可以用的Elasticsearch HTTP API端點都有一個強類型的請求類。請求的主要特徵是它包含與其相關的API端點的一個或多個URL。

定義多個URL的原因是,許多ElasticSearch的API都可以使用基本路徑或包含特定資源標識符的路徑進行調用。例如,Elasticsearch中有一個端點可以查詢集群運行狀況。該端點可以通過URL「_cluster/health」執行整個集群的一般健康檢查;也可以在路徑中加入索引名稱「_cluster/health/」來針對特定索引執行健康檢查。

在邏輯上,這些URL由庫中的同一個請求類處理。在創建請求時,消費者可以提供一個可選的請求值,以指定特定索引。在這種情況下,必須在運行時構建URL,通過用戶提供的索引名稱替換URL中的部分。如果請求沒有提供索引名稱,則使用較短的URL 「_cluster/health」。

因此,在請求被發送的時候,最終的URL必須已經確定並且構建好了。首先從可能的URL列表中找出要使用的URL模式。這個過程需要使用強類型請求對象指定的請求值。在URL模式匹配完成後,就可以生成最終的URL了。必要時還可以使用帶有標記的URL模式,利用調用者代碼提供的路由值替換可選的標記,從而創建最終的URL字元串。

該URL構建的核心主要包含在UrlLookup類中,該類包括一個ToUrl方法,如下所示:

上述代碼首先創建了StringBuilder實例。然後,遍歷帶有標記的URL中的每個字元串。URL路徑中的標記元素存儲在字元串數組欄位「_tokenized」中。在每次迭代中,如果字元串值以「@」字元開頭,則表明需要用相應的值替換它。然後搜索路由的值,找出與當前標記名稱匹配的值,保存在「_parts」數組中。如果找到匹配項,則在對URI進行轉義後將其值附加到URL StringBuilder中(第15行)。

對於不需要替換路徑中的任何部分,則無需修改即可將它們直接附加到StringBuilder上(第21行)。

當所有帶有標記的值都被添加並替換之後,就可以調用StringBuilder的ToString方法,返回最終的字元串。每次客戶端發送請求時,這段代碼都會被調用,因此是庫中的熱路徑。

下面我們來考慮:如何對其進行優化,以提高執行速度,並減少資源分配?

現在這段代碼使用的是StringBuilder,這是良好的實踐,在需要將補丁數量的字元串連接到一起時,可以避免字元串分配。有幾種使用Span的方法可以減少字元串分配的次數。但是,添加Span或其他技巧(如利用ArrayPools提供零分配緩衝區),會增加代碼複雜度。由於這個庫被許多調用者使用,因此這種做法也許值得。

在日常的編程工作中,除非你的服務處於極端的使用/負載狀態,否則這種優化可能有點過。如果你熟悉Span之類的高性能技巧,那麼可能會情不自禁朝著最佳優化(即零分配)努力。這樣的想法會讓你對應該優先考慮的簡單改動視而不見。

當回顧ToUrl方法並通過邏輯流程進行思考時,我有了一個想法。對於某些情況,可以有另外兩種方法,實現簡單但能有效地提升性能。再看一下上面的代碼,你能否找到簡單的提升性能的改進?提示:只需在方法開頭加上幾行。

讓我們再次考慮集群健康的示例,它有兩個URL模式,「 _cluster/health」和「 _cluster/health/」。

後者要求路徑的最後一部分使用用戶提供的索引名稱替換。但是前者並沒有任何替換的要求。對於絕大多數端點來說,只有一小部分情況需要使用路由的值替換路徑中的一部分。明白我的意思了嗎?

我的想法是,某些情況下ToUrl方法完全不需要構建URL。這樣就根本不需要使用(更不需要內存分配)StringBuilder示例,也不需要生成新的URL字元串。既然URL不需要替換,那麼其中就只包含完整的原始URL路徑字元串。那麼,直接返回就可以了。

優化代碼

在進行任何優化之前,我需要先做兩件事。首先,我需要檢查現有代碼是否有足夠的單元測試。任何重構都有可能破壞當前的行為。如果沒有測試,我就會先根據目前的行為編寫一些測試。在優化之後,如果測試依然能夠通過,就說明沒有破壞任何東西。為了簡潔起見,本文將省略測試,相信許多開發人員都已經非常熟悉了。

優化之前需要做的第二件事就是,在已有代碼上建立評測基準,這樣之後就可以確定代碼改動是否能夠提升性能,並定量地測量性能的提升。對性能做出假設是危險的,最安全的做法就是用科學的方法來確保。首先建立理論,測量已有的行為,然後進行試驗(代碼優化),最終再次測量,以驗證假設。編寫性能測試腳本的方法也許你並不熟悉,你可以參考我關於.NET性能測試的文章(https://www.stevejgordon.co.uk/introduction-to-benchmarking-csharp-code-with-benchmark-dot-net)。

在此ToUrl示例中,基準測試非常直觀。

其中一些靜態欄位用於設置性能測試的類型,以及需要的輸入。我們不希望測量性能的測試產生額外的開銷。接下來是兩個性能測試,分別用於兩個URL模式。我們希望優化那個不需要替換路由值的模式,但也有必要對另一種情況進行測試。我們不希望在改進一個的同時對另一個產生負面影響。

更改任何代碼之前,首次運行的結果如下:

這為我們提供了一個基準,供我們完成工作後進行比較。

在ToUrl方法中,我們希望在不需要進行替換時,略過根據路徑構建URL的過程。只需要添加兩行代碼即可實現。

只需要在方法開頭添加這兩行(如果你喜歡在return語句周圍添加大括弧,那麼就添加4行)。這段代碼執行三個邏輯檢查。如果它們都返回true,我們就知道不需要任何替換,可以直接返回。第一個檢查可以確保用戶沒有提供路由值。如果用戶提供了路由值,就應該假設需要進行某種替換。接下來我們檢查標記的數字是否包含一個元素,以及該元素的首字母不是「@」字元。

標準的集群健康檢查請求不會提供索引名稱,那麼這些條件就會滿足,可以直接從標記數組的0號位置返回「_cluster/health」字元串。

這些額外的代碼並不複雜。大多數開發人員都可以順利閱讀並理解其目的。為了完整起見,我們還可以將所有條件重構成一個小的方法或局部函數,這樣就可以給它起一個名字,讓代碼不言自明。本文省略這些內容。

現在代碼修改完了,而且單元測試仍然能夠通過,下面我們重新運行基準測試來比較一下結果。

第二個性能測試「HealthIndex」沒有發生任何變化,因為部分URL需要替換,所以像以前一樣整個方法都會執行。但是,第一個性能測試「Health」中更直接的情況改進了許多。該代碼路徑上不再有任何分配,因此減少了100%!我們不再分配StringBuilder,也不創建新字元串,而是直接返回原始字元串,在這裡,原始字元串的內存已經分配過了。

節省160個位元組似乎並沒有太讓人興奮,但是考慮到客戶端每發送一個請求這段代碼都會調用一次,因此節省的量非常可觀。10個請求(不需要替換的請求)就可以節省1Kb無用的內存分配。如果客戶非常頻繁地使用Elasticsearch,這個改進就非常值得。

執行時間也減少了87%,因為在這種情況下唯一需要執行的代碼就是條件檢查和返回。這些改進在熱路徑上非常成功,對於所有調用該方法的人都有益。由於這是一個客戶端庫,所以客戶也會看到好處,只需要使用包含此優化的最新版客戶端即可。

總結

在本文中,我們介紹了並非所有性能優化都需要複雜的實現。在文中的示例中,我們通過條件檢查避免執行需要分配內存的代碼,從而優化了NEST庫的ToUrl方法。儘管可以使用Span從理論上進行一些更廣泛的優化,但我們優先考慮了可以快速獲得性能提升的方法,這不會帶來複雜性,也不會加重維護代碼的負擔。為了確保示例中的代碼改動確實可以提升性能,我們使用了基準來衡量代碼變更前後的效果。儘管例子中沒有介紹,但我們應該運行單元測試,以避免在這個方法中引入回歸問題。

希望通過這個示例,你可以在自己的代碼中找出只需簡單的修改就能快速提升性能的地方。在尋求值得優化的代碼時,請優先考慮熱路徑,並從簡單的地方開始,嘗試解決能快速提升性能的問題,然後再轉向更複雜的優化。對於大多數代碼庫來說,類似於本文的某些修改應該是合理的,而更高級的優化可能會加重維護的負擔。就像本文的示例一樣,某些優化工作可能非常簡單,只需使用條件檢查避免某些代碼的執行即可。

原文:https://www.stevejgordon.co.uk/dotnet-performance-optimisations-dont-have-to-be-complex

作者:STEVE GORDON,微軟MVP。

本文為 CSDN 翻譯,轉載請註明來源出處。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

怎樣用 Python 控制圖片人物動起來?一文就能 Get!