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

golang?chan傳遞數(shù)據(jù)的性能開銷詳解

 更新時間:2024年01月17日 11:26:02   作者:apocelipes  
這篇文章主要為大家詳細(xì)介紹了Golang中chan在接收和發(fā)送數(shù)據(jù)時因為“復(fù)制”而產(chǎn)生的開銷,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下

這篇文章并不討論chan因為加鎖解鎖以及為了維持內(nèi)存模型定義的行為而付出的運行時開銷。

這篇文章要探討的是chan在接收和發(fā)送數(shù)據(jù)時因為“復(fù)制”而產(chǎn)生的開銷。

在做性能測試前先復(fù)習(xí)點基礎(chǔ)知識。

數(shù)據(jù)是如何在chan里流動的

首先我們來看看帶buffer的chan,這里要分成兩類來討論。那沒buffer的chan呢?后面會細(xì)說。

情況1:發(fā)送的數(shù)據(jù)有讀者在讀取

可能需要解釋一下這節(jié)的標(biāo)題,意思是:發(fā)送者正在發(fā)送數(shù)據(jù)同時另一個接收者在等待數(shù)據(jù),看圖可能更快一些↓

圖里的chan是空的,發(fā)送者協(xié)程正在發(fā)送數(shù)據(jù)到channel,同時有一個接收者協(xié)程正在等待從chan里接收數(shù)據(jù)。

如果你對chan的內(nèi)存模型比較了解的話,其實可以發(fā)現(xiàn)此時是buffered chan的一種特例,他的行為和無緩沖的chan是一樣的,事實上兩者的處理上也是類似的。

所以向無緩沖chan發(fā)送數(shù)據(jù)時的情況可以歸類到情況1里。

在這種情況下,雖然在圖里我們?nèi)匀划嬃薱han的緩沖區(qū),但實際上go有優(yōu)化:chan發(fā)現(xiàn)這種情況后會使用runtime api,直接將數(shù)據(jù)寫入接收者的內(nèi)存(通常是棧內(nèi)存),跳過chan自己的緩沖區(qū),只復(fù)制數(shù)據(jù)一次。

這種情況下就像數(shù)據(jù)直接從發(fā)送者流到了接收者那里一樣。

情況2:發(fā)送的數(shù)據(jù)沒有讀者在讀取

這個情況就簡單多了,基本上除了情況1之外的所有情形都屬于這種:

圖里描述的是最常見的情況,讀者和寫者在操作不同的內(nèi)存。寫者將數(shù)據(jù)復(fù)制進緩沖區(qū)然后返回,如果緩沖滿了就阻塞到有可用的空位為止;讀者從緩沖區(qū)中將數(shù)據(jù)復(fù)制到自己的內(nèi)存里然后把對應(yīng)位置的內(nèi)存標(biāo)記為可寫入,如果緩沖區(qū)是空的,就阻塞到有數(shù)據(jù)可讀為止。

可能有人會問,如果緩沖區(qū)滿了導(dǎo)致發(fā)送的一方被阻塞了呢?其實發(fā)送者從阻塞恢復(fù)后需要繼續(xù)發(fā)送數(shù)據(jù),這時是逃不出情況1和情況2的,所以是否會被阻塞在這里不會影響數(shù)據(jù)發(fā)送的方式,并不重要。

在情況2中,數(shù)據(jù)先要被復(fù)制進chan自己的緩沖區(qū),然后接收者在讀取的時候在從chan的緩沖區(qū)把數(shù)據(jù)復(fù)制到自己的內(nèi)存里??傮w來說數(shù)據(jù)要被復(fù)制兩次。

情況2中chan就像這個水池,數(shù)據(jù)先從發(fā)送者那流進水池里,過了一段時間后再從水池里流到接收者那里。

特例中的特例

這里要說的是空結(jié)構(gòu)體:struct{}。在chan直接傳遞這東西不會有額外的內(nèi)存開銷,因為空結(jié)構(gòu)體本身不占內(nèi)存。和處理空結(jié)構(gòu)體的map一樣,go對這個特例做了特殊處理。

當(dāng)然,雖然不會消耗額外的內(nèi)存,但內(nèi)存模型是不變的。為了方便起見你可以把這個特例想象成情況2,只是相比之下使用更少的內(nèi)存。

為什么要復(fù)制

在情況1里我們看到了,runtime實際上有能力直接操作各個goroutine的內(nèi)存,那么為什么不選擇將數(shù)據(jù)“移動”到目標(biāo)位置,而要選擇復(fù)制呢?

