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)
语法是什么意思?
2条答案
按热度按时间kognpnkq1#
它归结为这样一个事实,即如果编译时的转义分析确定它没有转义当前goroutine的堆栈,Go可以在堆栈上分配东西。引入将字符串传递给
fmt.Sprintf()
参数的代码,使其决定字符串是否逸出堆栈。字符串已经在堆栈外的版本不分配。例如:
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调用约定将前两个原始参数分别放在rax
和rbx
中。