Go在字符串上执行类型Assert来进行堆分配

vcirk6k6  于 2023-05-04  发布在  Go
关注(0)|答案(2)|浏览(153)
func Benchmark_maybeString(b *testing.B) {
    str := "foobar"

    b.ReportAllocs()
    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        _ = maybeString(str)
    }
}

func maybeString(val any) string {
    switch val.(type) {
    case string:
        return val.(string)
    default:
        return ""
    }
}
Benchmark_maybeString        0.3118 ns/op          0 B/op          0 allocs/op

然而,将default的情况(甚至没有在基准测试中运行)更改为更复杂的情况会突然使函数分配:

func maybeString(val any) string {
    switch val.(type) {
    case string:
        return val.(string)
    default:
        return fmt.Sprintf("not a string: %T", val)
    }
}
Benchmark_maybeString           18.65 ns/op       16 B/op          1 allocs/op

为什么?如果是因为字符串正在逃逸到堆中,这是否意味着Go可以在堆栈上分配字符串?下面这个版本怎么样,我假设它从一开始就应该在堆中,但仍然分配:

data := map[string]string{
    "foobar": time.Now().Format(time.RFC3339),
}
maybeString(data["foobar"])

到目前为止

1.将str := "foobar"更改为const str = "foobar"var str any = "foobar"会在两个版本中产生0个分配。为什么?Go语言中的字符串不总是平均分配的吗?
1.分配版本在执行类型AssertCMPL之前调用runtime.convTstring()。但是go:string."foobar"(SB)语法是什么意思?

kognpnkq

kognpnkq1#

它归结为这样一个事实,即如果编译时的转义分析确定它没有转义当前goroutine的堆栈,Go可以在堆栈上分配东西。引入将字符串传递给fmt.Sprintf()参数的代码,使其决定字符串是否逸出堆栈。
字符串已经在堆栈外的版本不分配。例如:

const str = "foobar"
var str any = "foobar"
str := map[string]any{
    "foobar": fmt.Sprintf("%v", time.Now().Format(time.RFC3339)),
}["foobar"]
vsmadaxz

vsmadaxz2#

分配是由于fmt.Sprintf接受接口,所以编译器将string转换为接口来调用函数(你看到的runtime.convTstring),但显然编译器在这里是愚蠢的。
1、该分配仅在默认情况下需要,不应在类型切换之前放置,将该分配推到需要它的地方肯定会删除该分配。
2、另一种选择是将分配提升到循环外,这也有助于消除循环内的分配。不过,这需要编译器知道iface结构中的指针指向一个常量。
3,类型开关应该是折叠的,我相信this change会解决这个问题。
您的问题:
str := "foobar"更改为const str = "foobar"允许编译器消除分配,因为str位于堆栈之外并且是常量,因此分配可以在编译时完成。并且更改为var str any = "foobar"提升了循环外部的分配,因此内部没有分配。
2,go:string."foobar"(SB)是包含char数组“foobar”的缓冲区的地址,这行代码将该地址加载到rax中,下面的代码将长度加载到rbx中。这是因为runtime.convTstring需要一个string,它由一个指针和一个int组成。Go调用约定将前两个原始参数分别放在raxrbx中。

相关问题