我們先來看看如果是“移動”會發(fā)生什么。參考其他語言的慣例,被移動的對象將不可再被訪問,它的數(shù)據(jù)也將處于一種不確定但可以被安全刪除的狀態(tài),簡單地說,一點變量里的數(shù)據(jù)被移動到其他地方,這個變量就不應(yīng)該再被訪問了。在一些語言里移動后變量將強制性不可訪問,另一些語言里雖然可以訪問但會產(chǎn)生“undefined behavior”使程序陷入危險的狀態(tài)。go就比較尷尬了,既沒有手段阻止變量在移動后繼續(xù)被訪問,也沒有類似“undefined behavior”的手段兜底這些意外情況,隨意panic不僅消耗性能更是穩(wěn)定性方面的大忌。

因此移動在go中不現(xiàn)實。

再來看看在goroutine之間共享數(shù)據(jù),對于可以操作goroutine內(nèi)存的runtime來說,這個比移動要費事的多,但也可以實現(xiàn)。共享可能在cpu資源上會有些損耗,但確實能節(jié)約很多內(nèi)存。

共享的可行性也比移動高一些,因為不會對現(xiàn)有語法和語言設(shè)計有較大的沖擊,甚至可以說完全是在這套語法框架下合情合理的操作。但只要一個問題:不安全。chan的使用場景大部分情況下都是在并發(fā)編程中,共享的數(shù)據(jù)會帶來嚴(yán)重的并發(fā)安全問題。最常見的就是共享的數(shù)據(jù)被意外修改。對于以便利且安全的并發(fā)操作為賣點的go語言來說,內(nèi)置的并發(fā)原語會無時不刻生產(chǎn)出并發(fā)安全問題,無疑是不可接受的。

最后只剩下一個方案了,使用復(fù)制來傳遞數(shù)據(jù)。復(fù)制能在語法框架下使用,與共享相比也不容易引發(fā)問題(只是相對而言,chan的淺拷貝問題有時候反而是并發(fā)問題的溫床)。這也是go遵循的CSP(Communicating Sequential Process)模型所提倡的。

復(fù)制導(dǎo)致的開銷

既然復(fù)制有正當(dāng)理由且不可避免,那我們只能選擇接受了。因此復(fù)制會帶來多大開銷變得至關(guān)重要。

內(nèi)存用量上的開銷很簡單就能計算出來,不管是情況1還是情況2,數(shù)據(jù)一個時刻最多只會有自己本體外加一個副本存在——情況1是發(fā)送者持有本體,接收者持有副本;情況2是發(fā)送者持有本體,chan的緩沖區(qū)或者接收者(從緩沖區(qū)復(fù)制過去后緩沖區(qū)置空)持有副本。當(dāng)然,發(fā)送者完全可以將本體銷毀這樣只有一份數(shù)據(jù)留存在內(nèi)存里。所以內(nèi)存的消耗在最壞情況下會增加一倍。

cpu的消耗以及對速度的影響就沒那么好估計了,這個只能靠性能測試了。

測試的設(shè)計很簡單,選擇大中小三組數(shù)據(jù)利用buffered chan來測試chan和協(xié)程直接復(fù)制數(shù)據(jù)的開銷。

小的標(biāo)準(zhǔn)是2個int64,大小16字節(jié),存進一個緩存行綽綽有余:

type SmallData struct {
    a, b int64
}

中型大小的數(shù)據(jù)更接**常的業(yè)務(wù)對象,大小是144字節(jié),包含十多個字段:

type Data struct {
	a, b, c, d     int64
	flag1, flag2   bool
	s1, s2, s3, s4 string
	e, f, g, h     uint64
	r1, r2         rune
}

最后是大對象,大對象包含十個中對象,大小1440字節(jié),我知道也許沒人會這么寫,也許實際項目里還有筆者更重量級的,我當(dāng)然只能選個看起來合理的值用于測試:

type BigData struct {
	d1, d2, d3, d4, d5, d6, d7, d8, d9, d10 Data
}

鑒于chan會阻塞協(xié)程的特殊性,我們只能發(fā)完數(shù)據(jù)后再把它從chan里取出來,不然就得反復(fù)創(chuàng)建和釋放chan,這樣代來的雜音太大,因此數(shù)據(jù)實際上要被復(fù)制上兩回,這里我們只關(guān)注內(nèi)存復(fù)制的開銷,其他因素控制好變量就不會有影響。完整的測試代碼長這樣:

