欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

golang遍歷處理map時的常見性能陷阱與解決方法

 更新時間:2025年05月30日 09:05:16   作者:apocelipes  
這篇文章主要為大家詳細介紹了Golang中有關循環(huán)處理map時的性能優(yōu)化,本文主要介紹了常見的三種場景,文中的示例代碼講解詳細,需要的可以了解下

最近一直在重構優(yōu)化老系統(tǒng),所以性能優(yōu)化相關的文章會比較多。

這次的是有關循環(huán)處理map時的性能優(yōu)化。預分配內存之類的大家都知道的就不多說了,今天來講點大伙不知道的。

要講的一共有三點,而且都和循環(huán)處理map有關。

不要用for-range循環(huán)清空map

這里要討論的“清空”是指刪除map中所有鍵值對,但保留map里已分配的內存供下次復用。

如果只是想釋放map并且不再需要復用,那么map1 = nil或者map2 = map[T]U{}就足夠了。

內置函數(shù)delete可以幫我們實現(xiàn)刪除鍵值對但保留它們在map中的內存空間,通常我們會這么寫:

for key := range Map1 {
    delete(Map1, key)
}

這種模式化的代碼太常見,以至于go編譯器專門對其做了優(yōu)化,只要形式上符合上述代碼片段的,編譯器都會把循環(huán)優(yōu)化成runtime.mapclear(Map1),使用runtime內置的map清理函數(shù)將map清空,這比靠循環(huán)遍歷刪除要快很多倍。

看到這里你可能會說這不是挺好嗎,為什么不讓用了呢?

因為現(xiàn)在有更好的替代方案了——內置函數(shù)clear。

clear應用在map上時本身就會調用runtime.mapclear(...),在性能上和循環(huán)方法大致一樣而且只快不慢。因為兩者最終生成代碼差不多,性能測試也就沒多大意義了,所以這里不做性能測試。

clear還有另一個好處,它更容易讓包含它的函數(shù)被內聯(lián)。

這是什么意思呢?go的編譯器實際上在編譯時要分很多個步驟,粗略地講go代碼在真正開始生成機器碼之前,得先經(jīng)過內聯(lián) -> 逃逸分析 -> 語法樹改寫這樣幾個階段。上文說的for循環(huán)刪除優(yōu)化在語法樹改寫這個階段完成。

這就帶來了一個問題,相比一個簡單的clear函數(shù)調用,編譯器認為for循環(huán)這種操作更“重量級”,一個函數(shù)擁有的“重”操作越多,那么這個函數(shù)被內聯(lián)優(yōu)化的可能性就越小。

我們看個例子:

func RangeClearMap() {
	for k := range bigMap {
		delete(bigMap, k)
	}
	for k := range bigPtrMap {
		delete(bigPtrMap, k)
	}
	for k := range smallMap {
		delete(smallMap, k)
	}
	for k := range smallPtrMap {
		delete(smallPtrMap, k)
	}
	for k := range bigMapIntKey {
		delete(bigMapIntKey, k)
	}
	for k := range bigPtrMapIntKey {
		delete(bigPtrMapIntKey, k)
	}
	for k := range smallMapIntKey {
		delete(smallMapIntKey, k)
	}
	for k := range smallPtrMapIntKey {
		delete(smallPtrMapIntKey, k)
	}
}

func BuiltinClearMap() {
	clear(bigMap)
	clear(bigPtrMap)
	clear(smallMap)
	clear(smallPtrMap)
	clear(bigMapIntKey)
	clear(bigPtrMapIntKey)
	clear(smallMapIntKey)
	clear(smallPtrMapIntKey)
}

同樣是清空8個map,一個用循環(huán),一個用內置函數(shù)。我們看下內聯(lián)分析的結果:

其中cost就是衡量一個函數(shù)里的操作有多“重”的數(shù)值標準,超過一定的cost,函數(shù)就無法內聯(lián)。可以看到,使用循環(huán)會比使用clear內置函數(shù)重整整四倍。雖然最后因為兩個函數(shù)都很簡單所以被內聯(lián)展開,但碰上更復制一點的函數(shù),顯然使用clear能有更多的冗余。

盡管編譯器最終會把兩者優(yōu)化成一樣的對runtime的map清理函數(shù)的調用,但對for循環(huán)的優(yōu)化在內聯(lián)處理之后,因此for不僅讓代碼更長,也更容易錯失內聯(lián)優(yōu)化的機會,而失去內聯(lián)優(yōu)化進而會影響逃逸分析從而損失更多性能,你可以在我以前的文章里看到內聯(lián)和逃逸分析對內聯(lián)的影響。

