Golang字符串实际大小的基准测试 - 结果有误?

Golang字符串实际大小的基准测试 - 结果有误? 以下是我的基准测试:

func get() string {
    return string([]byte{0x41, 0x42, 0x43})
}    

var g string

func BenchmarkString(b *testing.B) {
    for i := 0; i < b.N; i++ {
        g = get()
    }

    fmt.Println("unsafe.Sizeof(g)", unsafe.Sizeof(g), b.N, g)
}

这个测试正确吗?以下是输出结果: unsafe.Sizeof(g) 16 100000000 ABC 100000000 13.7 ns/op 3 B/op 1 allocs/op

字符串的大小似乎是16字节,而基准测试显示每次操作只有3字节,这看起来不太合理。该如何解释?

我原本预期字符串的实际大小应该是: len(str) + unsafe.Sizeof(str) * 4

也就是说len(str)是开销,然后每个符文(len(str) = 符文数量)再占用4个字节,因为符文的大小与int32相同。如果我的理解有误,请指正。


更多关于Golang字符串实际大小的基准测试 - 结果有误?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

这是一篇关于内存分配器的好文章,它以易读的方式解释了许多内容:https://povilasv.me/go-memory-management/

更多关于Golang字符串实际大小的基准测试 - 结果有误?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你还在进行从字节切片到字符串的转换,所以无论如何都需要进行内存分配。对于一个四字节的字符串分配四字节内存看起来是符合预期的。

calmh:

三个字节(字符串数据并非"宽"字符,每个代码点占四个字节,它只是普通数据)。由于你将其传递给 fmt.Println,它会被分配到堆上,因此编译器无法保证它不会逃逸出当前函数的生命周期范围。

我已经注释掉了打印行,所以没有输出。但它仍然分配了 3 B/op。如果我将字符串中的符文数量增加到 4,它会分配 4 B/op,依此类推。

看起来这里的情况比表面更复杂。

字符串的Sizeof为16字节,因为字符串是一个包含指针(8字节)和长度(8字节)的结构体。这个结构体本身没有分配内存,因为它位于栈上。分配的是指向的数据,即三个字节(字符串数据不是"宽"字符,每个代码点四字节,它只是数据)。由于你将其传递给fmt.Println,它会在堆上分配内存,因为编译器无法保证它不会超出当前函数调用的生命周期。

你进行了大量的微基准测试并对结果提出疑问。你的探索是否有更宏观的意义?如果现在你应该得出一个结论,那就是对于这类小细节,一切都取决于具体情况。是否有内存分配取决于数据的使用方式等等。

您的基准测试结果实际上是正确的,但您对Go字符串内存结构的理解有误。让我解释一下:

Go字符串的内部结构

Go字符串在内存中是一个结构体,包含两个字段:

type stringStruct struct {
    str unsafe.Pointer  // 指向底层字节数组的指针
    len int             // 字符串长度
}

在64位系统上:

  • unsafe.Pointer 占用 8 字节
  • int 占用 8 字节
  • 总计:16 字节

这就是为什么 unsafe.Sizeof(g) 返回 16 - 它测量的是字符串头结构的大小,而不是底层数据的大小。

基准测试结果分析

您的基准测试结果:

  • 13.7 ns/op - 每次操作耗时
  • 3 B/op - 每次操作分配3字节(字符串"ABC"的实际内容)
  • 1 allocs/op - 每次操作1次内存分配

这是正确的,因为:

  1. 字符串"ABC"确实占用3个字节(每个ASCII字符1字节)
  2. 字符串头结构(16字节)在栈上分配,不计入堆分配
  3. 只有字符串内容(3字节)在堆上分配

验证示例

func TestStringMemory(t *testing.T) {
    s := "ABC"
    
    // 字符串头大小
    fmt.Printf("String header size: %d bytes\n", unsafe.Sizeof(s))
    
    // 字符串内容大小
    fmt.Printf("String content size: %d bytes\n", len(s))
    
    // 总内存占用(近似)
    total := unsafe.Sizeof(s) + uintptr(len(s))
    fmt.Printf("Total approximate memory: %d bytes\n", total)
}

输出:

String header size: 16 bytes
String content size: 3 bytes  
Total approximate memory: 19 bytes

正确的理解

  • unsafe.Sizeof() 返回类型本身的大小,不包含指针指向的数据
  • 基准测试的 B/op 只计算堆分配,不包含栈分配
  • 字符串内容以UTF-8编码存储,ASCII字符每个占用1字节

您的基准测试结果是准确的,反映了Go字符串内存管理的真实行为。

回到顶部