import "testing"
 
type SmallData struct {
	a, b int64
}
 
func BenchmarkSendSmallData(b *testing.B) {
	c := make(chan SmallData, 1)
	sd := SmallData{
		a: -1,
		b: -2,
	}
	for i := 0; i < b.N; i++ {
		c <- sd
		<-c
	}
}
 
func BenchmarkSendSmallPointer(b *testing.B) {
	c := make(chan *SmallData, 1)
	sd := &SmallData{
		a: -1,
		b: -2,
	}
	for i := 0; i < b.N; i++ {
		c <- sd
		<-c
	}
}
 
type Data struct {
	a, b, c, d     int64
	flag1, flag2   bool
	s1, s2, s3, s4 string
	e, f, g, h     uint64
	r1, r2         rune
}
 
func BenchmarkSendData(b *testing.B) {
	c := make(chan Data, 1)
	d := Data{
		a:     -1,
		b:     -2,
		c:     -3,
		d:     -4,
		flag1: true,
		flag2: false,
		s1:    "甲甲甲",
		s2:    "乙乙乙",
		s3:    "丙丙丙",
		s4:    "丁丁丁",
		e:     4,
		f:     3,
		g:     2,
		h:     1,
		r1:    '測',
		r2:    '試',
	}
	for i := 0; i < b.N; i++ {
		c <- d
		<-c
	}
}
 
func BenchmarkSendPointer(b *testing.B) {
	c := make(chan *Data, 1)
	d := &Data{
		a:     -1,
		b:     -2,
		c:     -3,
		d:     -4,
		flag1: true,
		flag2: false,
		s1:    "甲甲甲",
		s2:    "乙乙乙",
		s3:    "丙丙丙",
		s4:    "丁丁丁",
		e:     4,
		f:     3,
		g:     2,
		h:     1,
		r1:    '測',
		r2:    '試',
	}
	for i := 0; i < b.N; i++ {
		c <- d
		<-c
	}
}
 
type BigData struct {
	d1, d2, d3, d4, d5, d6, d7, d8, d9, d10 Data
}
 
func BenchmarkSendBigData(b *testing.B) {
	c := make(chan BigData, 1)
	d := Data{
		a:     -1,
		b:     -2,
		c:     -3,
		d:     -4,
		flag1: true,
		flag2: false,
		s1:    "甲甲甲",
		s2:    "乙乙乙",
		s3:    "丙丙丙",
		s4:    "丁丁丁",
		e:     4,
		f:     3,
		g:     2,
		h:     1,
		r1:    '測',
		r2:    '試',
	}
	bd := BigData{
		d1:  d,
		d2:  d,
		d3:  d,
		d4:  d,
		d5:  d,
		d6:  d,
		d7:  d,
		d8:  d,
		d9:  d,
		d10: d,
	}
	for i := 0; i < b.N; i++ {
		c <- bd
		<-c
	}
}
 
func BenchmarkSendBigDataPointer(b *testing.B) {
	c := make(chan *BigData, 1)
	d := Data{
		a:     -1,
		b:     -2,
		c:     -3,
		d:     -4,
		flag1: true,
		flag2: false,
		s1:    "甲甲甲",
		s2:    "乙乙乙",
		s3:    "丙丙丙",
		s4:    "丁丁丁",
		e:     4,
		f:     3,
		g:     2,
		h:     1,
		r1:    '測',
		r2:    '試',
	}
	bd := &BigData{
		d1:  d,
		d2:  d,
		d3:  d,
		d4:  d,
		d5:  d,
		d6:  d,
		d7:  d,
		d8:  d,
		d9:  d,
		d10: d,
	}
	for i := 0; i < b.N; i++ {
		c <- bd
		<-c
	}
}

我們選擇傳遞指針作為對比,這是日常開發(fā)中另一種常見的作法。

Windows11上的測試結(jié)果:

Linux上的測試結(jié)果:

對于小型數(shù)據(jù),復(fù)制帶來的開銷并不是很突出。

對于中型和大型數(shù)據(jù)就沒那么樂觀了,性能分別下降了20%50%。

測試結(jié)果很清晰,但有一點容易產(chǎn)生疑問,為什么大型數(shù)據(jù)比中型大了10倍,但復(fù)制速度上只慢了2.5倍呢?

