一个无法内联的返回新内存的函数总是在堆上分配内存。例如,在base64包中,DecodeString
方法返回一个堆分配的字节切片:func (enc *Encoding) DecodeString(s string) ([]byte, error)
通常情况下,这个返回值的生命周期有限,很容易局限于栈上:
b, err := enc.DecodeString(s)
if err != nil { ... }
data = append(data, b...)
对于这种情况,DecodeString
方法可以通过将[]byte
作为参数编写的方式来提高效率:func (enc *Encoding) AppendDecodeString(out []byte, s string) ([]byte, error)
调用此版本无需进行堆分配。这种转换已在标准库的许多地方手动应用,从strconv中的Append*
函数到io.Reader
本身。
编译器可以自动执行此操作。
对于具体的方法,原始函数签名可以通过 Package 函数满足,该函数在堆上分配值并调用包含指针的返回值作为“out”参数的变体。当编译可以在栈上保留输出的代码时,可以将调用者修改为使用生成的函数。
例如,DecodeString
的实现是:
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
dbuf := make([]byte, enc.DecodedLen(len(s)))
n, _, err := enc.decode(dbuf, []byte(s))
return dbuf[:n], err
}
目前,逃逸分析确定dbuf
逃逸到堆中,而从未看到值是如何使用的。相反,编译器可以将此函数拆分为两个部分:一个可内联的分配函数和一个主体:
func (enc *Encoding) ΨDecodeString(s string) []byte {
return make([]byte, enc.DecodedLen(len(s)))
}
func (enc *Encoding) ΦDecodeString(s string, dbuf *[]byte) error {
n, _, err := enc.decode(*dbuf, []byte(s))
*dbuf = dbuf[:n]
return err
}
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
out := enc.ΨDecodeString(s) // inlined
err := enc.ΦDecodeString(s, &out)
return out, err
}
然后编译器可以将调用DecodeString
的地方(返回值适合放在栈上)转换为对ΨDecodeString
和ΦDecodeString
的两次调用。第一次被内联,确定不会逃逸,因此dbuf
位于栈上。
关于Ψ分配函数的一般分析以及它如何将数据依赖项传递给Φ函数可能会变得复杂。
3条答案
按热度按时间vohkndzv1#
是否有研究过额外的编译工作是否值得?不要误会我的意思——这很好——只是想知道是否已经考虑到了这一点。
此外,这是否是传递性的,例如也可以通过多次调用而不是仅仅一次来工作?
tkclm6bt2#
我还没有研究过它如何影响编译器时间,或者触发的频率。我怀疑最简单的方法来回答这两个问题就是构建一个原型。(我不打算在不久的将来这样做,只是想把这个想法写下来。)
如描述的那样,我相信它可以通过多次调用工作。Ψ函数将内联到中间函数中,并从内联体中提取一个新的Ψ函数用于该中间函数。很多都取决于在正确的时机进行内联。
zu0ti5jz3#
关于
Append*
风格的函数,请参阅提案#19366。