Golang 1.9 新特性預覽:Logging、interfaces 和 allocation

rb6843 7年前發布 | 72K 次閱讀 Go語言 Google Go/Golang開發

幾個星期前,Peter Bourgon在 golang-dev 開了一個關于標準化日志記錄的帖子。 日志很常用,因此性能很快提升。 go-kit日志包使用結構化日志,接口如下:

type Logger interface {    
   Log(keyvals ...interface{}) error
}

調用代碼:

logger.Log("transport", "HTTP", "addr", addr, "msg", "listening")

請注意,進入日志調用的所有內容都將轉換為interface{}。 這意味著它分配了不少內存。

與另一個結構化日志庫zap進行比較。 Zap為了避免內存分配和interface{}使用,導致了更丑的API:

logger.Info("Failed to fetch URL.",
  zap.String("url", url),
  zap.Int("attempt", tryNum),
  zap.Duration("backoff", sleepFor),
)

logger.Info的參數是logger.Field。 logger.Field是一種union-ish結構,包括一個string,一個int和一個interface{}。 因此,接口不必用來傳遞最常見的值。

關于logging先討論到這里。接下來討論為什么將具體值轉換為interface{}時有內存分配?

interface{}表示為一個類型指針和一個值指針。 Russ Cox寫了一篇 文章 解釋這個問題。

他的文章稍微有些過時了。但是他指出了一個優化方式:當值小于等于指針大小時,我們可以將值直接放入第二個字段。 然而,隨著并發垃圾收集的出現, 該優化被取消了 。 現在接口中的第二個字段總是一個指針。

考慮如下代碼:

fmt.Println(1)

在Go 1.4之前,這段代碼沒有內存分配,因為值1可以直接放入第二個字段。

也就是說,編譯器這樣處理:

fmt.Println({int, 1})

其中{typ,val}表示接口中的兩個字段。

從Go 1.4開始,這個代碼開始分配內存,因為1不是指針,第二個字必須包含一個指針。 所以,編譯器+運行時這樣處理:

i := new(int) // allocates!
*i = 1fmt.Println({int, i})

優化內存分配的第一點是確保當生成的接口沒有逃逸。 在這種情況下,臨時值可以放在棧上而不是堆上。 使用我們上面的示例代碼:

i := new(int) // now doesn't allocate, as long as e doesn't escape*i = 1var e interface{} = {int, i}// do things with e that don't make it escape

不幸的是,許多interface{}都會逃逸,包括在調用fmt.Println和我們上面的日志示例中使用的interface{}。

幸運的是,Go 1.9將帶來更多的優化,部分優化受logging的啟發。

第一個優化是不再將常量轉換為接口。 所以fmt.Println(1)將不再分配內存。 編譯器將值1放在只讀全局變量中,大致如下:

var i int = 1 // at the top level, marked as readonly
fmt.Println({int, &i})

因為常量是不可變的,所以每次接口轉換都會獲得相同的值,包括遞歸和并發調用。

這是由loggin直接啟發的。 在結構化日志中,許多參數是常量。 go-kit例子:

logger.Log("transport", "HTTP", "addr", addr, "msg", "listening")

此代碼將從6次內存分配減少到1次,因為其中五個參數是常量字符串。

第二個新的優化是不將bool和byte轉換為接口。 這種優化的工作原理是添加一個名為staticbytes的全局[256]字節數組,其中所有b的staticbytes [b] = b。 當編譯器想要將bool或uint8或其他單字節值放入接口時,它會使用一個指向該數組的指針代替。 那是:

var staticbytes [256]byte = {0, 1, 2, 3, 4, 5, ...}

i := uint8(1)
fmt.Println({int, &staticbytes[i]})

第三個新的優化建議仍在review,這個優化是轉換常見的零值優化。 它適用于整數,浮點數,字符串和切片。 此優化通過在運行時檢查值是否為0(或“”或nil)工作。 如果是零值,它使用指向現有的大塊零內存的指針,而不是分配一些內存并將其置零。

如果一切順利,Go 1.9應該在接口轉換期間消除相當數量的內存分配。但它不會消除所有的內存分配,這使得仍然存在以上討論的性能問題。

選擇API需要考慮性能。這也是為什么io.Reader要求/允許調用者使用自己的緩沖區。

性能在很大程度上是設計實現的結果。我們已經看到在這篇文章中,接口的實現細節可以大大改善內存分配。

很多設計和實現決策取決于人們寫什么樣的代碼。編譯器和運行時的作者想要優化實際的,通用的代碼。例如,在Go 1.4中,決定將接口值保持在兩個字而不是將它們改為三個,這使得調用fmt.Println(1)分配額外內存。

由于人們編寫的代碼通常被他們使用的API塑造,所以這是一種有機的反饋循環,這也是有趣的,有挑戰性的管理。

如果你設計一個API,并擔心性能問題,不要忘記現有的編譯器和運行時實際做了什么或者他們可以做什么。編寫當下的代碼,但設計未來的API。

 

來自:http://mp.weixin.qq.com/s/F8tmZnTR_uspqY2GP0o3Tw

 

 本文由用戶 rb6843 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!