原因是golang會對大數(shù)據(jù)啟用SIMD指令增加單位時間內(nèi)的數(shù)據(jù)吞吐量,因此數(shù)據(jù)大了確實復(fù)制會更慢,但不是數(shù)據(jù)量大10倍速度就會慢10倍的。

由此可見復(fù)制數(shù)據(jù)帶來的開銷是很難置之不理的。

如何避免開銷

既然chan復(fù)制數(shù)據(jù)會產(chǎn)生不可忽視的性能開銷,我們得想些對策來解決問題才行。這里提供幾種思路。

只傳小對象

多小才算小,這個爭議很大。我只能說說我自己的經(jīng)驗談:1個緩存行里存得下的就是小。

一個緩存行有多大?現(xiàn)代的x64 cpu上L1D的大小通常是32字節(jié),也就是4個普通數(shù)據(jù)指針/int64的大小。

從我們的測試來看小數(shù)據(jù)的復(fù)制開銷幾乎可以忽略不記,因此只在chan里傳遞這類小數(shù)據(jù)不會有什么性能問題。

唯一要注意的是string,目前的實現(xiàn)一個字符串本身的大小是16字節(jié),但這個大小是沒算字符串本身數(shù)據(jù)的,也就是說一個長度256的字符串和一個長度1的字符串,自身的結(jié)構(gòu)都是16字節(jié)大,但復(fù)制的時候一個要拷貝256個字符一個只用拷貝一個字符。因此字符串經(jīng)常出現(xiàn)看著小但實際大小很大的實例。

只傳指針

32字節(jié)實在是有點小,如果我需要傳遞2-3個緩存行大小的數(shù)據(jù)怎么辦?這個也是實際開發(fā)中的常見需求。

答案實際上在性能測試的對照組里給出了:傳指針給chan。

從性能測試的結(jié)果來看,只傳指針的情況下,無論數(shù)據(jù)多大,耗時都是一樣的,因為我們只復(fù)制了一份指針——8字節(jié)的數(shù)據(jù)。

這個作法也能節(jié)約內(nèi)存:只復(fù)制了指針,指針引用的數(shù)據(jù)沒有被復(fù)制。

看起來我們找到了向chan傳遞數(shù)據(jù)的銀彈——只傳指針,然而世界上并沒有銀彈——

  • 傳指針相當(dāng)于上一節(jié)說的“共享”數(shù)據(jù),很容易帶來并發(fā)安全問題;
  • 對于發(fā)送者,傳指針給chan很可能會影響逃逸分析,不僅會在堆上分配對象,還會使情況1中的優(yōu)化失去意義(調(diào)用runtime就為了寫入一個指針到接收者的棧上)
  • 對于接收者來說,操作指針引用的數(shù)據(jù)需要一次或多次的解引用,而這種解引用很難被優(yōu)化掉,因此在一些熱點代碼上很可能會帶來可見的性能影響(通常不會有復(fù)制數(shù)據(jù)帶來的開銷大,但一切得以性能測試為準(zhǔn))。
  • 太多的指針會加重gc的負(fù)擔(dān)

使用指針傳遞時切記要充分考慮上面列出的缺點。

使用lock-free數(shù)據(jù)結(jié)果替代chan

chan大部分時間都被用作并發(fā)安全的隊列,如果chan只有固定的一個發(fā)送者和固定一個的接收者,那么可以試試這種無鎖數(shù)據(jù)結(jié)構(gòu):SPSCQueue

無鎖數(shù)據(jù)結(jié)構(gòu)相比chan好處在于沒有mutex,且沒有數(shù)據(jù)復(fù)制的開銷。

缺點是只支持單一接收者和單一發(fā)送者,實現(xiàn)也相對復(fù)雜所以需要很高的代碼質(zhì)量來保證使用上的安全和運行結(jié)果的正確,找不到一個高質(zhì)量庫的時候我建議是最好別嘗試自己寫,也最好別用。(一個壞消息,go里可靠的無鎖數(shù)據(jù)結(jié)構(gòu)庫不是很多)

開銷可以接受的情況

有一類系統(tǒng)追求正確性和安全性,對性能損耗和資源消耗有較高的容忍度。對于這類系統(tǒng)來說,復(fù)制數(shù)據(jù)帶來的開銷一般是可接受的。

這時候明顯復(fù)制傳遞比傳指針等操作簡單而安全。

另一種常見情形是:chan并不是性能瓶頸,復(fù)不復(fù)制對性能的影響微乎其微。這時候我也傾向于選擇復(fù)制傳遞數(shù)據(jù)。

