为什么在Go语言中,切片会被垃圾回收?

huangapple go评论89阅读模式
英文:

Why does a slice in Go stack get garbage collected?

问题

  1. 我编写了一个函数,将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
}
  1. 然后,我编写了一个测试:
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

结果显示测试失败。

  1. 我分析了测试失败的原因,是因为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
}
  1. 但是!!!我有一个很大的问题是:
./string2bytes_test.go:73:26: xxxxxs does not escape

xxxxxs没有逃逸,它位于go栈中,它怎么会被垃圾回收呢!

谁能告诉我为什么?是解决方案"xxxxxs被垃圾回收"是错误的吗?还是其他原因?

英文:
  1. I write a function to copy string to []byte
func unsafeStringToBytes(xxxxxs string) []byte {
	p := &amp;xxxxxs
	sh := (*reflect.StringHeader)(unsafe.Pointer(p))
	sliceHeader := &amp;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
}
  1. Then, I write a test:
package gc

import (
	&quot;bufio&quot;
	&quot;log&quot;
	&quot;os&quot;
	&quot;reflect&quot;
	&quot;runtime&quot;
	&quot;testing&quot;
	&quot;time&quot;
	&quot;unsafe&quot;
)

func TestWriteLog(t *testing.T) {
	f1, err := os.Create(&quot;./log&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer f1.Close()

	for i := 0; i &lt;= 1000000; i++ {
		_, err := f1.WriteString(&quot;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n&quot;)
		if err != nil {
			t.Fatal(err)
		}
	}
}

func heapHeapHeap() {
	var a *[]byte
	for {
		tmp := make([]byte, 100000000, 100000000)
		a = &amp;tmp
		_ = a
	}
}

func TestStringToBytes(t *testing.T) {
	go heapHeapHeap()

	f1, err := os.Open(&quot;./log&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer f1.Close()

	reader := bufio.NewReader(f1)
	count := 1
	var firstChar byte

	for {
		s, _ := reader.ReadString(&#39;\n&#39;)
		if len(s) == 0 {
			continue
		}
		firstChar = s[0]

		// HERE BE DRAGONS
		bytes2 := unsafeStringToBytes(s)

		_, _ = reader.ReadString(&#39;\n&#39;)

		if len(bytes2) &gt; 0 &amp;&amp; firstChar != bytes2[0] {
			t.Fatalf(&quot;win! after %d iterations\n&quot;, count)
			os.Exit(0)
		}

		count++
		//t.Log(count)
	}
}

func unsafeStringToBytes(xxxxxs string) []byte {
	p := &amp;xxxxxs
	sh := (*reflect.StringHeader)(unsafe.Pointer(p))
	sliceHeader := &amp;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=&#39;-m&#39; -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: &amp;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: &amp;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.

  1. I analyze the reason why the test fails, is the xxxxxs has been GC, becase when I put the runtime.KeepAlive(xxxxxs) in the function, the failure has never recurred.
func unsafeStringToBytes(xxxxxs string) []byte {
	p := &amp;xxxxxs
	sh := (*reflect.StringHeader)(unsafe.Pointer(p))
	sliceHeader := &amp;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
}
  1. 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)).

huangapple
  • 本文由 发表于 2023年5月30日 16:41:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76363052.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定