Skip to content

unsafe: unsafe.Slice escape to heap #72732

@awalterschulze

Description

@awalterschulze

Go version

go version go1.24.0 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN='/Users/yyy/github/bin'
GOCACHE='/Users/yyy/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/yyy/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/th/45dyv7rs03g5t8v9clfswcp80000gn/T/go-build3229043422=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/yyy/github/src/github.com/katydid/parser-go-json/go.mod'
GOMODCACHE='/Users/yyy/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/yyy/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/yyy/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.0'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

I see reflect.SliceHeader is deprecated and I am trying to upgrade to unsafe.Slice.

I want to use it to cast from an int64 to a slice of bytes, without allocating any memory on the heap.

I tried upgrading to unsafe.Slice, but it seems that the pointer is leaked to the heap, which is usually not the case with reflect.SliceHeader.

Here is a small reproduction:

$ go build -gcflags "-m -l" main.go
# command-line-arguments
./main.go:10:13: ... argument does not escape
./main.go:10:14: "hello" escapes to heap
./main.go:16:35: &reflect.SliceHeader{...} does not escape
./main.go:24:26: moved to heap: i
./main.go:29:13: make([]byte, 8) escapes to heap
$ cat -n main.go
     1  package main
     2
     3  import (
     4          "fmt"
     5          "reflect"
     6          "unsafe"
     7  )
     8
     9  func main() {
    10          fmt.Println("hello")
    11
    12  }
    13
    14  // Doesn't allocate, but is deprecated
    15  func castFromInt64(i int64) []byte {
    16          return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
    17                  Len:  8,
    18                  Cap:  8,
    19                  Data: uintptr(unsafe.Pointer(&i)),
    20          }))
    21  }
    22
    23  // Allocates, so need to keep using deprecated method
    24  func unsafeCastFromInt64(i int64) []byte {
    25          return unsafe.Slice((*byte)(unsafe.Pointer(&i)), 8)
    26  }
    27
    28  func takeint(i int64) []byte {
    29          return make([]byte, 8)
    30  }

I can't seem to record an allocation in isolation, so maybe I did something else wrong here:

func TestAllocsUnsafeCast(t *testing.T) {
	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
	f := func() {
		want := int64(math.MaxInt64)
		bs := unsafeCastFromInt64(want)
		got := castToInt64(bs)
		if got != want {
			t.Fatalf("want %d got %d", want, got)
		}
	}
	allocs := testing.AllocsPerRun(1000, f)
	if allocs > 0 {
		t.Fatalf("UnsafeCast Allocs = %f", allocs)
	}
}

But I can reproduce it, if I use it in a larger program and make a single change:

katydid/parser-go-json@16defb4

$ cd json/parse 
$ go test -run=TestNoAllocsOnAverage -v
=== RUN   TestNoAllocsOnAverage
    alloc_test.go:27: seed = 1741363591861790000, got 3 allocs, want 0 allocs
--- FAIL: TestNoAllocsOnAverage (0.04s)
FAIL
exit status 1

The assembler analysis is also interesting and shows that it is significantly more instructions to use unsafe.Slice that to use reflect.SliceHeader for doing this type of cast: https://godbolt.org/z/azPebxG79

So I am just wondering if there is better way to cast from int64 to a slice of bytes without allocating any memoory?

Or is there a concern about the performance implications of moving from reflect.SliceHeader to unsafe.Slice?

I am also wondering if there is a new way to cast from a byte slice to string without doing a copy?

This is the old way, that I used to do it:

func castToString(buf []byte) string {
	header := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
	strHeader := reflect.StringHeader{Data: header.Data, Len: header.Len}
	return *(*string)(unsafe.Pointer(&strHeader))
}

What did you see happen?

This is also answered in previous question.

What did you expect to see?

This is also answered in previous question.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.PerformanceWaitingForInfoIssue is not actionable because of missing required information, which needs to be provided.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions