新聞中心
代碼的穩(wěn)健、可讀和高效是我們每一個(gè) coder 的共同追求。本文將結(jié)合 Go 語言特性,為書寫效率更高的代碼,從常用數(shù)據(jù)結(jié)構(gòu)、內(nèi)存管理和并發(fā),三個(gè)方面給出相關(guān)建議。話不多說,讓我們一起學(xué)習(xí) Go 高性能編程的技法吧。

創(chuàng)新互聯(lián)服務(wù)項(xiàng)目包括浠水網(wǎng)站建設(shè)、浠水網(wǎng)站制作、浠水網(wǎng)頁制作以及浠水網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,浠水網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到浠水省份的部分城市,未來相信會繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
常用數(shù)據(jù)結(jié)構(gòu)
1.反射雖好,切莫貪杯
標(biāo)準(zhǔn)庫 reflect 為 Go 語言提供了運(yùn)行時(shí)動態(tài)獲取對象的類型和值以及動態(tài)創(chuàng)建對象的能力。反射可以幫助抽象和簡化代碼,提高開發(fā)效率。
Go 語言標(biāo)準(zhǔn)庫以及很多開源軟件中都使用了 Go 語言的反射能力,例如用于序列化和反序列化的 json、ORM 框架 gorm、xorm 等。
1.1 優(yōu)先使用 strconv 而不是 fmt
基本數(shù)據(jù)類型與字符串之間的轉(zhuǎn)換,優(yōu)先使用 strconv 而不是 fmt,因?yàn)榍罢咝阅芨眩?/p>
// Bad
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
// Good
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}
BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
為什么性能上會有兩倍多的差距,因?yàn)?fmt 實(shí)現(xiàn)上利用反射來達(dá)到范型的效果,在運(yùn)行時(shí)進(jìn)行類型的動態(tài)判斷,所以帶來了一定的性能損耗。
1.2 少量的重復(fù)不比反射差
有時(shí),我們需要一些工具函數(shù)。比如從 uint64 切片過濾掉指定的元素。
利用反射,我們可以實(shí)現(xiàn)一個(gè)類型泛化支持?jǐn)U展的切片過濾函數(shù):
// DeleteSliceElms 從切片中過濾指定元素。注意:不修改原切片。
func DeleteSliceElms(i interface{}, elms ...interface{}) interface{} {
// 構(gòu)建 map set。
m := make(map[interface{}]struct{}, len(elms))
for _, v := range elms {
m[v] = struct{}{}
}
// 創(chuàng)建新切片,過濾掉指定元素。
v := reflect.ValueOf(i)
t := reflect.MakeSlice(reflect.TypeOf(i), 0, v.Len())
for i := 0; i < v.Len(); i++ {
if _, ok := m[v.Index(i).Interface()]; !ok {
t = reflect.Append(t, v.Index(i))
}
}
return t.Interface()
}
很多時(shí)候,我們可能只需要操作一個(gè)類型的切片,利用反射實(shí)現(xiàn)的類型泛化擴(kuò)展的能力壓根沒用上。退一步說,如果我們真地需要對 uint64 以外類型的切片進(jìn)行過濾,拷貝一次代碼又何妨呢?可以肯定的是,絕大部份場景,根本不會對所有類型的切片進(jìn)行過濾,那么反射帶來好處我們并沒有充分享受,但卻要為其帶來的性能成本買單。
// DeleteU64liceElms 從 []uint64 過濾指定元素。注意:不修改原切片。
func DeleteU64liceElms(i []uint64, elms ...uint64) []uint64 {
// 構(gòu)建 map set。
m := make(map[uint64]struct{}, len(elms))
for _, v := range elms {
m[v] = struct{}{}
}
// 創(chuàng)建新切片,過濾掉指定元素。
t := make([]uint64, 0, len(i))
for _, v := range i {
if _, ok := m[v]; !ok {
t = append(t, v)
}
}
return t
}
下面看一下二者的性能對比。
func BenchmarkDeleteSliceElms(b *testing.B) {
slice := []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9}
elms := []interface{}{uint64(1), uint64(3), uint64(5), uint64(7), uint64(9)}
for i := 0; i < b.N; i++ {
_ = DeleteSliceElms(slice, elms...)
}
}
func BenchmarkDeleteU64liceElms(b *testing.B) {
slice := []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9}
elms := []uint64{1, 3, 5, 7, 9}
for i := 0; i < b.N; i++ {
_ = DeleteU64liceElms(slice, elms...)
}
}
運(yùn)行上面的基準(zhǔn)測試。
go test -bench=. -benchmem main/reflect
goos: darwin
goarch: amd64
pkg: main/reflect
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkDeleteSliceElms-12 1226868 978.2 ns/op 296 B/op 16 allocs/op
BenchmarkDeleteU64liceElms-12 8249469 145.3 ns/op 80 B/op 1 allocs/op
PASS
ok main/reflect 3.809s
可以看到,反射涉及了額外的類型判斷和大量的內(nèi)存分配,導(dǎo)致其對性能的影響非常明顯。隨著切片元素的遞增,每一次判斷元素是否在 map 中,因?yàn)?map 的 key 是不確定的類型,會發(fā)生變量逃逸,觸發(fā)堆內(nèi)存的分配。所以,可預(yù)見的是當(dāng)元素?cái)?shù)量增加時(shí),性能差異會越來大。
當(dāng)使用反射時(shí),請問一下自己,我真地需要它嗎?
1.3 慎用 binary.Read 和 binary.Write
binary.Read 和 binary.Write 使用反射并且很慢。如果有需要用到這兩個(gè)函數(shù)的地方,我們應(yīng)該手動實(shí)現(xiàn)這兩個(gè)函數(shù)的相關(guān)功能,而不是直接去使用它們。
encoding/binary 包實(shí)現(xiàn)了數(shù)字和字節(jié)序列之間的簡單轉(zhuǎn)換以及 varints 的編碼和解碼。varints 是一種使用可變字節(jié)表示整數(shù)的方法。其中數(shù)值本身越小,其所占用的字節(jié)數(shù)越少。Protocol Buffers 對整數(shù)采用的便是這種編碼方式。
其中數(shù)字與字節(jié)序列的轉(zhuǎn)換可以用如下三個(gè)函數(shù):
// Read 從結(jié)構(gòu)化二進(jìn)制數(shù)據(jù) r 讀取到 data。data 必須是指向固定大小值的指針或固定大小值的切片。
func Read(r io.Reader, order ByteOrder, data interface{}) error
// Write 將 data 的二進(jìn)制表示形式寫入 w。data 必須是固定大小的值或固定大小值的切片,或指向此類數(shù)據(jù)的指針。
func Write(w io.Writer, order ByteOrder, data interface{}) error
// Size 返回 Wirte 函數(shù)將 v 寫入到 w 中的字節(jié)數(shù)。
func Size(v interface{}) int
下面以我們熟知的 C 標(biāo)準(zhǔn)庫函數(shù) ntohl() 函數(shù)為例,看看 Go 利用 binary 包如何實(shí)現(xiàn):
// Ntohl 將網(wǎng)絡(luò)字節(jié)序的 uint32 轉(zhuǎn)為主機(jī)字節(jié)序。
func Ntohl(bys []byte) uint32 {
r := bytes.NewReader(bys)
err = binary.Read(buf, binary.BigEndian, &num)
}
// 如將 IP 127.0.0.1 網(wǎng)絡(luò)字節(jié)序解析到 uint32
fmt.Println(Ntohl([]byte{0x7f, 0, 0, 0x1})) // 2130706433
如果我們針對 uint32 類型手動實(shí)現(xiàn)一個(gè) ntohl() 呢?
func NtohlNotUseBinary(bys []byte) uint32 {
return uint32(bys[3]) | uint32(bys[2])<<8 | uint32(bys[1])<<16 | uint32(bys[0])<<24
}
// 如將 IP 127.0.0.1 網(wǎng)絡(luò)字節(jié)序解析到 uint32
fmt.Println(NtohlNotUseBinary([]byte{0x7f, 0, 0, 0x1})) // 2130706433
該函數(shù)也是參考了 encoding/binary 包針對大端字節(jié)序?qū)⒆止?jié)序列轉(zhuǎn)為 uint32 類型時(shí)的實(shí)現(xiàn)。
下面看下剝?nèi)シ瓷淝昂蠖叩男阅懿町悾?/p>
func BenchmarkNtohl(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = Ntohl([]byte{0x7f, 0, 0, 0x1})
}
}
func BenchmarkNtohlNotUseBinary(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NtohlNotUseBinary([]byte{0x7f, 0, 0, 0x1})
}
}
運(yùn)行上面的基準(zhǔn)測試,結(jié)果如下:
go test -bench=BenchmarkNtohl.* -benchmem main/reflect
goos: darwin
goarch: amd64
pkg: main/reflect
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkNtohl-12 13026195 81.96 ns/op 60 B/op 4 allocs/op
BenchmarkNtohlNotUseBinary-12 1000000000 0.2511 ns/op 0 B/op 0 allocs/op
PASS
ok main/reflect 1.841s
可見使用反射實(shí)現(xiàn)的 encoding/binary 包的性能相較于針對具體類型實(shí)現(xiàn)的版本,性能差異非常大。
2. 避免重復(fù)的字符串到字節(jié)切片的轉(zhuǎn)換
不要反復(fù)從固定字符串創(chuàng)建字節(jié) slice,因?yàn)橹貜?fù)的切片初始化會帶來性能損耗。相反,請執(zhí)行一次轉(zhuǎn)換并捕獲結(jié)果。
// Bad
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
BenchmarkBad-4 50000000 22.2 ns/op
// Good
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}
BenchmarkGood-4 500000000 3.25 ns/op
3. 指定容器容量
盡可能指定容器容量,以便為容器預(yù)先分配內(nèi)存。這將在后續(xù)添加元素時(shí)減少通過復(fù)制來調(diào)整容器大小。
3.1 指定 map 容量提示
在盡可能的情況下,在使用 make() 初始化的時(shí)候提供容量信息。
make(map[T1]T2, hint)
向 make() 提供容量提示會在初始化時(shí)嘗試調(diào)整 map 的大小,這將減少在將元素添加到 map 時(shí)為 map 重新分配內(nèi)存。
注意,與 slice 不同。map capacity 提示并不保證完全的搶占式分配,而是用于估計(jì)所需的 hashmap bucket 的數(shù)量。因此,在將元素添加到 map 時(shí),甚至在指定 map 容量時(shí),仍可能發(fā)生分配。
// Bad
m := make(map[string]os.FileInfo)
files, _ := ioutil.ReadDir("./files")
for _, f := range files {
m[f.Name()] = f
}
// m 是在沒有大小提示的情況下創(chuàng)建的; 在運(yùn)行時(shí)可能會有更多分配。
// Good
files, _ := ioutil.ReadDir("./files")
m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
m[f.Name()] = f
}
// m 是有大小提示創(chuàng)建的;在運(yùn)行時(shí)可能會有更少的分配。
3.2 指定切片容量
在盡可能的情況下,在使用 make() 初始化切片時(shí)提供容量信息,特別是在追加切片時(shí)。
make([]T, length, capacity)
與 map 不同,slice capacity 不是一個(gè)提示:編譯器將為提供給 make() 的 slice 的容量分配足夠的內(nèi)存,這意味著后續(xù)的 append() 操作將導(dǎo)致零分配(直到 slice 的長度與容量匹配,在此之后,任何 append 都可能調(diào)整大小以容納其他元素)。
const size = 1000000
// Bad
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
BenchmarkBad-4 219 5202179 ns/op
// Good
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
BenchmarkGood-4 706 1528934 ns/op
執(zhí)行基準(zhǔn)測試:
go test -bench=^BenchmarkJoinStr -benchmem
BenchmarkJoinStrWithOperator-8 66930670 17.81 ns/op 0 B/op 0 allocs/op
BenchmarkJoinStrWithSprintf-8 7032921 166.0 ns/op 64 B/op 4 allocs/op
4. 字符串拼接方式的選擇
4.1 行內(nèi)拼接字符串推薦使用運(yùn)算符+
行內(nèi)拼接字符串為了書寫方便快捷,最常用的兩個(gè)方法是:
- 運(yùn)算符+
- fmt.Sprintf()
行內(nèi)字符串的拼接,主要追求的是代碼的簡潔可讀。fmt.Sprintf() 能夠接收不同類型的入?yún)?,通過格式化輸出完成字符串的拼接,使用非常方便。但因其底層實(shí)現(xiàn)使用了反射,性能上會有所損耗。
運(yùn)算符 + 只能簡單地完成字符串之間的拼接,非字符串類型的變量需要單獨(dú)做類型轉(zhuǎn)換。行內(nèi)拼接字符串不會產(chǎn)生內(nèi)存分配,也不涉及類型地動態(tài)轉(zhuǎn)換,所以性能上優(yōu)于fmt.Sprintf()。
從性能出發(fā),兼顧易用可讀,如果待拼接的變量不涉及類型轉(zhuǎn)換且數(shù)量較少(<=5),行內(nèi)拼接字符串推薦使用運(yùn)算符 +,反之使用 fmt.Sprintf()。
下面看下二者的性能對比:
// Good
func BenchmarkJoinStrWithOperator(b *testing.B) {
s1, s2, s3 := "foo", "bar", "baz"
for i := 0; i < b.N; i++ {
_ = s1 + s2 + s3
}
}
// Bad
func BenchmarkJoinStrWithSprintf(b *testing.B) {
s1, s2, s3 := "foo", "bar", "baz"
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s%s%s", s1, s2, s3)
}
}
執(zhí)行基準(zhǔn)測試結(jié)果如下:
go test -bench=^BenchmarkJoinStr -benchmem .
BenchmarkJoinStrWithOperator-8 70638928 17.53 ns/op 0 B/op 0 allocs/op
BenchmarkJoinStrWithSprintf-8 7520017 157.2 ns/op 64 B/op 4 allocs/op
4.2 非行內(nèi)拼接字符串推薦使用 strings.Builder
字符串拼接還有其他的方式,比如strings.Join()、strings.Builder、bytes.Buffer和byte[],這幾種不適合行內(nèi)使用。當(dāng)待拼接字符串?dāng)?shù)量較多時(shí)可考慮使用。
先看下其性能測試的對比:
func BenchmarkJoinStrWithStringsJoin(b *testing.B) {
s1, s2, s3 := "foo", "bar", "baz"
for i := 0; i < b.N; i++ {
_ = strings.Join([]string{s1, s2, s3}, "")
}
}
func BenchmarkJoinStrWithStringsBuilder(b *testing.B) {
s1, s2, s3 := "foo", "bar", "baz"
for i := 0; i < b.N; i++ {
var builder strings.Builder
_, _ = builder.WriteString(s1)
_, _ = builder.WriteString(s2)
_, _ = builder.WriteString(s3)
}
}
func BenchmarkJoinStrWithBytesBuffer(b *testing.B) {
s1, s2, s3 := "foo", "bar", "baz"
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
_, _ = buffer.WriteString(s1)
_, _ = buffer.WriteString(s2)
_, _ = buffer.WriteString(s3)
}
}
func BenchmarkJoinStrWithByteSlice(b *testing.B) {
s1, s2, s3 := "foo", "bar", "baz"
for i := 0; i < b.N; i++ {
var bys []byte
bys= append(bys, s1...)
bys= append(bys, s2...)
_ = append(bys, s3...)
}
}
func BenchmarkJoinStrWithByteSlicePreAlloc(b *testing.B) {
s1, s2, s3 := "foo", "bar", "baz"
for i := 0; i < b.N; i++ {
bys:= make([]byte, 0, 9)
bys= append(bys, s1...)
bys= append(bys, s2...)
_ = append(bys
網(wǎng)站欄目:Go高性能編程技法
分享URL:http://fisionsoft.com.cn/article/dphcjsd.html


咨詢
建站咨詢
