Golang - golang 内存配置文件如何计算分配/操作?

发布于 2025-01-16 09:28:57 字数 2293 浏览 0 评论 0原文

我正在编写一个自定义 JSON marshal 函数,并将其与内置 json.Marshal 方法进行比较。

我的理解是,当 bytes.Buffer 达到其容量时,它需要将其大小加倍,这会花费 1 次分配。

然而,基准测试结果似乎表明 json.Marshal 正在以一种不需要在增加底层缓冲区时进行分配的方式来执行此操作,而我的实现似乎每次缓冲区加倍时都会花费额外的分配。

为什么MarshalCustom(下面的代码)需要分配比json.Marshal更多的空间?

$ go test -benchmem -run=^$ -bench ^BenchmarkMarshalText$ test
BenchmarkMarshalText/Marshal_JSON-10               79623             13545 ns/op            3123 B/op          2 allocs/op
BenchmarkMarshalText/Marshal_Custom-10        142296              8378 ns/op           12464 B/op          8 allocs/op
PASS
ok      test    2.356s

完整代码。

type fakeStruct struct {
    Names []string `json:"names"`
}

var ResultBytes []byte

func BenchmarkMarshalText(b *testing.B) {
    names := randomNames(1000)

    b.Run("Marshal JSON", func(b *testing.B) {
        fs := fakeStruct{
            Names: names,
        }

        b.ReportAllocs()
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            ResultBytes, _ = json.Marshal(fs)
        }
    })

    b.Run("Marshal Custom", func(b *testing.B) {
        fs := fakeStruct{
            Names: names,
        }

        b.ReportAllocs()
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            ResultBytes = MarshalCustom(fs)
        }
    })
}

func MarshalCustom(fs fakeStruct) []byte {
    var b bytes.Buffer

    b.WriteByte('{')

    // Names
    b.WriteString(`,"names":[`)
    for i := 0; i < len(fs.Names); i++ {
        if i > 0 {
            b.WriteByte(',')
        }
        b.WriteByte('"')
        b.WriteString(fs.Names[i])
        b.WriteByte('"')
    }
    b.WriteByte(']')

    b.WriteByte('}')

    buf := append([]byte(nil), b.Bytes()...)
    return buf
}

func randomNames(num int) []string {
    const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    const maxLen = 5

    rand.Seed(time.Now().UnixNano())
    res := make([]string, rand.Intn(num))
    for i := range res {
        l := rand.Intn(maxLen) + 1 // cannot be empty
        s := make([]byte, l)
        for j := range s {
            s[j] = letters[rand.Intn(len(letters))]
        }
        res[i] = string(s)
    }

    return res
}

I'm writing a custom JSON marshal function and comparing it to the built-in json.Marshal method.

My understanding is that when bytes.Buffer reaches its capacity, it needs to double its size and this costs 1 allocation.

However, benchmark result seems to indicate json.Marshal is doing this in a way where it does NOT need to allocate whenever it grows the underlying buffer, whereas my implementation seems to cost an extra allocation everytime the buffer doubles.

Why would MarshalCustom (code below) need to allocate more than json.Marshal?

$ go test -benchmem -run=^$ -bench ^BenchmarkMarshalText$ test
BenchmarkMarshalText/Marshal_JSON-10               79623             13545 ns/op            3123 B/op          2 allocs/op
BenchmarkMarshalText/Marshal_Custom-10        142296              8378 ns/op           12464 B/op          8 allocs/op
PASS
ok      test    2.356s

Full code.

type fakeStruct struct {
    Names []string `json:"names"`
}

var ResultBytes []byte

func BenchmarkMarshalText(b *testing.B) {
    names := randomNames(1000)

    b.Run("Marshal JSON", func(b *testing.B) {
        fs := fakeStruct{
            Names: names,
        }

        b.ReportAllocs()
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            ResultBytes, _ = json.Marshal(fs)
        }
    })

    b.Run("Marshal Custom", func(b *testing.B) {
        fs := fakeStruct{
            Names: names,
        }

        b.ReportAllocs()
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            ResultBytes = MarshalCustom(fs)
        }
    })
}

func MarshalCustom(fs fakeStruct) []byte {
    var b bytes.Buffer

    b.WriteByte('{')

    // Names
    b.WriteString(`,"names":[`)
    for i := 0; i < len(fs.Names); i++ {
        if i > 0 {
            b.WriteByte(',')
        }
        b.WriteByte('"')
        b.WriteString(fs.Names[i])
        b.WriteByte('"')
    }
    b.WriteByte(']')

    b.WriteByte('}')

    buf := append([]byte(nil), b.Bytes()...)
    return buf
}

func randomNames(num int) []string {
    const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    const maxLen = 5

    rand.Seed(time.Now().UnixNano())
    res := make([]string, rand.Intn(num))
    for i := range res {
        l := rand.Intn(maxLen) + 1 // cannot be empty
        s := make([]byte, l)
        for j := range s {
            s[j] = letters[rand.Intn(len(letters))]
        }
        res[i] = string(s)
    }

    return res
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

机场等船 2025-01-23 09:28:57

@oakad 是正确的。如果我在基准测试的每次迭代中强制运行 GC,则分配/操作会更接近甚至相同。

for i := 0; i < b.N; i++ {
    // marshal here

    runtime.GC()
}

@oakad is correct. If I force a GC run in every iteration of the benchmark, the allocs/op is much closer or even the same.

for i := 0; i < b.N; i++ {
    // marshal here

    runtime.GC()
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文