clear()是go1.21添加的,因此只要你在用的go版本大于等于1.21,我推薦你盡量使用clear而不是for-range循環(huán)來清空map。

遍歷訪問map時的陷阱

遍歷處理map中的元素也是常見操作,不過不像循環(huán)刪除,編譯器在這種代碼上并沒有什么優(yōu)化。

最常見的寫法是這樣的:

for key, value := range Map1 {
    func1(key, value)
    func2(key, value)
}

這時候,一部分開發(fā)者會覺得每次循環(huán)都得復制一次value,尤其是1.22開始循環(huán)變量每輪都是新變量,這種操作是不是會很慢也很占用內存?畢竟在遍歷slice的時候確實有這些問題,那么能不能采用優(yōu)化slice遍歷的相同方法來優(yōu)化map遍歷呢:

for key := range Map1 {
    func1(key, Map1[key])
    func2(key, Map1[key])
}

現(xiàn)在不用復制value了,性能應該獲得提升了吧?真的是這樣嗎?

我說過很多次,性能優(yōu)化不能靠想象,要靠benchmark來說話,所以我們來做個性能測試。

我們測試大value和小value在兩種循環(huán)下的表現(xiàn),另外還會額外測試一下map里存放指針的情況,測試用的value類型主要是下面兩種:

// 128字節(jié)
type BigObject struct {
	n1, n2, n3, n4, n5, n6, n7, n8, n9, n10 int64
	s1, s2, s3                              string
}

func NewBigObjectPtr() *BigObject {
	return &BigObject{
		n1:  randNum(),
		n2:  randNum(),
		n3:  randNum(),
		n4:  randNum(),
		n5:  randNum(),
		n6:  randNum(),
		n7:  randNum(),
		n8:  randNum(),
		n9:  randNum(),
		n10: randNum(),
		s1:  genRandStr(),
		s2:  genRandStr(),
		s3:  genRandStr(),
	}
}

// 32字節(jié)
type SmallObject struct {
	n1, n2 int64
	s1     string
}

func NewSmallObject() SmallObject {
	return SmallObject{
		n1: randNum(),
		n2: randNum(),
		s1: genRandStr(),
	}
}

const table = "abcdefgh01234567"

// 生成固定16字符長度的隨機字符串
func genRandStr() string {
	buf := make([]byte, 16)
	num := rand.Uint64()
	for i := range buf {
		buf[i] = table[num&0xf]
		num >>= 4
	}
	return string(buf)
}

func randNum() int64 {
	for {
		num := rand.Int64()
		if num != 0 {
			return num
		}
	}
}

我們一共分8種case進行測試:

var (
	bigMap            = map[string]BigObject{}
	bigPtrMap         = map[string]*BigObject{}
	smallMap          = map[string]SmallObject{}
	smallPtrMap       = map[string]*SmallObject{}
)

func init() {
	for i := range int64(100) {
		strKey := fmt.Sprintf("Key:%03d", i)
		bigMap[strKey] = NewBigObject()
		bigPtrMap[strKey] = NewBigObjectPtr()
		smallMap[strKey] = NewSmallObject()
		smallPtrMap[strKey] = NewSmallObjectPtr()
	}
}

每個map里都填充100個元素,元素的值隨機生成,不過我限制了字符串都是等長的,這是為了結果的準確性。

測試也比較簡單:

func BenchmarkBigObjectLoopCopy(b *testing.B) {
	for b.Loop() {
		for _, v := range bigMap {
			if v.n1 == 0 || v.n2 == 0 {
				panic("error")
			}
		}
	}
}

func BenchmarkBigObjectPtrLoopCopy(b *testing.B) {
	for b.Loop() {
		for _, v := range bigPtrMap {
			if v.n1 == 0 || v.n2 == 0 {
				panic("error")
			}
		}
	}
}

func BenchmarkBigObjectLoopKey(b *testing.B) {
	for b.Loop() {
		for k := range bigMap {
			if bigMap[k].n1 == 0 || bigMap[k].n2 == 0 {
				panic("error")
			}
		}
	}
}

func BenchmarkBigObjectPtrLoopKey(b *testing.B) {
	for b.Loop() {
		for k := range bigPtrMap {
			if bigPtrMap[k].n1 == 0 || bigPtrMap[k].n2 == 0 {
				panic("error")
			}
		}
	}
}

func BenchmarkSmallObjectLoopCopy(b *testing.B) {
	for b.Loop() {
		for _, v := range smallMap {
			if v.n1 == 0 || v.n2 == 0 {
				panic("error")
			}
		}
	}
}

func BenchmarkSmallObjectPtrLoopCopy(b *testing.B) {
	for b.Loop() {
		for _, v := range smallPtrMap {
			if v.n1 == 0 || v.n2 == 0 {
				panic("error")
			}
		}
	}
}

func BenchmarkSmallObjectLoopKey(b *testing.B) {
	for b.Loop() {
		for k := range smallMap {
			if smallMap[k].n1 == 0 || smallMap[k].n2 == 0 {
				panic("error")
			}
		}
	}
}

func BenchmarkSmallObjectPtrLoopKey(b *testing.B) {
	for b.Loop() {
		for k := range smallPtrMap {
			if smallPtrMap[k].n1 == 0 || smallPtrMap[k].n2 == 0 {
				panic("error")
			}
		}
	}
}

8種case就是大value的map和小value的分別用k,v := range map遍歷和只用key遍歷,看看性能差異。

結果很是讓人詫異,自認為的不需要復制value所以訪問更快的結論是錯的,而且錯的離譜:復制value的做法性能是只用key遍歷的3倍!

為啥會這樣呢?因為你掉進hashmap的陷阱里了,我就直接說原因了:

  • for k,v := range m遍歷map時,鍵值對是被順序訪問的,這對緩存命中和cpu的模式預測更友好,性能會比用key進行hash的隨機訪問要好;
  • 使用m[k]訪問時需要計算key的hash,這一步是比較耗費計算資源的,哪怕新版本換了swissmap之后也一樣,而for-range循環(huán)不需要計算hash值。

這兩點就是hashmap的陷阱,本來在少量多次或者隨機查找等模式下算不上太大的問題,但在循環(huán)遍歷的情形下影響就會放大,最后導致出現(xiàn)3倍以上的性能差異。

所以當你要遍歷處理每一個value的時候,最好用for k,v := range m或者for _,v := range m。

不過也有例外:如果你的value比較大,你需要遍歷key,符合過濾條件的key的value才需要處理,這種時候那些無用的復制就會成為性能絆腳石了。不過還是老話,先做benchmark再談優(yōu)化。

復制map時的性能陷阱

最后一個性能陷阱埋在復制map時。

這里說的“復制”是淺復制,把鍵值對復制到一個新map里去,里面的指針或者slice都是淺拷貝的。

還是先上常見寫法,實際上在1.21之前你也只能這么寫:

m2 := make(map[T]U, len(m1))
for k, v := range m1 {
    m2[k] = v
}

編譯器同樣也不會對這種代碼有特殊優(yōu)化,循環(huán)會老老實實地執(zhí)行。

到了1.21,我們可以用maps.Clone做一樣的事情,而且這個標準庫函數(shù)也會在底層調用runtime里的專用map復制函數(shù),性能杠杠的。

我們準備一下性能測試,map樣本沿用上一節(jié)里的:

func BenchmarkMapClone(b *testing.B) {
	b.Run("range-smallMap", func(b *testing.B) {
		for b.Loop() {
			m := make(map[string]SmallObject, len(smallMap))
			for k, v := range smallMap {
				m[k] = v
			}
		}
	})
	b.Run("range-smallPtrMap", func(b *testing.B) {
		for b.Loop() {
			m := make(map[string]*SmallObject, len(smallPtrMap))
			for k, v := range smallPtrMap {
				m[k] = v
			}
		}
	})
	b.Run("range-bigMap", func(b *testing.B) {
		for b.Loop() {
			m := make(map[string]BigObject, len(bigMap))
			for k, v := range bigMap {
				m[k] = v
			}
		}
	})
	b.Run("range-bigPtrMap", func(b *testing.B) {
		for b.Loop() {
			m := make(map[string]*BigObject, len(bigPtrMap))
			for k, v := range bigPtrMap {
				m[k] = v
			}
		}
	})
	b.Run("clone-smallMap", func(b *testing.B) {
		for b.Loop() {
			maps.Clone(smallMap)
		}
	})
	b.Run("clone-smallPtrMap", func(b *testing.B) {
		for b.Loop() {
			maps.Clone(smallPtrMap)
		}
	})
	b.Run("clone-bigMap", func(b *testing.B) {
		for b.Loop() {
			maps.Clone(bigMap)
		}
	})
	b.Run("clone-bigPtrMap", func(b *testing.B) {
		for b.Loop() {
			maps.Clone(bigPtrMap)
		}
	})
}

在這里如果你用的go版本是1.24,那么你會踩到第一個陷阱,對沒錯,我說了有三種陷阱,沒說只有三個哦。

1.24的maps.clone實現(xiàn)有問題,會有嚴重的性能回退,所以你可以看到它和循環(huán)復制性能沒有差距,甚至有時候還更慢一點:

具體是什么樣的問題我就不深入講解了,因為是偷懶導致的很無聊的問題。好在這個問題會在1.25修復,修復代碼已經(jīng)在主分支上了,因此我們可以用go version go1.25-devel_3fd729b2a1 Sat May 24 08:48:53 2025 -0700 windows/amd64來測試:

修復后結果就和1.22以及1.23一樣了。總得來說maps.Clone雖然多浪費了一點內存,但速度是循環(huán)復制的1.5~3倍。

所以要復制map的時候,盡量去用maps.Clone,這樣就能避開循環(huán)復制慢這第二個陷阱。

總結

golang果然還是那個golang,大道至簡的外皮下往往暗藏殺機。

真要說什么原則的話,那就是如果有對應的標準庫函數(shù)/內置函數(shù),那就用,盡量少在map上直接用循環(huán)。

還有我說過無數(shù)次的,性能優(yōu)化要靠benchmark,切記不要依賴經(jīng)驗去預判,陷阱二就是我用benchmark找出來的“預判”失誤。

到此這篇關于golang遍歷處理map時的常見性能陷阱與解決方法的文章就介紹到這了,更多相關go遍歷map避坑內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Golang 實現(xiàn)獲取當前函數(shù)名稱和文件行號等操作

    Golang 實現(xiàn)獲取當前函數(shù)名稱和文件行號等操作

    這篇文章主要介紹了Golang 實現(xiàn)獲取當前函數(shù)名稱和文件行號等操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Go語言實現(xiàn)互斥鎖、隨機數(shù)、time、List

    Go語言實現(xiàn)互斥鎖、隨機數(shù)、time、List

    這篇文章主要介紹了Go語言實現(xiàn)互斥鎖、隨機數(shù)、time、List的相關資料,需要的朋友可以參考下
    2018-10-10
  • Go語言規(guī)范context?類型的key用法示例解析

    Go語言規(guī)范context?類型的key用法示例解析

    這篇文章主要為大家介紹了Go語言規(guī)范context?類型的key用法示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-08-08
  • Go標準庫常見錯誤分析和解決辦法

    Go標準庫常見錯誤分析和解決辦法

    Go語言的標準庫為開發(fā)者提供了豐富且高效的工具,涵蓋了從網(wǎng)絡編程到文件操作等各個方面,然而,標準庫雖好,使用不當卻可能適得其反,正所謂"工欲善其事,必先利其器",本文將深入剖析Go標準庫使用中的常見錯誤,幫助開發(fā)者避開這些坑,寫出更加健壯的代碼
    2025-04-04
  • Golang Gorm 更新字段save、update、updates

    Golang Gorm 更新字段save、update、updates

    在gorm中,批量更新操作可以通過使用Update方法來實現(xiàn),本文主要介紹了Golang Gorm 更新字段save、update、updates,具有一定的參考價值,感興趣的可以了解一下
    2023-12-12
  • golang中值類型/指針類型的變量區(qū)別總結

    golang中值類型/指針類型的變量區(qū)別總結

    golang的值類型和指針類型receiver一直是大家比較混淆的地方,下面這篇文章主要給大家總結介紹了關于golang中值類型/指針類型的變量區(qū)別的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下。
    2017-12-12
  • 詳解prometheus監(jiān)控golang服務實踐記錄

    詳解prometheus監(jiān)控golang服務實踐記錄

    這篇文章主要介紹了詳解prometheus監(jiān)控golang服務實踐記錄,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-11-11
  • Golang測試框架goconvey進行單元測試流程介紹

    Golang測試框架goconvey進行單元測試流程介紹

    goconvey是一款針對Golang的測試框架,可以管理和運行測試用例,同時提供了豐富的斷言函數(shù),并支持很多Web界面特性,這篇文章主要介紹了使用goconvey進行單元測試流程,感興趣的同學可以參考下文
    2023-05-05
  • golang中的nil接收器詳解

    golang中的nil接收器詳解

    這篇文章主要介紹了golang中的nil接收器,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-10-10
  • 在go語言中安裝與使用protobuf的方法詳解

    在go語言中安裝與使用protobuf的方法詳解

    protobuf以前只支持C++, Python和Java等語言, Go語言出來后, 作為親兒子, 那有不支持的道理呢? 這篇文章主要給大家介紹了關于在go語言中使用protobuf的相關資料,文中介紹的非常詳細,需要的朋友可以參考借鑒,下面來一起看看吧。
    2017-08-08

最新評論