使用goroutines提高程序的性能

我們知道Golang語言的一個大殺器就是其goroutines機制,可以通過多核併發計算能大幅度提高程序的性能。但是Golang的協程如果使用不當反而會成為影響程序執行的瓶頸,本文中蟲蟲使用實例來說明Golang協程使用中存在的問題、及其原因,最後使用通道來解決問題並最終實現性能的提高。

簡單循環計算

首先,我們以一個簡單循環累加為例子,我們循環計算100億次累加:

使用goroutines提高程序的性能

我們用time開頭,用來簡單計算程序執行的耗時,結果如下:

使用goroutines提高程序的性能

注意由於累加100億次的結果實際上已經超出了證書最大值結果為負數,我們暫時不用管它,我們注意下程序執行時間10.299秒。

使用goroutines和slice併發計算

上面的例子中,簡單循環顯然是簡單直接,只能使用一個CPU進行計算,所以肯定比較慢。那麼我們改造一下,利用goroutines來併發使用多CPU進行計算:

使用goroutines提高程序的性能

代碼解釋:

代碼中,我們首先使用GOMAXPROCS 獲取可用的硬件線程總數。通常為物理CPU數量的兩倍。建立一個整數n個元素的全局slice來sums保存計算結果。我們使用sync的WaitGroup功能來管理goroutines。 它的Wait方法可以用來阻塞並檢測 goroutine的完成;Add方法用來增加或者減少goroutine的數量,Done方法其實上等於Add(-1)。然後用協程做併發計算。最後使用range函數把slice中的中間結果加起來就是總的結果。

我們執行下,結果如下:

使用goroutines提高程序的性能

對比簡單循環計算的結果,不論是user用戶態(多核)執行的時間,還是真正總時間都比簡單循環的多。多核執行花費時間是單核的4倍(user態),基本上沒有節省時間。

CPU和CPU緩存

要解釋這個現象,我們就需要從現代CPU架構及CPU緩存來說明。為了提高計算系能現代CPU架構中都使用了多級緩存來緩存內存到CPU通訊來提高IO,解決內核帶寬的瓶頸等問題,來極大的提高性能。

CPU緩存

一般來說,緩存是一個非常小但超快的內存塊。它位於CPU芯片上,因此每次讀取或寫入值時,CPU都不需要直接從內存獲取指令和數據。而是先將值讀取到緩存中,CPU從緩存中讀寫值,然後通定期同內存進行數據同步。每個CPU核心都有自己的緩存,不會與任何其他核心共享。對於n顆大小的CPU內核,最多可以有n+1個相同數據備份:一個在位於內存中,還有n個CPU的緩存中。當CPU內核更改其本地緩存中的值時,必須在某個時刻將其同步到內存。同樣,如果緩存中存的值在對應內存中改變了(另一個CPU內核緩存同步導致),則緩存的值就失效了,需要從主內存從新讀取刷新緩存。下面動圖顯示了這一過程:

使用goroutines提高程序的性能

緩存行

為了高效的同步緩存和主存儲器,數據會以64B大小的塊進行同步。這些塊稱為緩存行。

當緩存值更改時,會以最小緩存行(64B)大小的塊為單位同步到內存。同樣同步到其他CPU緩存時候也是以此為單位。

在我們的程序中,在併發循環中使用了全局slice來存儲中間的累加結果。slice的元素存儲在連續的內存空間中。有極大的概率,其中相鄰的切片元素間可能共同位於一個緩存行中。那麼問題就來了,n具有n個高速緩存的CPU核心重複讀取和寫入位於同一高速緩存行中的切片元素。此時,只要一個CPU核心計算出了sum結果,並更新了對應的slice元素,其他同緩存行的緩存就會失效,必須等待CPU核心將其同步回寫到內存,並更新其他CPU的緩存才能繼續計算。這個過程會非常耗時,甚至超過了單核循環更新單個和變量所需的時間。這就是我們的更新的併發計算更加耗時的原因:對一個切​​片的併發更新都會導致同一緩存行下的切片都要重複大量的緩存同步。下圖動態顯示了這一過程:

使用goroutines提高程序的性能

使用通道和goroutines併發計算

上面我們使用CPU架構和緩存原理說說明了,併發計算沒有代理性能改善的原因。那麼怎麼解決就簡單了:我們將切片轉換為n個單獨的變量,這樣讓他們彼此孤立分散在內存各處,不讓他們共享一個緩存行即可。

下面我們用一個變量存儲每個goroutine的中間計算結果,並通過一個通道將結果將結果傳遞到主程序中。也使用通道來控制goroutine併發:

使用goroutines提高程序的性能

代碼解釋:

chansum函數生使用n(硬件線程總數)個goroutines,使用一箇中間變量保存這n個數的和,通過一個整型通道res傳回結果。

當沒有要讀取的元素時,通道會自動阻塞。所以,我們無需做同步管理。

我們執行下改善程序,結果如下:

使用goroutines提高程序的性能

用時只需2秒,性能提高了五倍,OK,我們多核併發計算的效果真正體現出來了。

使用testing包進行基準測試

上面例子中我們都是使用linux的time工具測試程序執行時間來進行性能對比。實際上Golang語言本身就自帶了一個testing測試包,可以用來幫我們做測試,當然基準測試也是他的功能之一,那麼本文最後一部分我們就介紹一下使用testing包進行基準測試,比較三種方法的性能。首先我們要添加一個測試用例文件名稱必須和主程序xxx相同,同一目錄名稱為xxx_test.go,並且程序中要在同一package包。基準測試時候用例的函數必須以BenchmarkXXX開頭,函數必須為大寫。本例子我們的測試文件如下:

使用goroutines提高程序的性能

上面就是對三種方法(函數)進行基準測試的測試用例函數。

我們通過go test -bench .運行測試,結果如下:

使用goroutines提高程序的性能

總結:

本文中,我們展示用golang的goroutine並行機制進行計算性能改善,並且揭示了一個使用goroutine中常見的問題,導致併發計算實際上並沒有提高性能。我們利用CPU架構和緩存的底層處理機制解釋了出現這種問題的原因,最後通過通道和本地變量的方法改善了程序使得計算性能得到大幅度的提高。最後我們還介紹通過Golang自帶的testing包進行基準測試的方法。


分享到:


相關文章: