高性能 Go 服務(wù)的內(nèi)存優(yōu)化(譯)

原文地址: Allocation Efficiency in High-Performance Go Services, 沒(méi)有原模原樣的翻譯, 但不影響理解蔬墩。

關(guān)于工具

我們的第一個(gè)建議就是: 不要過(guò)早優(yōu)化。Go 提供了很棒的性能調(diào)優(yōu)工具可以直接指出代碼上哪里消耗了大量?jī)?nèi)存。沒(méi)必要重復(fù)造輪子,建議讀者閱讀下 Go 官方博客上的這篇很贊的文章诗祸;里面會(huì)一步步教你使用 pprof 對(duì) CPU 和內(nèi)存進(jìn)行調(diào)優(yōu)溪厘。在 Segment 我們也是用這些工具去找到項(xiàng)目的性能瓶頸的。

用數(shù)據(jù)來(lái)驅(qū)動(dòng)優(yōu)化忠荞。

逃逸分析

Go 可以自動(dòng)的管理內(nèi)存,這幫我們避免了大量潛在 bug帅掘,但它并沒(méi)有將程序員徹底的從內(nèi)存分配的事情上解脫出來(lái)委煤。因?yàn)?Go 沒(méi)有提供直接操作內(nèi)存的方式,所以開發(fā)者必須要搞懂其內(nèi)部機(jī)制修档,這樣才能將收益最大化碧绞。

如果讀了這篇文章后,你只能記住一點(diǎn)吱窝,那請(qǐng)記住這個(gè):棧分配廉價(jià)讥邻,堆分配昂貴。現(xiàn)在讓我們深入講述下這是什么意思院峡。

Go 有兩個(gè)地方可以分配內(nèi)存:一個(gè)全局堆空間用來(lái)動(dòng)態(tài)分配內(nèi)存兴使,另一個(gè)是每個(gè) goroutine 都有的自身?xiàng)?臻g照激。
Go 更傾向于在椃⑵牵空間上分配內(nèi)存 —— 一個(gè) Go 程序大部分的內(nèi)存分配都是在棧空間上的俩垃。它的代價(jià)很低励幼,因?yàn)橹恍枰獌蓚€(gè) CPU 指令:一個(gè)是把數(shù)據(jù) push 到棧空間上以完成分配口柳,另一個(gè)是從椘凰冢空間上釋放。

不幸的是, 不是所有的內(nèi)存都可以在椩灸郑空間上分配的嵌削。椕茫空間分配要求一個(gè)變量的生命周期和內(nèi)存足跡能在編譯時(shí)確定。
否則就需要在運(yùn)行時(shí)在堆空間上進(jìn)行動(dòng)態(tài)分配掷贾。
malloc 必須找到一塊足夠大的內(nèi)存來(lái)存放新的變量數(shù)據(jù)睛榄。后續(xù)釋放時(shí),垃圾回收器掃描堆空間尋找不再被使用的對(duì)象想帅。
不用多說(shuō)场靴,這明顯要比只需兩個(gè)指令的棧分配更加昂貴。

譯者注: 內(nèi)存足跡, 代表和一個(gè)變量相關(guān)的所有內(nèi)存塊港准。
比如一個(gè) struct 中含有成員 *int, 那么這個(gè) *int 所指向的內(nèi)存塊屬于該 struct 的足跡旨剥。

編譯器使用逃逸分析的技術(shù)來(lái)在這兩者間做選擇∏掣祝基本的思路就是在編譯時(shí)做垃圾回收的工作轨帜。
編譯器會(huì)追蹤變量在代碼塊上的作用域。變量會(huì)攜帶有一組校驗(yàn)數(shù)據(jù)衩椒,用來(lái)證明它的整個(gè)生命周期是否在運(yùn)行時(shí)完全可知蚌父。如果變量通過(guò)了這些校驗(yàn),它就可以在棧上分配毛萌。否則就說(shuō)它 逃逸 了苟弛,必須在堆上分配。

逃逸分析的機(jī)制阁将,并沒(méi)有在 Go 語(yǔ)言官方說(shuō)明上闡述膏秫。對(duì) Go 程序員來(lái)說(shuō),學(xué)習(xí)這些規(guī)則最有效的方式就是憑經(jīng)驗(yàn)做盅。編譯命令 go build -gcflags '-m' 會(huì)讓編譯器在編譯時(shí)輸出逃逸分析的結(jié)果缤削。
讓我們來(lái)看一個(gè)例子:

package main

import "fmt"

func main() {
        x := 42
        fmt.Println(x)
}
$ go build -gcflags '-m' ./main.go
# command-line-arguments
./main.go:7: x escapes to heap
./main.go:7: main ... argument does not escape

我們看到 x escapes to heap, 表示它會(huì)在運(yùn)行時(shí)在堆空間上動(dòng)態(tài)分配。
這個(gè)例子讓人有些費(fèi)解吹榴,直覺(jué)上亭敢,很明顯變量 x 并沒(méi)有逃出 main() 函數(shù)之外。
編譯器沒(méi)有說(shuō)明它為什么認(rèn)為這個(gè)變量逃逸了图筹。為得到更詳細(xì)的內(nèi)容帅刀,多傳幾個(gè) -m 參數(shù)給編譯器,會(huì)打印出更詳細(xì)的內(nèi)容婿斥。

$ go build -gcflags '-m -m' ./main.go
# command-line-arguments
./main.go:5: cannot inline main: non-leaf function
./main.go:7: x escapes to heap
./main.go:7:         from ... argument (arg to ...) at ./main.go:7
./main.go:7:         from *(... argument) (indirection) at ./main.go:7
./main.go:7:         from ... argument (passed to call[argument content escapes]) at ./main.go:7
./main.go:7: main ... argument does not escape

是的,上面顯示了哨鸭,變量 x 之所以逃逸了民宿,是因?yàn)樗粋魅肓艘粋€(gè)逃逸的函數(shù)內(nèi)。

這個(gè)機(jī)制乍看上去有些難以捉摸像鸡,但多用幾次這個(gè)工具后活鹰,就能搞明白這其中的規(guī)律了哈恰。長(zhǎng)話短說(shuō),下面是一些我們找到的志群,能引起變量逃逸到堆上的典型情況:

  • 發(fā)送指針或帶有指針的值到 channel 中着绷。在編譯時(shí),是沒(méi)有辦法知道哪個(gè) goroutine 會(huì)在 channel 上接收數(shù)據(jù)锌云。所以編譯器沒(méi)法知道變量什么時(shí)候才會(huì)被釋放荠医。

  • 在一個(gè)切片上存儲(chǔ)指針或帶指針的值。一個(gè)典型的例子就是 []*string桑涎。這會(huì)導(dǎo)致切片的內(nèi)容逃逸彬向。盡管其后面的數(shù)組可能是在棧上分配的,但其引用的值一定是在堆上攻冷。

  • slice 的背后數(shù)組被重新分配了娃胆,因?yàn)?append 時(shí)可能會(huì)超出其容量(cap)。slice 初始化的地方在編譯時(shí)是可以知道的等曼,它最開始會(huì)在棧上分配里烦。如果切片背后的存儲(chǔ)要基于運(yùn)行時(shí)的數(shù)據(jù)進(jìn)行擴(kuò)充,就會(huì)在堆上分配禁谦。

  • 在 interface 類型上調(diào)用方法胁黑。在 interface 類型上調(diào)用方法都是動(dòng)態(tài)調(diào)度的 —— 方法的真正實(shí)現(xiàn)只能在運(yùn)行時(shí)知道。想像一個(gè) io.Reader 類型的變量 r, 調(diào)用 r.Read(b) 會(huì)使得 r 的值和切片 b 的背后存儲(chǔ)都逃逸掉枷畏,所以會(huì)在堆上分配别厘。

以我們的經(jīng)驗(yàn),這四點(diǎn)是 Go 程序中最常見的導(dǎo)致堆分配的原因拥诡。幸運(yùn)的是触趴,是有解決辦法的!下面我們深入幾個(gè)具體例子說(shuō)明渴肉,如何定位線上系統(tǒng)的內(nèi)存性能問(wèn)題冗懦。

關(guān)于指針

一個(gè)經(jīng)驗(yàn)是:指針指向的數(shù)據(jù)都是在堆上分配的。因此仇祭,在程序中減少指針的運(yùn)用可以減少堆分配披蕉。這不是絕對(duì)的,但是我們發(fā)現(xiàn)這是在實(shí)際問(wèn)題中最常見的問(wèn)題乌奇。

一般情況下我們會(huì)這樣認(rèn)為:“值的拷貝是昂貴的没讲,所以用一個(gè)指針來(lái)代替〗该纾”
但是爬凑,在很多情況下,直接的值拷貝要比使用指針廉價(jià)的多试伙。你可能要問(wèn)為什么嘁信。

  • 編譯器會(huì)在解除指針時(shí)做檢查于样。目的是在指針是 nil 的情況下直接 panic() 以避免內(nèi)存泄露。這就必須在運(yùn)行時(shí)執(zhí)行更多的代碼潘靖。如果數(shù)據(jù)是按值傳遞的穿剖,那就不需要做這些了,它不可能是 nil

  • 指針通常有糟糕的局部引用卦溢。一個(gè)函數(shù)內(nèi)部的所有值都會(huì)在椇啵空間上分配。局部引用是編寫高效代碼的重要環(huán)節(jié)既绕。它會(huì)使得變量數(shù)據(jù)在 CPU Cache(cpu 的一級(jí)二級(jí)緩存) 中的熱度更高啄刹,進(jìn)而減少指令預(yù)取時(shí) Cache 不命中的的幾率。

  • 在 Cache 層拷貝一堆對(duì)象凄贩,可粗略地認(rèn)為和拷貝一個(gè)指針效率是一樣的誓军。CPU 在各 Cache 層和主內(nèi)存中以固定大小的 cache 進(jìn)行內(nèi)存移動(dòng)。x86 機(jī)器上是 64 字節(jié)疲扎。而且昵时,Go 使用了Duff’s device 技術(shù)來(lái)使得常規(guī)內(nèi)存操作變得更高效。

指針應(yīng)該主要被用來(lái)做映射數(shù)據(jù)的所有權(quán)和可變性的椒丧。實(shí)際項(xiàng)目中壹甥,用指針來(lái)避免拷貝的方式應(yīng)該盡量少用。
不要掉進(jìn)過(guò)早優(yōu)化的陷阱壶熏。養(yǎng)成一個(gè)按值傳遞的習(xí)慣句柠,只在需要的時(shí)候用指針傳遞。另一個(gè)好處就是可以較少 nil 帶來(lái)的安全問(wèn)題棒假。

減少程序中指針的使用的另一個(gè)好處是溯职,如果可以證明它里面沒(méi)有指針,垃圾回收器會(huì)直接越過(guò)這塊內(nèi)存帽哑。例如谜酒,一塊作為 []byte 背后存儲(chǔ)的堆上內(nèi)存,是不需要進(jìn)行掃描的妻枕。對(duì)于那些不包含指針的數(shù)組和 struct 數(shù)據(jù)類型也是一樣的僻族。

譯者注: 垃圾回收器回收一個(gè)變量時(shí),要檢查該類型里是否有指針屡谐。
如果有述么,要檢查指針?biāo)赶虻膬?nèi)存是否可被回收,進(jìn)而才能決定這個(gè)變量能否被回收愕掏。如此遞歸下去度秘。
如果被回收的變量里面沒(méi)有指針, 就不需要進(jìn)去遞歸掃描了,直接回收掉就行亭珍。

減少指針的使用不僅可以降低垃圾回收的工作量敷钾,它會(huì)產(chǎn)生對(duì) cache 更加友好的代碼。讀內(nèi)存是要把數(shù)據(jù)從主內(nèi)存讀到 CPU 的 cache 中肄梨。
Cache 的空間是有限的阻荒,所以其他的數(shù)據(jù)必須被抹掉,好騰出空間众羡。
被抹掉的數(shù)據(jù)很可能程序的另外一部分相關(guān)侨赡。
由此產(chǎn)生的 cache 抖動(dòng)會(huì)引起線上服務(wù)的一些意外的和突然的抖動(dòng)。

還是關(guān)于指針

減少指針的使用就意味著要深入我們自定義的數(shù)據(jù)類型粱侣。我們的一個(gè)服務(wù)羊壹,用帶有一組數(shù)據(jù)結(jié)構(gòu)的循環(huán) buffer 構(gòu)建了一個(gè)失敗操作的隊(duì)列好做重試;它大致是這個(gè)樣子:

type retryQueue struct {
    buckets       [][]retryItem // each bucket represents a 1 second interval
    currentTime   time.Time
    currentOffset int
}

type retryItem struct {
    id   ksuid.KSUID // ID of the item to retry
    time time.Time   // exact time at which the item has to be retried
}

buckets 中外面的數(shù)組大小是固定的, 但是 []retryItem 中 item 的數(shù)量是在運(yùn)行時(shí)變化的齐婴。重試次數(shù)越多, 切片增長(zhǎng)的越大油猫。

挖掘一下 retryItem 的具體實(shí)現(xiàn),我們發(fā)現(xiàn) KSUID[20]byte 的別名, 里面沒(méi)有指針柠偶,所以可以排除情妖。currentOffset 是一個(gè) int 類型, 也是固定長(zhǎng)度的,故也可排除诱担。接下來(lái)毡证,看一下 time.Time 的實(shí)現(xiàn):

type Time struct {
    sec  int64
    nsec int32
    loc  *Location // pointer to the time zone structure
}

time.Time 的結(jié)構(gòu)體中包含了一個(gè)指針成員 loc。在 retryItem 中使用它會(huì)導(dǎo)致 GC 每次經(jīng)過(guò)堆上的這塊區(qū)域時(shí)蔫仙。
都要去追蹤到結(jié)構(gòu)體里面的指針。

我們發(fā)現(xiàn),這個(gè)案例很典型峡谊。 在正常運(yùn)行期間失敗情況很少趾撵。 只有少量?jī)?nèi)存用于存儲(chǔ)重試操作。 當(dāng)失敗突然飆升時(shí)涎嚼,重試隊(duì)列中的對(duì)象數(shù)量每秒增長(zhǎng)好幾千阱州,從而對(duì)垃圾回收器增加很多壓力。

在這種情況下法梯,time.Time 中的時(shí)區(qū)信息不是必要的苔货。這些保存在內(nèi)存中的時(shí)間截從來(lái)不會(huì)被序列化。所以可以重寫這個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)避免這種情況:

type retryItem struct {
    id   ksuid.KSUID
    nsec uint32
    sec  int64
}

func (item *retryItem) time() time.Time {
    return time.Unix(item.sec, int64(item.nsec))
}

func makeRetryItem(id ksuid.KSUID, time time.Time) retryItem {
    return retryItem{
        id:   id,
        nsec: uint32(time.Nanosecond()),
        sec:  time.Unix(),
}

注意現(xiàn)在的 retryItem 不包含任何指針立哑。這大大降低了 gc 壓力夜惭,因?yàn)?retryItem 的整個(gè)足跡都可以在編譯時(shí)知道。

傳遞 Slice

切片是造成低效內(nèi)存分配行為的狂熱區(qū)域铛绰。除非切片的大小在編譯時(shí)就能知道诈茧,否則切片背后的數(shù)組(map也一樣)會(huì)在堆上分配。讓我們來(lái)講幾個(gè)方法捂掰,讓切片在棧上分配而不是在堆上敢会。

一個(gè)重度依賴于 MySQL 的項(xiàng)目曾沈。整個(gè)項(xiàng)目的性能嚴(yán)重依賴 MySQL 客戶端驅(qū)動(dòng)的性能。
使用 pprof 對(duì)內(nèi)存分配進(jìn)行分析后鸥昏,我們發(fā)現(xiàn) MySQL driver 中序列化 time.Time 的那段代碼非常低效塞俱。

