英文:
Why does a slice in Go stack get garbage collected?
问题
- 我编写了一个函数,将
string
复制到[]byte
中:
func unsafeStringToBytes(xxxxxs string) []byte {
p := &xxxxxs
sh := (*reflect.StringHeader)(unsafe.Pointer(p))
sliceHeader := &reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
runtime.GC()
time.Sleep(10 * time.Nanosecond)
b := *(*[]byte)(unsafe.Pointer(sliceHeader))
return b
}
- 然后,我编写了一个测试:
package gc
import (
"bufio"
"log"
"os"
"reflect"
"runtime"
"testing"
"time"
"unsafe"
)
func TestWriteLog(t *testing.T) {
f1, err := os.Create("./log")
if err != nil {
log.Fatal(err)
}
defer f1.Close()
for i := 0; i <= 1000000; i++ {
_, err := f1.WriteString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n")
if err != nil {
t.Fatal(err)
}
}
}
func heapHeapHeap() {
var a *[]byte
for {
tmp := make([]byte, 100000000, 100000000)
a = &tmp
_ = a
}
}
func TestStringToBytes(t *testing.T) {
go heapHeapHeap()
f1, err := os.Open("./log")
if err != nil {
log.Fatal(err)
}
defer f1.Close()
reader := bufio.NewReader(f1)
count := 1
var firstChar byte
for {
s, _ := reader.ReadString('\n')
if len(s) == 0 {
continue
}
firstChar = s[0]
// HERE BE DRAGONS
bytes2 := unsafeStringToBytes(s)
_, _ = reader.ReadString('\n')
if len(bytes2) > 0 && firstChar != bytes2[0] {
t.Fatalf("win! after %d iterations\n", count)
os.Exit(0)
}
count++
//t.Log(count)
}
}
func unsafeStringToBytes(xxxxxs string) []byte {
p := &xxxxxs
sh := (*reflect.StringHeader)(unsafe.Pointer(p))
sliceHeader := &reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
runtime.GC()
time.Sleep(10 * time.Nanosecond)
b := *(*[]byte)(unsafe.Pointer(sliceHeader))
//runtime.KeepAlive(xxxxxs)
return b
}
首先,运行TestWriteLog
生成log
文件。然后我们运行TestStringToBytes
:
$ go test -gcflags='-m' -v -run TestStringToBytes
# go_test/gc [go_test/gc.test]
./string2bytes_test.go:15:22: inlining call to os.Create
./string2bytes_test.go:29:6: can inline heapHeapHeap
./string2bytes_test.go:41:20: inlining call to os.Open
./string2bytes_test.go:47:27: inlining call to bufio.NewReader
./string2bytes_test.go:47:27: inlining call to bufio.NewReaderSize
./string2bytes_test.go:47:27: inlining call to bufio.(*Reader).reset
./string2bytes_test.go:14:19: leaking param: t
./string2bytes_test.go:17:12: ... argument does not escape
./string2bytes_test.go:24:11: ... argument does not escape
./string2bytes_test.go:32:3: moved to heap: tmp
./string2bytes_test.go:32:14: make([]byte, 100000000, 100000000) escapes to heap
./string2bytes_test.go:73:26: xxxxxs does not escape
./string2bytes_test.go:76:17: &reflect.SliceHeader{...} does not escape
./string2bytes_test.go:38:24: leaking param: t
./string2bytes_test.go:43:12: ... argument does not escape
./string2bytes_test.go:47:27: new(bufio.Reader) does not escape
./string2bytes_test.go:47:27: make([]byte, bufio.size) escapes to heap
./string2bytes_test.go:64:12: ... argument does not escape
./string2bytes_test.go:64:13: count escapes to heap
./string2bytes_test.go:90:24: leaking param: s
# go_test/gc.test
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:39:6: can inline init.0
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:47:24: inlining call to testing.MainStart
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:47:42: testdeps.TestDeps{} escapes to heap
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:47:24: &testing.M{...} escapes to heap
=== RUN TestStringToBytes
string2bytes_test.go:64: win! after 164307 iterations
--- FAIL: TestStringToBytes (57.46s)
FAIL
exit status 1
FAIL go_test/gc 57.938s
结果显示测试失败。
- 我分析了测试失败的原因,是因为
xxxxxs
已经被垃圾回收了,因为当我在函数中加入runtime.KeepAlive(xxxxxs)
时,失败就再也没有发生过。
func unsafeStringToBytes(xxxxxs string) []byte {
p := &xxxxxs
sh := (*reflect.StringHeader)(unsafe.Pointer(p))
sliceHeader := &reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
runtime.GC()
time.Sleep(10 * time.Nanosecond)
b := *(*[]byte)(unsafe.Pointer(sliceHeader))
runtime.KeepAlive(xxxxxs)
return b
}
- 但是!!!我有一个很大的问题是:
./string2bytes_test.go:73:26: xxxxxs does not escape
xxxxxs
没有逃逸,它位于go栈中,它怎么会被垃圾回收呢!
谁能告诉我为什么?是解决方案"xxxxxs被垃圾回收"是错误的吗?还是其他原因?
英文:
- I write a function to copy
string
to[]byte
:
func unsafeStringToBytes(xxxxxs string) []byte {
p := &xxxxxs
sh := (*reflect.StringHeader)(unsafe.Pointer(p))
sliceHeader := &reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
runtime.GC()
time.Sleep(10 * time.Nanosecond)
b := *(*[]byte)(unsafe.Pointer(sliceHeader))
return b
}
- Then, I write a test:
package gc
import (
"bufio"
"log"
"os"
"reflect"
"runtime"
"testing"
"time"
"unsafe"
)
func TestWriteLog(t *testing.T) {
f1, err := os.Create("./log")
if err != nil {
log.Fatal(err)
}
defer f1.Close()
for i := 0; i <= 1000000; i++ {
_, err := f1.WriteString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n")
if err != nil {
t.Fatal(err)
}
}
}
func heapHeapHeap() {
var a *[]byte
for {
tmp := make([]byte, 100000000, 100000000)
a = &tmp
_ = a
}
}
func TestStringToBytes(t *testing.T) {
go heapHeapHeap()
f1, err := os.Open("./log")
if err != nil {
log.Fatal(err)
}
defer f1.Close()
reader := bufio.NewReader(f1)
count := 1
var firstChar byte
for {
s, _ := reader.ReadString('\n')
if len(s) == 0 {
continue
}
firstChar = s[0]
// HERE BE DRAGONS
bytes2 := unsafeStringToBytes(s)
_, _ = reader.ReadString('\n')
if len(bytes2) > 0 && firstChar != bytes2[0] {
t.Fatalf("win! after %d iterations\n", count)
os.Exit(0)
}
count++
//t.Log(count)
}
}
func unsafeStringToBytes(xxxxxs string) []byte {
p := &xxxxxs
sh := (*reflect.StringHeader)(unsafe.Pointer(p))
sliceHeader := &reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
runtime.GC()
time.Sleep(10 * time.Nanosecond)
b := *(*[]byte)(unsafe.Pointer(sliceHeader))
//runtime.KeepAlive(xxxxxs)
return b
}
At first, run the TestWriteLog
to generate log
file. Then we run the TestStringToBytes
:
$ go test -gcflags='-m' -v -run TestStringToBytes 1 ↵
# go_test/gc [go_test/gc.test]
./string2bytes_test.go:15:22: inlining call to os.Create
./string2bytes_test.go:29:6: can inline heapHeapHeap
./string2bytes_test.go:41:20: inlining call to os.Open
./string2bytes_test.go:47:27: inlining call to bufio.NewReader
./string2bytes_test.go:47:27: inlining call to bufio.NewReaderSize
./string2bytes_test.go:47:27: inlining call to bufio.(*Reader).reset
./string2bytes_test.go:14:19: leaking param: t
./string2bytes_test.go:17:12: ... argument does not escape
./string2bytes_test.go:24:11: ... argument does not escape
./string2bytes_test.go:32:3: moved to heap: tmp
./string2bytes_test.go:32:14: make([]byte, 100000000, 100000000) escapes to heap
./string2bytes_test.go:73:26: xxxxxs does not escape
./string2bytes_test.go:76:17: &reflect.SliceHeader{...} does not escape
./string2bytes_test.go:38:24: leaking param: t
./string2bytes_test.go:43:12: ... argument does not escape
./string2bytes_test.go:47:27: new(bufio.Reader) does not escape
./string2bytes_test.go:47:27: make([]byte, bufio.size) escapes to heap
./string2bytes_test.go:64:12: ... argument does not escape
./string2bytes_test.go:64:13: count escapes to heap
./string2bytes_test.go:90:24: leaking param: s
# go_test/gc.test
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:39:6: can inline init.0
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:47:24: inlining call to testing.MainStart
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:47:42: testdeps.TestDeps{} escapes to heap
/var/folders/6w/7dl1f7mx2pv1_v48_mg37br80000gn/T/go-build1924552004/b001/_testmain.go:47:24: &testing.M{...} escapes to heap
=== RUN TestStringToBytes
string2bytes_test.go:64: win! after 164307 iterations
--- FAIL: TestStringToBytes (57.46s)
FAIL
exit status 1
FAIL go_test/gc 57.938s
As the results shows, it fails.
- I analyze the reason why the test fails, is the
xxxxxs
has been GC, becase when I put theruntime.KeepAlive(xxxxxs)
in the function, the failure has never recurred.
func unsafeStringToBytes(xxxxxs string) []byte {
p := &xxxxxs
sh := (*reflect.StringHeader)(unsafe.Pointer(p))
sliceHeader := &reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
runtime.GC()
time.Sleep(10 * time.Nanosecond)
b := *(*[]byte)(unsafe.Pointer(sliceHeader))
runtime.KeepAlive(xxxxxs)
return b
}
- BUT!!! I have a big question is that:
./string2bytes_test.go:73:26: xxxxxs does not escape
The xxxxxs
does not escape, it is located in the go stack, How can it be GC!
Who can tell me why? Is the solution "xxxxxs
be GC" wrong? Or any other reason?
答案1
得分: 7
你的代码违反了unsafe.Pointer规则。特别是规则6指定你不应该声明或分配reflect.SliceHeader
类型的变量。
由于这个原因,当堆栈增长/缩小/移动时,垃圾回收器可能会丢失对你的字符串的跟踪。
请注意,在现代的Go版本中,有一种更简单、更安全的方法来进行你想要的转换:unsafe.Slice(unsafe.StringData(s), len(s))
。
英文:
Your code violates the unsafe.Pointer rules. In particular, rule 6 specifies that you should not declare or allocate variables of reflect.SliceHeader
.
As a consequence of this, it's possible that the GC loses track of your string when it grows/shrinks/moves the stack around.
Note that there is a simpler and safer way to do the conversion you want, in modern Go versions: unsafe.Slice(unsafe.StringData(s), len(s))
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论