總結(jié)

總體來說chan還是很方便的,在go里又是還不得不用。

我寫這篇文章不是為了嚇唬大家,只是提醒大家一些使用chan時可能發(fā)生的性能陷阱和對應(yīng)的解決辦法。

至于你怎么用chan,除了要結(jié)合實際需求之外,性能測試是另一個重要的參考標(biāo)準(zhǔn)。

如果問我,那么我傾向于復(fù)制數(shù)據(jù)優(yōu)先于指針傳遞,除非數(shù)據(jù)十分巨大/性能瓶頸在復(fù)制上/接收方發(fā)送方需要在同一個對象上做些協(xié)同作業(yè)。同樣性能測試和profile是我采用這些方式的參考標(biāo)準(zhǔn)。

到此這篇關(guān)于golang chan傳遞數(shù)據(jù)的性能開銷詳解的文章就介紹到這了,更多相關(guān)go chan傳遞數(shù)據(jù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Golang中處理import自定義包出錯問題的解決辦法

    Golang中處理import自定義包出錯問題的解決辦法

    最近開始使用Go/GoLand在import自定義包時出現(xiàn)各種狀況,下面這篇文章主要給大家介紹了關(guān)于Golang中處理import自定義包出錯問題的解決辦法,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下
    2023-11-11
  • golang copy函數(shù)使用的坑

    golang copy函數(shù)使用的坑

    本文主要介紹了golang copy函數(shù)使用的坑,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • 淺析Go語言中的超時控制

    淺析Go語言中的超時控制

    日常開發(fā)中我們大概率會遇到超時控制的場景,而一個良好的超時控制可以有效的避免一些問題,所以本文就來和大家深入探討一下Go語言中的超時控制吧
    2023-10-10
  • golang中beego入門

    golang中beego入門

    Beego是一個基于Go語言的開源框架,用于構(gòu)建Web應(yīng)用程序和API,本文主要介紹了golang中beego入門,具有一定的參考價值,感興趣的可以了解一下
    2023-12-12
  • 使用Go語言連接和操作數(shù)據(jù)庫的基本步驟

    使用Go語言連接和操作數(shù)據(jù)庫的基本步驟

    在Go語言中,連接和操作數(shù)據(jù)庫通常使用database/sql包,它提供了一個數(shù)據(jù)庫抽象層,支持多種數(shù)據(jù)庫引擎,如MySQL、PostgreSQL、SQLite等,下面我將以MySQL為例,詳細(xì)講解如何使用Go語言連接和操作數(shù)據(jù)庫,需要的朋友可以參考下
    2024-06-06
  • 深入探討Go語言中的map是否是并發(fā)安全以及解決方法

    深入探討Go語言中的map是否是并發(fā)安全以及解決方法

    這篇文章主要來和大家探討?Go?語言中的?map?是否是并發(fā)安全的,并提供三種方案來解決并發(fā)問題,文中的示例代碼講解詳細(xì),需要的可以參考一下
    2023-05-05
  • GoLand編譯帶有構(gòu)建標(biāo)簽的程序思路詳解

    GoLand編譯帶有構(gòu)建標(biāo)簽的程序思路詳解

    這篇文章主要介紹了GoLand編譯帶有構(gòu)建標(biāo)簽的程序,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-11-11
  • Golang利用channel協(xié)調(diào)協(xié)程的方法詳解

    Golang利用channel協(xié)調(diào)協(xié)程的方法詳解

    go?當(dāng)中的并發(fā)編程是通過goroutine來實現(xiàn)的,利用channel(管道)可以在協(xié)程之間傳遞數(shù)據(jù),所以本文就來講講Golang如何利用channel協(xié)調(diào)協(xié)程吧
    2023-05-05
  • Go安裝和環(huán)境配置圖文教程

    Go安裝和環(huán)境配置圖文教程

    本文主要介紹了Go安裝和環(huán)境配置圖文教程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • Go語言學(xué)習(xí)筆記之錯誤和異常詳解

    Go語言學(xué)習(xí)筆記之錯誤和異常詳解

    Go語言采用返回值的形式來返回錯誤,這一機制既可以讓開發(fā)者真正理解錯誤處理的含義,也可以大大降低程序的復(fù)雜度,下面這篇文章主要給大家介紹了關(guān)于Go語言學(xué)習(xí)筆記之錯誤和異常的相關(guān)資料,需要的朋友可以參考下
    2022-07-07

最新評論