追加切片为什么性能低下?原因何在?

7

我目前正在使用GoLang创建一个游戏。我正在测量FPS。我注意到在使用for循环将内容附加到切片时,会损失约7个FPS,例如:

vertexInfo := Opengl.OpenGLVertexInfo{}

for i := 0; i < 4; i = i + 1 {
    vertexInfo.Translations = append(vertexInfo.Translations, float32(s.x), float32(s.y), 0)
    vertexInfo.Rotations = append(vertexInfo.Rotations, 0, 0, 1, s.rot)
    vertexInfo.Scales = append(vertexInfo.Scales, s.xS, s.yS, 0)
    vertexInfo.Colors = append(vertexInfo.Colors, s.r, s.g, s.b, s.a)

}

我对每个精灵、每个绘制都执行此操作。问题是,为什么我只是循环几次并将相同的内容附加到这些切片中,却会出现如此巨大的性能损失?有没有更有效的方法来做到这一点?并不像我添加了过多的数据。每个切片包含约16个元素,如上所示(4 x 4)。
当我简单地将所有16个元素放在一个[]float32{1..16}中时,fps提高了约4。
更新:我对每个附加进行了基准测试,似乎每个附加需要1 fps才能执行。考虑到这些数据非常静态,这似乎是很多...我只需要4次迭代...
更新:添加了github repo https://github.com/Triangle345/GT

1
增加更多的切片意味着需要进行更多的分配。如果您存储的数据格式实际上是静态的,那么可以将它们声明为静态的,这样就不需要进行簿记:type Vec3 struct { X, Y, Z float32 }; type Vec4 struct { X, Y, Z, W float32 }; type Color struct { R, G, B, A byte } 然后您有 type VertexInfo { Translations, Scales [4]Vec3; Rotations [4]Vec4; Colors [4]Color }(或者如果都一样,可以删除内部数组并使用 [4]VertexInfo)。 - twotwotwo
这是一个好主意,我回家后会尝试一下。 - efel
考虑到这一点...不能在结构体中以这种方式实现,因为它们必须以“数组”的形式进入OpenGL CGO。 - efel
啊,糟糕了。这可能主要是由于垃圾收集(GC)导致的开销。调整内存布局对我来说是有意义的,但使用sync.Pool进行回收,或者如果您有多余的RAM,则可以使用runtme.SetGCPercent来更少频繁地触发GC以换取更多的RAM也会有所帮助。 - twotwotwo
2
@efel,对于帧之间的切片你是怎么处理的?如果你将它们设置为“nil”,那么下一次添加就会导致重新分配内存。你可以使用a=a[:0]将一个切片设为零长度和非零容量——如果下一帧中切片长度相同,则不会重新分配。 - chill
2个回答

6
内置的append()函数在目标切片的容量小于追加后切片长度时需要创建一个新的支持数组。这还需要将目标现有元素复制到新分配的数组中,因此会有很多开销。
由于您使用了切片字面值来创建Opengl.OpenGLVertexInfo值,因此您附加的切片很可能是空切片。尽管append()会考虑未来并分配比追加指定元素所需更大的数组,但在您的情况下,可能需要多次重新分配才能完成四次迭代。
如果您像这样创建和初始化vertexInfo,则可以避免重新分配:
vertexInfo := Opengl.OpenGLVertexInfo{
    Translations: []float32{float32(s.x), float32(s.y), 0, float32(s.x), float32(s.y), 0, float32(s.x), float32(s.y), 0, float32(s.x), float32(s.y), 0},
    Rotations:    []float64{0, 0, 1, s.rot, 0, 0, 1, s.rot, 0, 0, 1, s.rot, 0, 0, 1, s.rot},
    Scales:       []float64{s.xS, s.yS, 0, s.xS, s.yS, 0, s.xS, s.yS, 0, s.xS, s.yS, 0},
    Colors:       []float64{s.r, s.g, s.b, s.a, s.r, s.g, s.b, s.a, s.r, s.g, s.b, s.a, s.r, s.g, s.b, s.a},
}

注意这个结构体字面量会自动处理切片所需的数组重新分配问题。但是如果你在代码中的其他地方(我们看不到的地方)向这些切片添加了更多的元素,它们可能会引起重新分配。如果是这种情况,你应该创建容量更大的切片以覆盖“未来”的分配(例如make([]float64, 16, 32))。


我尝试将所有4个迭代放在里面,像这样做确实增加了大约4个fps。然而,我仍然失去了大约4个fps,因为按照你提到的方式创建了这个切片,为什么?? - efel
@efel,从你的代码中看不出什么问题。也许在代码的其他地方,你会添加更多需要重新分配内存的操作。如果是这种情况,你可以分配一个更大的切片(例如使用 make() 函数)。 - icza
1
请注意,在 make([]float64, 16, 32) 中,16 是切片长度,而32是数组(分配)大小。因此,它可以附加到多达32个元素而无需重新分配空间。 - ben schwartz

4

一个空的切片是没有元素的。如果要添加元素,必须先分配内存。当你进行更多的添加操作时,还需要分配更多的内存。

为了加快速度,可以使用固定大小的数组或使用 make 函数创建具有正确长度的切片,或在声明时初始化切片。


使用项目进行初始化确实节省了一些帧率,但我仍然失去了大约4个帧率。这没有意义,仅进行3-4次迭代的成本如此之高吗? - efel
@efel 在使用 make 时,您是否指定了长度和/或容量?如果您每帧都这样做,那么最好重用单个切片,而不是每帧创建一个新的切片。根据您如何创建和使用切片,Go 可能需要在堆上分配内存并创建垃圾(这两种情况会以不同的方式减慢您的速度)。 - szablica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接