Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

非类型安全指针-使用模式六中对于KeepAlive的一个疑问 #27

Closed
lniwn opened this issue Nov 23, 2019 · 7 comments
Closed
Labels
question Further information is requested

Comments

@lniwn
Copy link

lniwn commented Nov 23, 2019

你好,关于非类型安全指针一文中的使用模式六,有个疑问,前来请教。

一个使用了reflect.SliceHeader的例子:

package main

import (
	"fmt"
	"unsafe"
	"reflect"
	"runtime"
)

func main() {
	a := [6]byte{'G', 'o', '1', '0', '1'}
	bs := []byte("Golang")
	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	hdr.Data = uintptr(unsafe.Pointer(&a))
	runtime.KeepAlive(&a) // 必不可少!
	hdr.Len = 2
	hdr.Cap = len(a)
	fmt.Printf("%s\n", bs) // Go
	bs = bs[:cap(bs)]
	fmt.Printf("%s\n", bs) // Go101
}

注意:上例中的runtime.KeepAlive调用必不可少。 否则,在转换uintptr(unsafe.Pointer(&a))被执行之前,分配给数组a的内存可能已经被回收。

这里对于a被回收的原因有些疑问,因为后面hdr.Cap = len(a)有引用到a,为何a仍然可能会被回收呢?难道说是因为len(a)操作在编译期变成了常量6,导致没有对a产生引用?

@TapirLiu
Copy link
Contributor

难道说是因为len(a)操作在编译期变成了常量6

这个是肯定的,见 https://gfw.go101.org/article/summaries.html#compile-time-evaluation
不过这一点不是此问题的关键。

我重新仔细考虑了一下这个例子,感觉此处的runtime.KeepAlive调用其实是不必要的。此例和文中其它例子中的情况还是有所区别的。因为对hdr.Data的赋值其实就是对bs内部的底层数组指针的赋值,所以数组a的内存时时刻刻都被一个有效的指针引用着。

你觉得呢?

其实reflect.SliceHeaderreflect.StringHeader这两个类型非常不推荐使用,可以说这两个类型是一个设计失误。见 golang/go#19367
最好使用文末提到的自定义SliceHeaderStringHeader类型。

@TapirLiu TapirLiu added the question Further information is requested label Nov 24, 2019
@TapirLiu
Copy link
Contributor

这块比较微妙,如果编译器不能保证hdr.Data = uintptr(unsafe.Pointer(&a))被原子执行,则runtime.KeepAlive调用还是需要的。我再到go-nuts上问问。;)

@lniwn
Copy link
Author

lniwn commented Nov 24, 2019

多谢回复。理解你在原文中表达的意思了。 赋值之后,bs会引用着a内存,所以不会被回收,因此关注点在赋值之前的时间。

通过命令go tool compile -N -S main.go看了下汇编文件,只有在进行

hdr.Data = tmpPtr

才会有writeBarrier写屏障保护,所以感觉runtime.KeepAlive(&a)还是不应该去掉。

如果我没理解错的话,hdr.Data = uintptr(unsafe.Pointer(&a))这一句分开两行写和写在一行并没有发现根本性的差异。

原始文件写在同一行

	0x00d4 00212 (main.go:14)	TESTB	AL, (DI)
	0x00d6 00214 (main.go:14)	PCDATA	$2, $4
	0x00d6 00214 (main.go:14)	MOVQ	"".&a+168(SP), AX
	0x00de 00222 (main.go:14)	PCDATA	$2, $-2
	0x00de 00222 (main.go:14)	PCDATA	$0, $-2
	0x00de 00222 (main.go:14)	CMPL	runtime.writeBarrier(SB), $0
	0x00e5 00229 (main.go:14)	JEQ	236
	0x00e7 00231 (main.go:14)	JMP	1245
	0x00ec 00236 (main.go:14)	MOVQ	AX, "".bs+352(SP)
	0x00f4 00244 (main.go:14)	JMP	246
	0x00f6 00246 (main.go:15)	PCDATA	$2, $1
	0x00f6 00246 (main.go:15)	PCDATA	$0, $5
	0x00f6 00246 (main.go:15)	MOVQ	"".&a+168(SP), AX
	0x00fe 00254 (main.go:15)	PCDATA	$0, $6
	0x00fe 00254 (main.go:15)	MOVQ	AX, ""..autotmp_28+112(SP)
	0x0103 00259 (main.go:15)	PCDATA	$2, $0
	0x0103 00259 (main.go:15)	MOVQ	AX, ""..autotmp_14+160(SP)

修改后的源文件

package main

import (
	"fmt"
	"unsafe"
	"reflect"
	"runtime"
)

func main() {
	a := [6]byte{'G', 'o', '1', '0', '1'}
	bs := []byte("Golang")
	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	tmpPtr := uintptr(unsafe.Pointer(&a))
	hdr.Data = tmpPtr
	runtime.KeepAlive(&a) // 必不可少!
	hdr.Len = 2
	hdr.Cap = len(a)
	fmt.Printf("%s\n", bs) // Go
	bs = bs[:cap(bs)]
	fmt.Printf("%s\n", bs) // Go101
}

