背景
由于GC形状要求,将标量(非指针)值存储到任何接口类型的变量中,会强制该值以间接方式存储,通常分配在堆上。在某些应用程序中,这可能导致许多意外的分配和分配器和垃圾收集器的极大负载,导致显著的性能下降。在最坏的情况下,这可能是对未广泛优化的服务的DOS向量,尤其是那些使用像image或encoding/json这样的包的服务。
接口值具体表示为两种不同的类型:具有非空方法集的接口runtime.iface
和interface{}
的接口。
go/src/runtime/runtime2.go
第202行至第210行 1129a60
| | typeifacestruct { |
| | tabitab |
| | data unsafe.Pointer |
| | } |
| | |
| | typeefacestruct { |
| | _type_type |
| | data unsafe.Pointer |
| | } |
建议
为了减少在使用接口中的小类型时程序中的分配次数,我建议添加一个GOEXPERIMENT
的值,例如GOEXPERIMENT=largeiface
,将runtime.iface
和runtime.eface
更改为以下内容:
type iface2 struct {
tab *itab
data unsafe.Pointer
sdat [2]uintptr // scalar data
}
type eface2 struct {
_type *_type
data unsafe.Pointer
sdat [2]uintptr
}
然后,只要某种类型包含不超过一个指针和两个标量字(无论顺序如何),在任何数量的字段加上填充中,该类型的值都可以复制到iface2或eface2值中,而无需在堆上分配。如果某种类型包含超过一个指针或超过两个标量字,则只有指向该类型的值的指针才会在分配给接口类型的变量时存储。这扩展了当前的行为,即与零而不是两个标量字相同。
请注意,这些类型与现有的类型名称不同。GOEXPERIMENT启用时,运行时中不存在名称为iface
和eface
的类型,而禁用时不存在名称为iface2
和eface2
的类型。这通过确保始终使用实验设置的正确名称来提高可维护性。
示例
有了这个建议,以下类型将成为所有受支持目标上的直接可分配给接口值:
int
int64
string
[]T // for any type T
struct {
b [8]byte
p *T
}
// color.(N)RGBA64
struct {
R, G, B, A uint16
}
// assuming unsafe.Alignof(new(T)) == unsafe.Sizeof(uintptr(0))
// and unsafe.Sizeof(thistype{}) % unsafe.Alignof(new(T)) == 0
struct {
a uint8
p *T
b uint8
}
以下类型仍将仅间接地可分配给接口值:
interface{} // too many pointers
[2]*T // too many pointers
// reflect.SliceHeader; too many scalar words
struct {
Data uintptr
Len int
Cap int
}
// too many scalar words with padding,
// assuming the compiler never reorders struct fields
struct {
a uint8
u uintptr
b uint8
}
赋值组合
本节假定编译器永远不会重新排序结构字段。
有两种实现字段之间传输的方法,以支持任意顺序的字段,以便支持iface2(eface2)值中的字段。第一种方法是在runtime._type
中添加一个新的uint8
字段,其中包含三个二位字段,描述iface2中的每个后续数据字段是否传输到动态值的第一个、第二个或第三个字中,或者根本不传输。对于convT2E/I和Assert来说,这非常容易实现。
第二种方法是枚举9个唯一的赋值组合,并将适当的组合存储在一个新字段中,或者在tflag
字段的未使用位中存储。这要么不使用额外的存储空间,要么留出空间用于存储有关排列的其他数据,例如每个标量数据字段是否为浮点值,以便更有效地与新的寄存器ABI交互。
请注意,唯一赋值组合是(无赋值,即大小为零的类型),P,S,PS,SP,SS,PSS,SPS,SSP。为每个S添加考虑因素会产生23种替代方案。或者,无赋值情况可以通过存储对runtime.zerobase
的引用来转换为P情况,从而将组合减少到8种。
我建议将此添加为GOEXPERIMENT,而不是彻底的更改,原因有两点。首先,将每个接口值的大小加倍可能会严重惩罚一些Go程序,尤其是那些在云函数等环境中运行的程序,其中可用内存可能非常小。其次,有一些several Go repositories(并非详尽无遗的搜索;值得注意的是,有些仓库在供应商目录中出现多次,但在此搜索中未出现),它们依赖于当前接口值的布局,使用不安全或汇编语言。GOEXPERIMENT提供了机制——构建标签和汇编定义——使此类代码能够更新以与实验一起启用和禁用。
代价是,作为GOEXPERIMENT,这将需要大量的并行维护。实现实验的cmd/compile和其他编译器需要为接口分配和转换生成不同的代码,此外还需要检测符合条件的类型并计算它们的赋值组合。运行时、反射和内部/reflectlite将需要大量的重复代码路径来处理不同的布局。sync/atomic包中的atomic.Value的实现需要被复制,sync.Pool也需要一些轻微的复制。如果某些第三方包打算支持实验,它们可能需要复制代码。
作为实验,目标应该是收集关于空间-性能权衡的数据。我们应该找到哪些程序从减少分配和垃圾回收中受益最大,以及它们受益的程度有多大。此外,我们还应该测量许多程序之间的内存使用变化,并找出哪些程序经历了不可接受的增加。如果实验揭示出“CPU使用率降低,由于更少的时间用于小对象而内存使用量保持在几个百分点以内”的结果,那么它就可以推广到默认情况。
相关问题:
此将(完全或大部分)解决的问题:
- database/sql: provide optional way to mitigate convT2E allocations #6918 - 由于频繁地将[]byte分配给database/sql中的eface导致许多分配,导致性能缓慢。该问题提议数据库驱动程序可以使用新的API来避免分配。通过此实验,这些分配不再分配。
- runtime: remove unnecessary allocations in convT2E #8892 - 通过在数据指针中使用位掩码,在64位平台上避免为分配给iface/eface的4字节标量分配内存。此实验涵盖了这种情况。
- image: optimize Image.At().RGBA() #15759 - 从图像中检索像素数据会导致O(m×n)的分配,这些分配立即被丢弃。此实验允许所有标准库中的concrete color类型分配给color.Color而不分配。
- cmd/compile: stack allocate string and slice headers when passed through non-escaping interfaces #23676 - 通过更好的转义分析避免为分配给iface/eface的字符串和切片头分配内存。此实验允许这些头分配而不分配,无论值是否转义。
- cmd/compile, runtime: pack info into low bits of interface type pointers #26680 - 将iface/eface值的itab/type指针的低位解释为描述值属性的位字段。此实验涵盖了每种潜在用途描述的情况。
- reflect: map iteration does unnecessary excessive allocation for non-pointer types #32424 - 使用reflect遍历具有标量键或元素类型的Map会导致每个Map值被分配。当Map中的键或元素类型较小时(包括本问题中提到的所有类型),此实验将阻止这些分配。我忘了reflect会使用Value,而不是interfaces。 🙂
- proposal: encoding/json: garbage-free reading of tokens #40128 - encoding/json解码器“是一个垃圾工厂”,因为它将令牌分配给json.Token接口。目前分配给json.Token的所有类型都将通过此实验避免分配。
- image, image/draw: add interfaces for using RGBA64 directly #44808 - 类似于上面的image: optimize Image.At().RGBA() #15759,但提议向image和image/draw添加接口以显式避免接口分配。此实验将大大消除对这些接口的需求。
一些相关的已关闭问题:
- cmd/gc: make interface updates atomic wrt garbage collector #8405 - 最初与间接为接口分配值有关的问题。值得注意的是,这包括讨论向iface和eface添加一个标量字段的内容。
- runtime: don't allocate when putting a bool into an interface #17725 - 为小型整数分配到接口的无分配性赋值。此实验大大扩展了这一更改的优势。
- cmd/compile: don't allocate when putting constant strings in an interface #18704 - 防止为接口值分配常量时的内存分配。此实验可能能够回收与关联CL相关的二进制大小增加(因为所有类型的常量都不需要分配(除了32位目标上的complex128常量))。
9条答案
按热度按时间irtuqstp1#
你的意图是让这个实验永远存在,还是在某个时候我们会决定使用哪种表示形式,然后根据需要进行切换?我不愿意永久维护两种表示形式——最终它变成了一个旋钮,而Go通常避免使用旋钮。
eh57zj3b2#
这个结构体在提案中是否分配?
也就是说,你是提议重新打包结构体,还是只重新排序字段?
64位系统上
[2]int64
怎么办?这需要将一个字段拆分成其组成部分。waxmsbnn3#
@josharian
你的意图是让这个实验永远存在,还是说在某个时候我们会决定使用哪种表示法,然后根据需要进行切换?我不愿意永久维护两种表示法——它最终会变成一个旋钮,而Go语言通常避免使用旋钮。
老实说,我预计这个提案会被拒绝,主要是因为实现和维护所需的工作量很大。它会涉及到很多代码。即使找到所有需要更新的内容也可能很困难;这也是反对在#8405中向iface/eface添加标量字的一个论点。另一方面,“可能很难”不是一个不尝试的理由,正如他们所说。
这个变成旋钮的想法让我感到担忧;它应该是一个实验,而不是新的
-O2
。提前决定实验将存在一段时间(也许足够长,包括开发者调查),这是一个不错的解决方案。这个结构在提案中分配内存吗?
[省略了,因为我觉得在这个帖子里说得太多了]
也就是说,你是提议重新打包结构体,还是只重新排序字段?
这将在所有当前目标上分配内存,因为
*int
的对齐要求在a
和p
之间以及之后添加填充,使得有太多的标量字。我假设指针类型需要与其大小相等的对齐,但我相信这是普遍适用的。提案是在运行时在具体类型值和接口之间传输时重新排序字段。这不包括打包。另一种表述所提议的行为的方式是说,一个值必须能够被重新解释(不考虑指针,但其他方面没有数据丢失)为
[n]uintptr
,其中n
是0、1、2或3;然后关于一个指针和两个标量字的规则适用。64位系统上的
[2]int64
怎么办?那需要将一个字段拆分为其组件。这不会分配内存。一般来说,我使用“标量字”一词而不是“标量字段”,因为赋值组合适用于无论类型如何分割成字段的情况——它是在内存级别上,而不是类型级别上。例如,这个类型不会在64位目标上分配:
现在我意识到,一个类型的对齐可能至少需要与
uintptr
相同才能应用这一点。oogrdqng4#
我不认为有必要通过提案过程来实现这个功能。像这样的实验的目的只是为了看看它是否应该成为新的默认实现。这实际上是运行时和编译器团队需要做出的决定:是否值得尝试,谁来做这项工作,如何决定是否采纳新的想法。
因此,我将把这个功能从提案过程中移除。(我不会对这个想法是否好坏发表意见。)
感谢提出这个想法。
ivqmmu1c5#
这是一个相当复杂的实验,涉及到{cmd/compile, runtime, reflect};但我认为它应该对那些足够有动力学习这些细节的人来说是可访问的。我很高兴向任何想要从事这项工作的人提供建议。
9rygscc16#
我有几周的时间可用,所以我会从这里开始。如果我能让它正常工作,我计划提交给Go 1.19,因为已经有计划在1.18对编译器进行重大更改。
3wabscal7#
https://golang.org/cl/343070提到了这个问题:
all: add GOEXPERIMENT=largeiface
nnsrf1az8#
https://golang.org/cl/343071提到了这个问题:
runtime: implement largeiface GOEXPERIMENT
ygya80vv9#
尽管我还没有测试,但这个实验的运行时部分应该快完成了。在实现相等性和哈希算法时,我制定了一些与我最初提出的规则略有不同的规则,用于在接口中直接存储类型:
此外,我还不确定runtime-gdb.py应该怎么做。它通过字段的C风格类型检查一个值是否为接口,但我不知道它期望看到[2]uintptr的哪种类型,也不知道如何找到答案。
我预计对编译器的更改将与对运行时的更改一样广泛。反映和内部/reflectlite的更改应该更简单。其他需要更新的领域包括cmd/link、sync/atomic和x/tools/go/analysis/passes/asmdecl(使
go vet
了解大型接口的不同布局)。与cgo相关的包也可能需要更改,尽管我不确定到什么程度。