如何给Golang的gc减负

这篇文章想聊聊如何给gc减负的问题,也即我们在写业务的时候,有时候需要考虑下gc老人家的感受,但又不能丧失代码的可读性,有些情况下代码需不需要优化,优化后能取得多大的性能提升,其中的平衡需要把握。过早的优化是万恶的开始

  1. 字符串拼接,demo代码:
package main

import (
    "bytes"
)

func f1(l int) {
    var s, s1 string = ``, `hello world`
    for i := 0; i < l; i++ {
        s = s + s1
    }
}
func f2(l int) {
    buf := bytes.NewBuffer([]byte{})
    var s1 string = `hello world`
    for i := 0; i < l; i++ {
        buf.WriteString(s1)
    }
}
func f3(l int) {
    var s []string
    var s1 string = `hello world`
    for i := 0; i < l; i++ {
        s = append(s, s1)
    }
    strings.Join(s, ``)
}

测试代码:

package main

import (
    "testing"
)
func Benchmark_F1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        f1(100000)
    }
}

func Benchmark_F2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        f2(100000)
    }
}

func Benchmark_F3(b *testing.B) {
    for i := 0; i < b.N; i++ {
        f3(100000)
    }
}

go test -bench=".*" -test.benchmem -count=1 输出的结果是:

Benchmark_F1-4             1    9334113815 ns/op    55401130720 B/op      100056 allocs/op
Benchmark_F2-4          1000       2318146 ns/op     2891600 B/op         18 allocs/op
Benchmark_F3-4           100      14660804 ns/op    11459184 B/op         32 allocs/op
PASS
ok      _/Users/taomin/pprof/string 13.394s

从bench的结果来看,10W个字符串的拼接,+的最差,bytes.NewBuffe最好。内存和时间上的差距大概在3个数量级左右。这种字符串拼接的优化,除非你的场景确实存在大量字符串拼接,不然不要使用什么bytes或者strings.join,直接+拼接起来就好了。

  1. 临时对象池
    对象池将对象存放到池里,通过复用之前的对象,从而减少分配对象的个数。每次GC,runtime会调用poolCleanup函数来将Pool清空,这样原本Pool中储存的对象会被GC全部回收。这是Pool的一个特性,这个特性会导致:有状态的对象不能储存在Pool中,Pool不能用作连接池;官方文档说明如下:
A Pool is a set of temporary objects that may be individually saved and retrieved.

Any item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.

A Pool is safe for use by multiple goroutines simultaneously.

Pool's purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector. That is, it makes it easy to build efficient, thread-safe free lists. However, it is not suitable for all free lists.

An appropriate use of a Pool is to manage a group of temporary items silently shared among and potentially reused by concurrent independent clients of a package. Pool provides a way to amortize allocation overhead across many clients.

An example of good use of a Pool is in the fmt package, which maintains a dynamically-sized store of temporary output buffers. The store scales under load (when many goroutines are actively printing) and shrinks when quiescent.

On the other hand, a free list maintained as part of a short-lived object is not a suitable use for a Pool, since the overhead does not amortize well in that scenario. It is more efficient to have such objects implement their own free list.

A Pool must not be copied after first use.

type Pool struct {

        // New optionally specifies a function to generate
        // a value when Get would otherwise return nil.
        // It may not be changed concurrently with calls to Get.
        New func() interface{}
        // contains filtered or unexported fields
}

在网上有哥们写的Pool的例子,觉得很能说明问题,这里用他的代码作为例子来说明:

package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
)

// 并发过程使用了多少次 []byte
var mu sync.Mutex
var holder map[string]bool = make(map[string]bool)

// 临时对象池
var p = sync.Pool{
    New: func() interface{} {
        buffer := make([]byte, 1024)
        return &buffer
    },
}

func readContent(wg *sync.WaitGroup) {
    defer wg.Done()
    resp, err := http.Get("http://my.oschina.net/xinxingegeya/home")
    if err != nil {
        fmt.Println(err)
    }
    defer resp.Body.Close()
    byteSlice := p.Get().(*[]byte) //类型断言
    key := fmt.Sprintf("%p", byteSlice)
    mu.Lock()
    _, ok := holder[key]
    if !ok {
        holder[key] = true
    }
    mu.Unlock()
    _, err = io.ReadFull(resp.Body, *byteSlice)
    if err != nil {
        fmt.Println(err)
    }
    p.Put(byteSlice)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go readContent(&wg)
    }
    wg.Wait()
    for key, val := range holder {
        fmt.Println("Key:", key, "Value:", val)
    }
}

Pool要慎用哦,因为它的脾气不太好。

  1. string转字节数组
    string类型的变量里面的值会被复制到字节数组中,雨虹学堂上之前一种优化思路,通过unsafe包的指针操作,直接对string的地址进行操作,这样避免了内存复制操作,但个人感觉这个优化有点过了,这会让代码失去可读性,例如:
s := "hello world!"
b := []byte(s)

被改成了:

 func str2bytes(s string) []byte {
    x := (*[2]uintptr)(unsafe.Pointer(&s))
    h := [3]uintptr{x[0], x[1], x[1]}
    return *(*[]byte)(unsafe.Pointer(&h))
}

上面的代码如果不好好研究下,完全看不懂在搞神马。

以上方案都在围绕一个中心点:减少对象的数量。是的,没错,对象数量是影响gc的关键数量。因为gc首先要去找到这些对象,判断对象当前是否可用,不用的状态就标记上,后续等待回收。gc在1.5之后就采用三色标记法去做垃圾回收:

GC中用三种颜色标记不同的对象:
(1)黑色:本身强引用,并已处理对象中的子引用
(2)灰色:本身强引用,还没处理对象中的子引用
(3)白色:不可达对象

从根出发,根包括:全局指针和goroutine栈上的指针。mark有两个过程:从root开始遍历,标记那些可达的节点为灰色,然后开始遍历灰色队列,将那些从灰色节点可达的节点也加入灰色,同时将自己标记为黑色,重复这个过程知道遍历所有灰色节点,剩下的白色节点就是我们清理掉的节点。mark过程和用户程序是并行的,这样在mark过程完成之后还得re-scan一次,re-scan过程是需要stw。

这里就不多说gc回收原理了,给gc减负应该是golang工程师的必备伎俩。end~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供...
    简欲明心阅读 89,344评论 17 311
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,515评论 3 83
  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,940评论 2 31
  • 脑子已经图尽了,一个词都不知道用什么图表达了,画得如何先不说,关键是不知道画什么图表示,没有好办法,听老师的话刻意练习!
    平平淡淡1020阅读 309评论 2 0