go cmd/compile: 避免对零长度类型的runtime.makeslice调用?

8cdiaqws  于 4个月前  发布在  Go
关注(0)|答案(8)|浏览(56)

Go版本

Go 1.22

你做了什么?

return make([]struct{}, 123) 编译成调用 runtime.makeslice :

LEAQ    type:struct {}(SB), AX
        MOVL    $123, BX
        MOVQ    BX, CX
        PCDATA  $1, $0
        NOP
        CALL    runtime.makeslice(SB)
        MOVL    $123, BX
        MOVQ    BX, CX
        ADDQ    $24, SP
        POPQ    BP
        RET

我们在调用 https://pkg.go.dev/tailscale.com/types/views#Slice.LenIter (早于Go 1.22的range-over-int)时看到了这个(虽然很小,但非零):

// LenIter returns a slice the same length as the v.Len().
// The caller can then range over it to get the valid indexes.
// It does not allocate.
func (v Slice[T]) LenIter() []struct{} { return make([]struct{}, len(v.ж)) }

我意识到当类型宽度为零时,编译器可以省略这些 makeslice 调用(因为没有实际分配)。
当切片长度为零时,编译器已经这样做了:

// Recognise make([]T, 0) and replace it with a pointer to the zerobase
(StaticLECall {callAux} _ (Const(64|32) [0]) (Const(64|32) [0]) mem)
        && isSameCall(callAux, "runtime.makeslice")
        => (MakeResult (Addr <v.Type.FieldType(0)> {ir.Syms.Zerobase} (SB)) mem)

/cc @golang/compiler @twitchyliquid64@maisem

dced5bon

dced5bon1#

我为零大小的切片添加了这个精确的优化,但我没有为零大小的类型添加它,因为我无法弄清楚如何在SSA传递中恢复编译类型指针的编译器*types.Type,而且这不是我想优化的内容,所以我从未调查过它。
经过5分钟的搜索,当时我们失去了编译类型指针在genssa之后的类型信息,与前端对象进行交谈并不简单。

y4ekin9u

y4ekin9u2#

我们可以在genssa中进行这种优化,因为我们拥有所有所需的类型信息,但这很棘手,因为我们不能依赖prove来正确证明索引是否在范围内。因为这会导致恐慌:

var a = -1
_ = make([]struct{}, a)

genssa期间,我们可以检查参数是否为字面量或常量,但其他任何事情都非常困难。

for i := range make([]struct{}, 10) { // this would be optimized
 for j := range make([]struct{}, i) { /* ... */ } // here optimizing it in genssa would fail while SSA can use prove to handle this
}

回到你的例子:

func (v Slice[T]) LenIter() []struct{} { return make([]struct{}, len(v.ж)) }

这不会在genssa中被优化,就像之前一样,在prove之前,我们不知道len(v.ж)可能是什么。实际上没关系,len不能返回无效长度(除了* unsafe代码有误之外,但如果不正确使用unsafe导致错误,那也没关系)。
我甚至不确定prove是否能正确处理这个问题,因为它在一些边缘情况下与lenmakeslice失败。
我认为这应该在一个第三方的linter中处理,该linter找到所有的make([]zeroSizedType, n)调用范围,并恳请更新代码以使用int的范围遍历,git grep -F "make([]struct{}, "应该已经让你走了95%的路程。

dy1byipe

dy1byipe3#

关于SSA规则,我想到了这个:

(StaticLECall {callAux} (Addr {t} _) _ _ mem)
	&& isSameCall(callAux, "runtime.makeslice")
	&& t.(*obj.LSym).String() == "type:struct {}"
	=> (MakeResult (Addr <v.Type.FieldType(0)> {ir.Syms.Zerobase} (SB)) mem)

但我必须承认,我对这方面缺乏适当的理解,如果在优化过程中的早期阶段可能还有更好的方法来实现相同的效果。

s5a0g9ez

s5a0g9ez4#

@mauri870 这个不应该在长度和容量超出范围时恐慌。
检查字符串lsym是一个有趣的想法,但它非常花哨。
对于像这样的东西不起作用:

type empty struct{}

// ...
_ = make([]empty, n)

尽管那并不那么糟糕。

6jjcrrmo

6jjcrrmo5#

我同意,我的方法非常天真。可能有很多问题,如果它能以某种方式作为起点,我会很高兴。感谢关注这个问题。

0h4hbjxa

0h4hbjxa6#

我将在internal/walk中制作一个补丁,该补丁将处理字面量、常量和len / cap调用作为make的参数,如果编译器团队认为这是合适的(cc @randall77)。但是,我认为消费者应该重写他们的代码,使用int范围进行操作。

kupeojn6

kupeojn67#

是的,没问题。
我们可以为所有的 make 调用生成负测试,针对 make 参数进行测试,如果满足条件则调用 runtime.panicmakeslicelen。不确定这样做是否值得。

bxgwgixi

bxgwgixi8#

我认为我们应该内联热代码路径而不是冷路径。

相关问题