性能分析器顯示了堆上分配的內(nèi)存有很大比例都是用來(lái)序列化 time.Time 的,所以才導(dǎo)致了 MySQL driver 低效吏垮。

go tool pprof 結(jié)果

這段低效的代碼就是調(diào)用了 time.TimeFormat() 方法, 它返回一個(gè) string障涯。等等,我們不是在討論切片嘛膳汪?好吧唯蝶,根據(jù) Go 官方的博客,一個(gè) string 實(shí)際就是一個(gè)只讀的 []byte遗嗽,只是語(yǔ)言上在語(yǔ)法上多了點(diǎn)支持粘我。在內(nèi)存分配上規(guī)則都是一樣的。

分析結(jié)果告訴我們 12.38% 的內(nèi)存分配都是 Format() 引起的痹换,Format() 都做了什么涂滴?

它表示使用標(biāo)準(zhǔn)庫(kù)還有更高效的方式來(lái)達(dá)到通樣的效果。但是 Format() 用起來(lái)很方便晴音,使用 AppendFormat() 在內(nèi)存分配上更友好柔纵。剖析下 time 包的源碼,我們發(fā)現(xiàn)里面都是使用 AppendFormat() 而不是 Format()锤躁。這更說(shuō)明了 AppendFormat() 可以帶來(lái)更高的性能搁料。

實(shí)際上, Format() 函數(shù)只是對(duì) AppendFormat() 的一層封裝系羞。

func (t Time) Format(layout string) string {
          const bufSize = 64
          var b []byte
          max := len(layout) + 10
          if max < bufSize {
                  var buf [bufSize]byte
                  b = buf[:0]
          } else {
                  b = make([]byte, 0, max)
          }
          b = t.AppendFormat(b, layout)
          return string(b)
}

更重要的是郭计,AppendFormat() 給程序員留了更多的優(yōu)化空間。它需要傳入一個(gè)切片進(jìn)行存儲(chǔ)椒振,而不是直接返回一個(gè) string 昭伸。使用 AppendFormat() 代替 Format() 可以用固定大小的內(nèi)存空間來(lái)完成同樣的事,而且這些操作是在椗煊空間完成的庐杨。

Interface 類型

眾所周知的,在 Interface 類型上調(diào)用方法要比直接在 Struct 上調(diào)用方法效率低夹供。在 interface 類型上調(diào)用方法是動(dòng)態(tài)調(diào)度的灵份。這就極大的限制了編譯器確定運(yùn)行時(shí)代碼執(zhí)行方式的能力。到目前為止我們已經(jīng)大量的討論了哮洽,調(diào)整代碼好讓編譯器能在編譯時(shí)更好的理解你的代碼行為填渠。但 interface 類型會(huì)讓這一切都白做。

不幸的是,interface 類型還是一個(gè)非常有用的抽象方式 —— 它能讓我們寫出擴(kuò)展性更高的代碼氛什。interface 的一個(gè)普遍應(yīng)用場(chǎng)景是標(biāo)準(zhǔn)庫(kù)里的 hash 包中的哈希函數(shù)莺葫。hash 包定義了通用的接口,然后提供了幾個(gè)具體的實(shí)現(xiàn)枪眉。讓我們看幾個(gè)例子:

package main

import (
        "fmt"
        "hash/fnv"
)

func hashIt(in string) uint64 {
        h := fnv.New64a()
        h.Write([]byte(in))
        out := h.Sum64()
        return out
}

func main() {
        s := "hello"
        fmt.Printf("The FNV64a hash of '%v' is '%v'\n", s, hashIt(s))
}

編譯上段代碼徙融,加上逃逸分析參數(shù),會(huì)有以下輸出:

./foo1.go:9:17: inlining call to fnv.New64a
./foo1.go:10:16: ([]byte)(in) escapes to heap
./foo1.go:9:17: hash.Hash64(&fnv.s·2) escapes to heap
./foo1.go:9:17: &fnv.s·2 escapes to heap
./foo1.go:9:17: moved to heap: fnv.s·2
./foo1.go:8:24: hashIt in does not escape
./foo1.go:17:13: s escapes to heap
./foo1.go:17:59: hashIt(s) escapes to heap
./foo1.go:17:12: main ... argument does not escape

這說(shuō)明了瑰谜,hash 對(duì)象,輸入字符串树绩,和 []byte 都會(huì)逃逸到堆上萨脑。
人肉眼看上去很明顯這些數(shù)據(jù)根本沒(méi)有逃逸,但是 interface 類型限制了編譯器的功能饺饭。
沒(méi)有辦法不進(jìn)入 hash 的 interface 結(jié)構(gòu)而安全的調(diào)用其具體實(shí)現(xiàn)渤早。
所以碰到這種情況,除非自己手動(dòng)實(shí)現(xiàn)一個(gè)不使用 interface 的庫(kù)瘫俊,沒(méi)什么好辦法鹊杖。

一個(gè)小把戲

最后一點(diǎn)要比實(shí)際情況更搞笑。但是扛芽,它能讓我們對(duì)編譯器的逃逸分析機(jī)制有更深刻的理解骂蓖。當(dāng)通過(guò)閱讀標(biāo)準(zhǔn)庫(kù)源碼來(lái)解決性能問(wèn)題時(shí),我們看到了下面這樣的代碼:

// noescape hides a pointer from escape analysis.  noescape is
// the identity function but escape analysis doesn't think the
// output depends on the input.  noescape is inlined and currently
// compiles down to zero instructions.
// USE CAREFULLY!
//go:nosplit
func noescape(p unsafe.Pointer) unsafe.Pointer {
    x := uintptr(p)
    return unsafe.Pointer(x ^ 0)
}

這個(gè)函數(shù)會(huì)把指針參數(shù)從編譯器的逃逸分析中隱藏掉川尖。這意味著什么呢登下?讓我們來(lái)舉個(gè)例子看下。

package main

import (
        "unsafe"
)

type Foo struct {
        S *string
}

func (f *Foo) String() string {
        return *f.S
}

type FooTrick struct {
        S unsafe.Pointer
}

func (f *FooTrick) String() string {
        return *(*string)(f.S)
}

func NewFoo(s string) Foo {
        return Foo{S: &s}
}

func NewFooTrick(s string) FooTrick {
        return FooTrick{S: noescape(unsafe.Pointer(&s))}
}

func noescape(p unsafe.Pointer) unsafe.Pointer {
        x := uintptr(p)
        return unsafe.Pointer(x ^ 0)
}

func main() {
        s := "hello"
        f1 := NewFoo(s)
        f2 := NewFooTrick(s)
        s1 := f1.String()
        s2 := f2.String()
}

上段代碼對(duì)同樣的功能有兩種實(shí)現(xiàn):他們包含一個(gè) string叮喳,然后用 String() 函數(shù)返回這個(gè)字符串被芳。
但是編譯器的逃逸分析輸出表名了 FooTrick 版本沒(méi)有逃逸。

./foo3.go:24:16: &s escapes to heap
./foo3.go:23:23: moved to heap: s
./foo3.go:27:28: NewFooTrick s does not escape
./foo3.go:28:45: NewFooTrick &s does not escape
./foo3.go:31:33: noescape p does not escape
./foo3.go:38:14: main &s does not escape
./foo3.go:39:19: main &s does not escape
./foo3.go:40:17: main f1 does not escape
./foo3.go:41:17: main f2 does not escape

關(guān)鍵在這兩行

./foo3.go:24:16: &s escapes to heap
./foo3.go:23:23: moved to heap: s

編譯器識(shí)別出了 NewFoo() 函數(shù)引用了字符串并將其存儲(chǔ)在結(jié)構(gòu)體中馍悟,導(dǎo)致了逃逸畔濒。但是,NewFooTrick() 卻沒(méi)有這樣的輸出锣咒。如果把調(diào)用 noescape() 的代碼刪掉侵状,就會(huì)出現(xiàn)逃逸的情況。
到底發(fā)生了什么毅整?

func noescape(p unsafe.Pointer) unsafe.Pointer {
    x := uintptr(p)
    return unsafe.Pointer(x ^ 0)
}

noescape()函數(shù)遮蔽了輸入和輸出的依賴關(guān)系壹将。編譯器不認(rèn)為 p 會(huì)通過(guò) x 逃逸, 因?yàn)?uintptr() 產(chǎn)生的引用是編譯器無(wú)法理解的毛嫉。
內(nèi)置的 uintptr 類型讓人相信這是一個(gè)真正的指針類型诽俯,但是在編譯器層面,它只是一個(gè)足夠存儲(chǔ)一個(gè) point 的 int 類型。代碼的最后一行返回 unsafe.Pointer 也是一個(gè) int暴区。

noescape()runtime 包中使用 unsafe.Pointer 的地方被大量使用闯团。如果作者清楚被 unsafe.Pointer 引用的數(shù)據(jù)肯定不會(huì)被逃逸,但編譯器卻不知道的情況下仙粱,這是很有用的房交。

但是請(qǐng)記住,我們強(qiáng)烈不建議使用這種技術(shù)伐割。這就是為什么包的名字叫做 unsafe 而且源碼中包含了 USE CAREFULLY! 注釋的原因候味。

小貼士

  1. 不要過(guò)早優(yōu)化,用數(shù)據(jù)來(lái)驅(qū)動(dòng)我們的優(yōu)化工作隔心。
  2. 棸兹海空間分配是廉價(jià)的,堆空間分配是昂貴的硬霍。
  3. 了解逃逸機(jī)制可以讓我們寫出更高效的代碼帜慢。
  4. 指針的使用會(huì)導(dǎo)致棧分配更不可行。
  5. 找到在低效代碼塊中提供分配控制的 api唯卖。
  6. 在調(diào)用頻繁的地方慎用 interface粱玲。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拜轨,隨后出現(xiàn)的幾起案子抽减,更是在濱河造成了極大的恐慌,老刑警劉巖橄碾,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胯甩,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡堪嫂,警方通過(guò)查閱死者的電腦和手機(jī)偎箫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)皆串,“玉大人淹办,你說(shuō)我怎么就攤上這事《窀矗” “怎么了怜森?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)谤牡。 經(jīng)常有香客問(wèn)我副硅,道長(zhǎng),這世上最難降的妖魔是什么翅萤? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任恐疲,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘培己。我一直安慰自己碳蛋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布省咨。 她就那樣靜靜地躺著肃弟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪零蓉。 梳的紋絲不亂的頭發(fā)上笤受,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音敌蜂,去河邊找鬼箩兽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛紊册,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播快耿,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼囊陡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了掀亥?” 一聲冷哼從身側(cè)響起撞反,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搪花,沒(méi)想到半個(gè)月后遏片,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡撮竿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年吮便,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幢踏。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡髓需,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出房蝉,到底是詐尸還是另有隱情僚匆,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布搭幻,位于F島的核電站咧擂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏檀蹋。R本人自食惡果不足惜松申,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧攻臀,春花似錦焕数、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至设联,卻和暖如春善已,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背离例。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工换团, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宫蛆。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓艘包,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親耀盗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子想虎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 閱讀前請(qǐng)悉知:本文是一篇翻譯文章,出于對(duì)原文的喜愛與敬畏叛拷,所以需要強(qiáng)調(diào):如果讀者英文閱讀能力好舌厨,請(qǐng)直接移步文末原文...
    wu_sphinx閱讀 2,642評(píng)論 2 4
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,321評(píng)論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • 第二部分 自動(dòng)內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,130評(píng)論 0 2
  • 工作的不順心 生活的不滿意 自己能力的否定 ………… 我就像一只丑小鴨 永遠(yuǎn)也不可能逆襲成白天鵝 仰望星空 有時(shí)我...
    一棵煩人草閱讀 288評(píng)論 3 4