写在不同行

	0x00ba 00186 (main.go:14)	PCDATA	$2, $1
	0x00ba 00186 (main.go:14)	LEAQ	"".a+82(SP), AX
	0x00bf 00191 (main.go:14)	PCDATA	$2, $0
	0x00bf 00191 (main.go:14)	MOVQ	AX, "".tmpPtr+88(SP)
	0x00c4 00196 (main.go:15)	PCDATA	$2, $4
	0x00c4 00196 (main.go:15)	MOVQ	"".hdr+120(SP), DI
	0x00c9 00201 (main.go:15)	TESTB	AL, (DI)
	0x00cb 00203 (main.go:15)	PCDATA	$2, $-2
	0x00cb 00203 (main.go:15)	PCDATA	$0, $-2
	0x00cb 00203 (main.go:15)	CMPL	runtime.writeBarrier(SB), $0
	0x00d2 00210 (main.go:15)	JEQ	217
	0x00d4 00212 (main.go:15)	JMP	1216
	0x00d9 00217 (main.go:15)	MOVQ	AX, (DI)
	0x00dc 00220 (main.go:15)	JMP	222

@TapirLiu
Copy link
Contributor

TapirLiu commented Nov 24, 2019

感觉这个和writeBarrier的关系并不太大。writeBarrier主要是在垃圾回收执行过程对指针赋值做出的特殊处理,以防止误把仍在使用的内存标记为垃圾。

Go核心团队成员曾说过:为了避免垃圾回收算法的复杂性,目前官方编译器保证指针的赋值都是原子操作,而并未对其它赋值做出这个保证。这样垃圾回收器不会在一个指针赋值的执行撕裂成两部分,但对于其它赋值,是有可能被撕裂成两部分的,比如上面提到的hdr.Data = uintptr(unsafe.Pointer(&a))。所以这块KeepAlive应该还是需要的。

@TapirLiu
Copy link
Contributor

这里“垃圾回收器不会在一个指针赋值的执行撕裂成两部分”,确切地指在垃圾回收器的扫描过程不会运行在一个指针赋值的执行之间,而只能完全运行于此指针赋值的执行之前或者之后。

@lniwn
Copy link
Author

lniwn commented Nov 25, 2019

我在Windows平台下使用go build -gcflags="-m" .对下面的源代码进行逃逸分析

package main

import (
	"fmt"
	"unsafe"
	"reflect"
	// "runtime"
)

func main() {
	a := [6]byte{'G', 'o', '1', '0', '1'}
	bs := []byte("Golang")
	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	hdr.Data = uintptr(unsafe.Pointer(&a))
	// runtime.KeepAlive(&a) // 必不可少!
	hdr.Len = 2
	hdr.Cap = len(a)
	fmt.Printf("%s\n", bs) // Go
	bs = bs[:cap(bs)]
	fmt.Printf("%s\n", bs) // Go101
}
.\main.go:14:36: &a escapes to heap
.\main.go:11:2: moved to heap: a
.\main.go:18:12: io.Writer(os.Stdout) escapes to heap
.\main.go:18:13: bs escapes to heap
.\main.go:12:14: ([]byte)("Golang") escapes to heap
.\main.go:20:12: io.Writer(os.Stdout) escapes to heap
.\main.go:20:13: bs escapes to heap
.\main.go:13:47: main &bs does not escape
.\main.go:18:12: main []interface {} literal does not escape
.\main.go:20:12: main []interface {} literal does not escape
<autogenerated>:1: os.(*File).close .this does not escape
<autogenerated>:1: os.(*File).isdir .this does not escape

基于上面的讨论,此处的KeepAlive还是加上比较合适,因为a逃逸了,也就是在堆上分配的;如果a没有逃逸,就不会收到gc干扰,就不用加了。

多谢耐心解答。

@lniwn lniwn closed this as completed Nov 25, 2019
@TapirLiu
Copy link
Contributor

TapirLiu commented Nov 25, 2019

看来hdr.Data = uintptr(unsafe.Pointer(&a))这行赋值让编译器觉得将a开在堆上比较安全。如果没有这行,a将被开在栈上。

这个和KeepAlive的使用应该也没有直接联系。事实上,开在栈上比开在堆上对于使用unsafe更加得微妙。因为目前对于标准编译器来说,开在堆上的内存从来不会被移动,但是开在栈上的内存有在栈增缩的时候会被移动。开辟在两者之上的内存都有可能被回收。开在堆上内存是被垃圾回收器回收的。开在栈上的内存准确的说不叫回收,每个栈有一个游标,表示着栈顶,此游标在程序运行时,可能增加(对应开辟新的内存),或者减小(对应回收)。

一个KeepAlive调用将使它的参数引用的内存开辟在堆上,从而避免了此内存被移动的可能。对一个值是否应该使用KeepAlive调用的准则是:如果不使用KeepAlive调用,此值的内存可能在此内存仍被使用之前被回收掉(或者被移动)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants