自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go 真實項目的性能案例研究

開發(fā) 前端
避免支付這種懲罰的方法是簡單地使用指向結(jié)構(gòu)的指針作為接收者,避免昂貴的內(nèi)存復(fù)制操作。但是在概括這個建議時要小心——你對哪種技術(shù)性能更好的直覺可能是不正確的,因為它實際上取決于許多與你的應(yīng)用程序不同的因素。

大家好,我是程序員幽鬼。

Dolt DB[1] 是世界上第一個可以像 git 存儲庫一樣分支和合并、推送和拉取、分叉和克隆的 SQL 數(shù)據(jù)庫。

我們從頭開始構(gòu)建 Dolt 的存儲引擎,以加快這些操作。編寫行存儲引擎和 SQL 執(zhí)行引擎并不容易。大多數(shù)人甚至不嘗試它是有原因的,但在 DoltHub,我們的構(gòu)建方式不同。

今天,我們將回顧幾個在對 Dolt 進行基準(zhǔn)測試以使行訪問與 MySQL 一樣快時遇到的性能問題的案例研究。每個案例研究都是我們遇到并解決的實際性能問題。

讓我們開始吧。

案例研究:接口斷言

在嘗試以更快的方式將行從磁盤中讀出并通過執(zhí)行引擎獲取時,我們決定制作新的行迭代接口。因為我們已經(jīng)有了許多原始接口的實現(xiàn),所以我們認(rèn)為可以擴展它,如下所示:

type RowIter interface {
Next(ctx *Context) (Row, error)
}

type RowIter2 interface {
RowIter
Next2(ctx *Context, frame *RowFrame) error
}

我們的 SQL 引擎的架構(gòu)涉及一個深度嵌套的執(zhí)行圖,圖中的每個節(jié)點從下一層獲取一行,對其進行一些工作,然后將其傳遞到鏈上。當(dāng)我們在迭代器中實現(xiàn)該Next2方法時,它看起來像這樣:

type iter struct {
childIter sql.RowIter
}

func (t iter) Next2(ctx *sql.Context, frame *sql.RowFrame) error {
return t.childIter.(sql.RowIter2).Next2(ctx, frame)
}

但是當(dāng)我們運行這段代碼時,我們在分析器圖表中注意到,由于這種模式,我們付出了非常可觀的性能損失。在我們的 CPU 圖表中,我們注意到很多時間都花在了 runtime.assertI2I:

圖片

runtime.assertI2I是 go 運行時調(diào)用的方法,用于驗證接口的靜態(tài)類型是否可以在運行時轉(zhuǎn)換為不同的類型。這涉及到接口表的查找和接口指針轉(zhuǎn)換,這不是那么昂貴,但也肯定不是免費的。由于嵌套的執(zhí)行圖,我們在每行獲取的數(shù)據(jù)中多次執(zhí)行此操作,從而導(dǎo)致相當(dāng)嚴(yán)重的性能損失。

解決方案:消除接口類型轉(zhuǎn)換

為了消除這種損失,我們只需在必要的地方存儲兩個字段來消除轉(zhuǎn)換,每個靜態(tài)類型一個字段。

type iter struct {
childIter sql.RowIter
childIter2 sql.RowIter2
}
func (t iter) Next(...) {
return t.childIter.Next(ctx)
}
func (t iter) Next2(...) {
return t.childIter2.Next2(ctx, frame)
}

這兩個字段指向內(nèi)存中的同一個對象,但由于它們具有不同的靜態(tài)類型,因此它們需要不同的字段以避免轉(zhuǎn)換損失。

案例研究:切片

接下來,我們將研究分配切片的不同方法及其對性能的影響,尤其是垃圾收集器開銷。我們將分析一種生成隨機元素切片然后將它們相加的方法。我們將在我們的個人資料中一遍又一遍地調(diào)用它。

func sumArray() uint64 {
a := randArray()
var sum uint64
for i := range a {
sum += uint64(a[i].(byte))
}
return sum
}

我們將研究實現(xiàn)該randArray() 功能的 4 種不同方法,從最差到最好,并展示不同實現(xiàn)對程序性能的影響。

最差:append 到切片

獲得這個切片的更糟糕的方法是創(chuàng)建一個長度為零的切片,然后一遍又一遍地調(diào)用 append,如下所示:

func randArray() []interface{} {
var a []interface{}
for i := 0; i < 1000; i++ {
a = append(a, byte(rand.Int() % 255))
}
return a
}

不管我們是從上面的 nil 切片開始,還是用 make([]interface{}, 0) 制作一個長度為零的切片,效果都是一樣的。當(dāng)我們基于 profile 生成火焰圖時,我們可以看到垃圾收集占用了巨大的開銷,并且 runtime.growslice占用了整整四分之一的 CPU 周期。

圖片

總的來說,只有不到一半的 CPU 周期直接用于有用的工作。

之所以如此昂貴,是因為 go slices 有一個底層數(shù)組,當(dāng)調(diào)用 append 時,如果底層數(shù)組容量不足,運行時必須分配一個更大的數(shù)組,復(fù)制所有元素,并回收舊數(shù)組。

圖片

我們可以做得比這更好。

更好:靜態(tài)數(shù)組大小

我們可以通過在創(chuàng)建時固定切片的大小來消除 runtime.growslice 開銷,就像這樣。

func randArray() []interface{} {
a := make([]interface{}, 1000)
for i := 0; i < len(a); i++ {
a[i] = byte(rand.Int() % 255)
}
return a
}

當(dāng)我們 profile 這段代碼時,我們可以看到已經(jīng)完全消除了 runtime.growslice 的開銷,并減輕了垃圾收集器的壓力。

圖片

你可以一眼看出這個實現(xiàn)花費了更多的時間做有用的工作。但是每次創(chuàng)建切片仍然會對性能造成重大影響。我們整整 13% 的運行時間都花在分配切片上,即 runtime.makeslice.

更好的是:原始切片類型

奇怪的是,我們可以通過分配 byte 切片而不是切片interface{}來做得更好。執(zhí)行此操作的代碼:

func randArray() []byte {
a := make([]byte, 1000)
for i := 0; i < len(a); i++ {
a[i] = byte(rand.Int() % 255)
}
return a
}

當(dāng)我們查看 profile 時,我們可以看到 runtime.makesliceCPU 的影響從超過 13% 下降到大約 3%。你甚至在火焰圖上幾乎看不到它,而且也很容易看到垃圾收集器開銷的相應(yīng)減少。

圖片

造成這種差異的原因只是interface{}類型的分配成本更高(一對 8 字節(jié)指針,而不是單個字節(jié)),而且垃圾收集器推理和處理的成本也更高。這個故事的寓意是,在可能的情況下,分配原始切片類型而不是接口類型通常會在性能方面得到回報。

最佳:在循環(huán)外分配

但實現(xiàn)此功能的唯一最佳方法是根本不在其中分配任何內(nèi)存。相反,讓我們在外部范圍內(nèi)分配一次切片,然后將其傳遞給此函數(shù)以進行填充。

func randArray(a []interface{}) {
for i := 0; i < len(a); i++ {
a[i] = byte(rand.Int() % 255)
}
}

當(dāng)我們這樣做時,我們完全消除了所有垃圾收集壓力,并且我們有效地將所有 CPU 周期用于做有用的工作。

圖片

我們不必將切片作為參數(shù)傳入,我們只需要避免每次調(diào)用此函數(shù)時都分配它。實現(xiàn)此目的的另一種方法是使用全局sync.pool變量在函數(shù)調(diào)用之間重用切片。

總結(jié)

通過調(diào)用 rand.Int 所花費的時間作為我們花費多少 CPU 時間做有用工作的示例,我們得到以下摘要:

  • appending: 20%
  • interface{} 類型靜態(tài)切片: 38%
  • 靜態(tài) byte 切片 : 62%
  • 切片作為參數(shù): 70%

底線:如何使用切片會對程序的整體性能產(chǎn)生非常顯著的影響。

案例研究:結(jié)構(gòu)體還是指針作為接收器

關(guān)于在實現(xiàn)接口時是使用結(jié)構(gòu)體還是指向結(jié)構(gòu)體的指針作為接收器類型,golang 社區(qū)中一直存在著非常激烈的爭論。意見不一。

現(xiàn)實情況是,這兩種方法都需要權(quán)衡取舍。將結(jié)構(gòu)體復(fù)制到堆棧上通常比指針更昂貴,尤其是對于大型結(jié)構(gòu)。但是將對象保留在堆棧上而不是堆上可以避免垃圾收集器的壓力,這在某些情況下可能會更快。從美學(xué)/設(shè)計的角度來看,也存在權(quán)衡:有時確實需要強制執(zhí)行不變性語義,這可以通過結(jié)構(gòu)體接收器獲得。

讓我們來說明你為大型結(jié)構(gòu)付出的性能損失。我們將使用一個非常大的 36 個字段,并在其上一遍又一遍地調(diào)用一個方法。

type bigStruct struct {
v1, v2, v3, v4, v5, v6, v7, v8, v9 uint64
f1, f2, f3, f4, f5, f6, f7, f8, f9 float64
b1, b2, b3, b4, b5, b6, b7, b8, b9 []byte
s1, s2, s3, s4, s5, s6, s7, s8, s9 string
}

func (b bigStruct) randFloat() float64 {
x := rand.Float32()
switch {
case x < .1:
return b.f1

}

當(dāng)我們一次又一次地分析這個方法時,我們可以看到,我們在一個名為 runtime.duffcopy 的東西上付出了非常大的代價,35% 的 CPU 周期。

圖片

runtime.duffcopy 是什么?在某些情況下,這是 go runtime 復(fù)制大量連續(xù)內(nèi)存塊(通常是結(jié)構(gòu))的方式。之所以這樣稱呼它,是因為 duffcopy 有點像 Duff 的設(shè)備,它是 Tom Duff 在 80 年代發(fā)現(xiàn)的 C 編譯器黑客。他意識到你可以濫用 C 編譯器通過交錯循環(huán)和開關(guān)的構(gòu)造來實現(xiàn)循環(huán)展開:

register short *to, *from;
register count;
{
register n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}

循環(huán)展開可以極大地加快速度,因為你不必在每次通過循環(huán)時檢查循環(huán)條件。Go 的 duffcopy 實際上并不是 Duff 的設(shè)備,但它是一個循環(huán)展開:編譯器發(fā)出 N 條指令來復(fù)制內(nèi)存,而不是使用循環(huán)來這樣做。

避免支付這種懲罰的方法是簡單地使用指向結(jié)構(gòu)的指針作為接收者,避免昂貴的內(nèi)存復(fù)制操作。但是在概括這個建議時要小心——你對哪種技術(shù)性能更好的直覺可能是不正確的,因為它實際上取決于許多與你的應(yīng)用程序不同的因素。歸根結(jié)底,你確實需要對替代方案進行 profile 分析,以了解哪些方案總體上表現(xiàn)更好,包括垃圾收集的影響。

總結(jié)

要使 Dolt 與更成熟的數(shù)據(jù)庫技術(shù)一樣具有高性能,我們還有很多工作要做,尤其是在查詢計劃方面。但是我們已經(jīng)設(shè)法通過這些簡單的優(yōu)化將引擎的性能提高了 2 倍,并通過完全重寫我們的存儲層再提高了 3 倍[2]。在簡單的基準(zhǔn)測試中,我們現(xiàn)在的延遲大約是 MySQL 的 2.5 倍,但我們還沒有完成。隨著我們接近 1.0 版本,我們很高興能夠繼續(xù)提高我們的數(shù)據(jù)庫速度。

原文鏈接:https://www.dolthub.com/blog/2022-10-14-golang-performance-case-studies/

參考資料

[1]Dolt DB: https://doltdb.com/

[2]并通過完全重寫我們的存儲層再提高了 3 倍: https://www.dolthub.com/blog/2022-09-30-new-format-default/

責(zé)任編輯:武曉燕 來源: 幽鬼
相關(guān)推薦

2023-07-07 07:22:13

ReactProfiler

2021-12-29 08:21:01

Performance優(yōu)化案例工具

2021-07-06 12:07:27

Go 服務(wù)性能

2018-01-15 15:35:15

數(shù)據(jù)庫性能調(diào)優(yōu)案例

2010-10-09 10:39:45

2010-08-30 17:52:31

Windows Azu云計算數(shù)據(jù)中心

2019-03-22 13:46:13

公共云云計算云端

2011-04-01 10:55:41

OA系統(tǒng)應(yīng)用案例

2020-07-13 10:26:41

CIOIT項目技術(shù)

2024-12-17 09:14:48

項目http 庫API

2024-10-28 09:04:38

Go項目客戶端

2022-03-09 20:18:49

TypeScript類型函數(shù)

2024-02-27 13:07:49

用戶畫像數(shù)據(jù)分析HR

2022-12-02 10:38:50

機器學(xué)習(xí)開發(fā)工程項目

2016-07-04 14:22:47

DevOps案例軟件

2016-09-27 20:47:01

2018-04-05 23:29:35

2022-06-13 14:58:19

系統(tǒng)案例

2013-08-06 13:45:29

Android性能個案

2012-05-15 02:54:31

項目開發(fā)項目
點贊
收藏

51CTO技術(shù)棧公眾號