``` cmd/compile, runtime: GOEXPERIMENT to add two non-pointer words to iface/eface ```

w6lpcovy  于 6个月前  发布在  Go
关注(0)|答案(9)|浏览(67)

背景

由于GC形状要求,将标量(非指针)值存储到任何接口类型的变量中,会强制该值以间接方式存储,通常分配在堆上。在某些应用程序中,这可能导致许多意外的分配和分配器和垃圾收集器的极大负载,导致显著的性能下降。在最坏的情况下,这可能是对未广泛优化的服务的DOS向量,尤其是那些使用像image或encoding/json这样的包的服务。
接口值具体表示为两种不同的类型:具有非空方法集的接口runtime.ifaceinterface{}的接口。
go/src/runtime/runtime2.go
第202行至第210行 1129a60
| | typeifacestruct { |
| | tabitab |
| | data unsafe.Pointer |
| | } |
| | |
| | typeefacestruct { |
| | _type
_type |
| | data unsafe.Pointer |
| | } |

建议

为了减少在使用接口中的小类型时程序中的分配次数,我建议添加一个GOEXPERIMENT的值,例如GOEXPERIMENT=largeiface,将runtime.ifaceruntime.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启用时,运行时中不存在名称为ifaceeface的类型,而禁用时不存在名称为iface2eface2的类型。这通过确保始终使用实验设置的正确名称来提高可维护性。

示例

有了这个建议,以下类型将成为所有受支持目标上的直接可分配给接口值:

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使用率降低,由于更少的时间用于小对象而内存使用量保持在几个百分点以内”的结果,那么它就可以推广到默认情况。
相关问题:
此将(完全或大部分)解决的问题:

一些相关的已关闭问题:

irtuqstp

irtuqstp1#

你的意图是让这个实验永远存在,还是在某个时候我们会决定使用哪种表示形式,然后根据需要进行切换?我不愿意永久维护两种表示形式——最终它变成了一个旋钮,而Go通常避免使用旋钮。

eh57zj3b

eh57zj3b2#

这个结构体在提案中是否分配?

struct {
  a [10]byte
  p *int
  b [2]byte
}

也就是说,你是提议重新打包结构体,还是只重新排序字段?
64位系统上 [2]int64 怎么办?这需要将一个字段拆分成其组成部分。

waxmsbnn

waxmsbnn3#

@josharian
你的意图是让这个实验永远存在,还是说在某个时候我们会决定使用哪种表示法,然后根据需要进行切换?我不愿意永久维护两种表示法——它最终会变成一个旋钮,而Go语言通常避免使用旋钮。
老实说,我预计这个提案会被拒绝,主要是因为实现和维护所需的工作量很大。它会涉及到很多代码。即使找到所有需要更新的内容也可能很困难;这也是反对在#8405中向iface/eface添加标量字的一个论点。另一方面,“可能很难”不是一个不尝试的理由,正如他们所说。
这个变成旋钮的想法让我感到担忧;它应该是一个实验,而不是新的-O2。提前决定实验将存在一段时间(也许足够长,包括开发者调查),这是一个不错的解决方案。
这个结构在提案中分配内存吗?
[省略了,因为我觉得在这个帖子里说得太多了]
也就是说,你是提议重新打包结构体,还是只重新排序字段?
这将在所有当前目标上分配内存,因为*int的对齐要求在ap之间以及之后添加填充,使得有太多的标量字。我假设指针类型需要与其大小相等的对齐,但我相信这是普遍适用的。
提案是在运行时在具体类型值和接口之间传输时重新排序字段。这不包括打包。另一种表述所提议的行为的方式是说,一个值必须能够被重新解释(不考虑指针,但其他方面没有数据丢失)为[n]uintptr,其中n是0、1、2或3;然后关于一个指针和两个标量字的规则适用。
64位系统上的[2]int64怎么办?那需要将一个字段拆分为其组件。
这不会分配内存。一般来说,我使用“标量字”一词而不是“标量字段”,因为赋值组合适用于无论类型如何分割成字段的情况——它是在内存级别上,而不是类型级别上。例如,这个类型不会在64位目标上分配:

struct {
	a, b, c, d  byte
	e, f        int16
	p           *int
	g, h, i, j, k, l, m, n int8
}

现在我意识到,一个类型的对齐可能至少需要与uintptr相同才能应用这一点。

oogrdqng

oogrdqng4#

我不认为有必要通过提案过程来实现这个功能。像这样的实验的目的只是为了看看它是否应该成为新的默认实现。这实际上是运行时和编译器团队需要做出的决定:是否值得尝试,谁来做这项工作,如何决定是否采纳新的想法。
因此,我将把这个功能从提案过程中移除。(我不会对这个想法是否好坏发表意见。)
感谢提出这个想法。

ivqmmu1c

ivqmmu1c5#

这是一个相当复杂的实验,涉及到{cmd/compile, runtime, reflect};但我认为它应该对那些足够有动力学习这些细节的人来说是可访问的。我很高兴向任何想要从事这项工作的人提供建议。

9rygscc1

9rygscc16#

我有几周的时间可用,所以我会从这里开始。如果我能让它正常工作,我计划提交给Go 1.19,因为已经有计划在1.18对编译器进行重大更改。

3wabscal

3wabscal7#

https://golang.org/cl/343070提到了这个问题:all: add GOEXPERIMENT=largeiface

nnsrf1az

nnsrf1az8#

https://golang.org/cl/343071提到了这个问题:runtime: implement largeiface GOEXPERIMENT

ygya80vv

ygya80vv9#

尽管我还没有测试,但这个实验的运行时部分应该快完成了。在实现相等性和哈希算法时,我制定了一些与我最初提出的规则略有不同的规则,用于在接口中直接存储类型:

  • 不要仅仅使用类型的大小,而是使用其对齐(指针大小以上)。这样,我们可以直接重新排序字段,而无需考虑无法作为uintptrs传输的变量。然而,这意味着不能直接存储更多的类型;例如,color.NRGBA64具有2字节对齐,因此根据此规则,它有四个字。
  • 浮点值在相等性和哈希计算中需要特殊处理。即使我们按目标分割可能的组合,也无法将这些组合存储在_type.tflag中的未使用位中,而且执行这些算法的代码会变得庞大。目前,我的解决方案是为“即使看起来像其他类型,也不能直接存储在接口中”添加一个新的tflag,这将适用于任何具有浮点字段的类型。

此外,我还不确定runtime-gdb.py应该怎么做。它通过字段的C风格类型检查一个值是否为接口,但我不知道它期望看到[2]uintptr的哪种类型,也不知道如何找到答案。
我预计对编译器的更改将与对运行时的更改一样广泛。反映和内部/reflectlite的更改应该更简单。其他需要更新的领域包括cmd/link、sync/atomic和x/tools/go/analysis/passes/asmdecl(使go vet了解大型接口的不同布局)。与cgo相关的包也可能需要更改,尽管我不确定到什么程度。

相关问题