日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Go高性能編程技法

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

創(chuàng)新互聯(lián)服務(wù)項目包括浠水網(wǎng)站建設(shè)、浠水網(wǎng)站制作、浠水網(wǎng)頁制作以及浠水網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,浠水網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到浠水省份的部分城市,未來相信會繼續(xù)擴大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!

常用數(shù)據(jù)結(jié)構(gòu)

1.反射雖好,切莫貪杯

標準庫 reflect 為 Go 語言提供了運行時動態(tài)獲取對象的類型和值以及動態(tài)創(chuàng)建對象的能力。反射可以幫助抽象和簡化代碼,提高開發(fā)效率。

Go 語言標準庫以及很多開源軟件中都使用了 Go 語言的反射能力,例如用于序列化和反序列化的 json、ORM 框架 gorm、xorm 等。

1.1 優(yōu)先使用 strconv 而不是 fmt

基本數(shù)據(jù)類型與字符串之間的轉(zhuǎn)換,優(yōu)先使用 strconv 而不是 fmt,因為前者性能更佳:

// 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

為什么性能上會有兩倍多的差距,因為 fmt 實現(xiàn)上利用反射來達到范型的效果,在運行時進行類型的動態(tài)判斷,所以帶來了一定的性能損耗。

1.2 少量的重復(fù)不比反射差

有時,我們需要一些工具函數(shù)。比如從 uint64 切片過濾掉指定的元素。

利用反射,我們可以實現(xiàn)一個類型泛化支持擴展的切片過濾函數(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()
}

很多時候,我們可能只需要操作一個類型的切片,利用反射實現(xiàn)的類型泛化擴展的能力壓根沒用上。退一步說,如果我們真地需要對 uint64 以外類型的切片進行過濾,拷貝一次代碼又何妨呢?可以肯定的是,絕大部份場景,根本不會對所有類型的切片進行過濾,那么反射帶來好處我們并沒有充分享受,但卻要為其帶來的性能成本買單。

// 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...)
}
}

運行上面的基準測試。

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 中,因為 map 的 key 是不確定的類型,會發(fā)生變量逃逸,觸發(fā)堆內(nèi)存的分配。所以,可預(yù)見的是當元素數(shù)量增加時,性能差異會越來大。

當使用反射時,請問一下自己,我真地需要它嗎?

1.3 慎用 binary.Read 和 binary.Write

binary.Read 和 binary.Write 使用反射并且很慢。如果有需要用到這兩個函數(shù)的地方,我們應(yīng)該手動實現(xiàn)這兩個函數(shù)的相關(guān)功能,而不是直接去使用它們。

encoding/binary 包實現(xiàn)了數(shù)字和字節(jié)序列之間的簡單轉(zhuǎn)換以及 varints 的編碼和解碼。varints 是一種使用可變字節(jié)表示整數(shù)的方法。其中數(shù)值本身越小,其所占用的字節(jié)數(shù)越少。Protocol Buffers 對整數(shù)采用的便是這種編碼方式。

其中數(shù)字與字節(jié)序列的轉(zhuǎn)換可以用如下三個函數(shù):

// Read 從結(jié)構(gòu)化二進制數(shù)據(jù) r 讀取到 data。data 必須是指向固定大小值的指針或固定大小值的切片。
func Read(r io.Reader, order ByteOrder, data interface{}) error
// Write 將 data 的二進制表示形式寫入 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 標準庫函數(shù) ntohl() 函數(shù)為例,看看 Go 利用 binary 包如何實現(xiàn):

// Ntohl 將網(wǎng)絡(luò)字節(jié)序的 uint32 轉(zhuǎn)為主機字節(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 類型手動實現(xiàn)一個 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 類型時的實現(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})
}
}



運行上面的基準測試,結(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



可見使用反射實現(xiàn)的 encoding/binary 包的性能相較于針對具體類型實現(xiàn)的版本,性能差異非常大。

2. 避免重復(fù)的字符串到字節(jié)切片的轉(zhuǎn)換

不要反復(fù)從固定字符串創(chuàng)建字節(jié) slice,因為重復(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ù)添加元素時減少通過復(fù)制來調(diào)整容器大小。

3.1 指定 map 容量提示

在盡可能的情況下,在使用 make() 初始化的時候提供容量信息。

make(map[T1]T2, hint)



向 make() 提供容量提示會在初始化時嘗試調(diào)整 map 的大小,這將減少在將元素添加到 map 時為 map 重新分配內(nèi)存。

注意,與 slice 不同。map capacity 提示并不保證完全的搶占式分配,而是用于估計所需的 hashmap bucket 的數(shù)量。因此,在將元素添加到 map 時,甚至在指定 map 容量時,仍可能發(fā)生分配。

// Bad
m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
m[f.Name()] = f
}
// m 是在沒有大小提示的情況下創(chuàng)建的; 在運行時可能會有更多分配。

// Good
files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
m[f.Name()] = f
}
// m 是有大小提示創(chuàng)建的;在運行時可能會有更少的分配。



3.2 指定切片容量

在盡可能的情況下,在使用 make() 初始化切片時提供容量信息,特別是在追加切片時。

make([]T, length, capacity)



與 map 不同,slice capacity 不是一個提示:編譯器將為提供給 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í)行基準測試:

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)拼接字符串推薦使用運算符+

行內(nèi)拼接字符串為了書寫方便快捷,最常用的兩個方法是:

  • 運算符+
  • fmt.Sprintf()

行內(nèi)字符串的拼接,主要追求的是代碼的簡潔可讀。fmt.Sprintf() 能夠接收不同類型的入?yún)?,通過格式化輸出完成字符串的拼接,使用非常方便。但因其底層實現(xiàn)使用了反射,性能上會有所損耗。

運算符 + 只能簡單地完成字符串之間的拼接,非字符串類型的變量需要單獨做類型轉(zhuǎn)換。行內(nèi)拼接字符串不會產(chǎn)生內(nèi)存分配,也不涉及類型地動態(tài)轉(zhuǎn)換,所以性能上優(yōu)于fmt.Sprintf()。

從性能出發(fā),兼顧易用可讀,如果待拼接的變量不涉及類型轉(zhuǎn)換且數(shù)量較少(<=5),行內(nèi)拼接字符串推薦使用運算符 +,反之使用 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í)行基準測試結(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)使用。當待拼接字符串數(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
新聞名稱:Go高性能編程技法
當前地址:http://www.5511xx.com/article/dphcjsd.html