Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Addresses of slices of empty structs

Tags:

slice

pointers

go

I have a basic question on empty structs and am trying to get my head around the following different outputs I am getting when attempting to get the address of backing arrays' elements for two slices:

a := make([]struct{}, 10)
b := make([]struct{}, 20)
fmt.Println("&a == &b", &a == &b)
fmt.Println("&a[0] == &b[0]", &a[0] == &b[0])

The above snippet returns:

&a == &b false
&a[0] == &b[0] true

However, considering the following slightly changed snippet:

a := make([]struct{}, 10)
b := make([]struct{}, 20)
fmt.Println(a[0], &a[0])
fmt.Println("&a == &b", &a == &b)
fmt.Println("&a[0] == &b[0]", &a[0] == &b[0])

The above snippet returns:

{} &{}
&a == &b false
&a[0] == &b[0] false

Can someone please explain the reason for the above difference? Thank you!

[Follow Up] Making the following modifications:

package main

import "fmt"

type S struct{}

func (s *S) addr() { fmt.Printf("%p\n", s) }

func main() {
    a := make([]S, 10)
    b := make([]S, 20)
    fmt.Println(a[0], &a[0])
    fmt.Println("&a == &b", &a == &b)
    fmt.Println("&a[0] == &b[0]", &a[0] == &b[0])
    //a[0].addr()
    //b[0].addr()
}

Still returns the same output:

{} &{}
&a == &b false
&a[0] == &b[0] false

Though, uncommenting the method calls, returns:

{} &{}
&a == &b false
&a[0] == &b[0] true
0x19583c // ==> [depends upon env]
0x19583c // ==> [depends upon env]
like image 448
Kevin Ghaboosi Avatar asked Jan 01 '18 19:01

Kevin Ghaboosi


People also ask

Why would you prefer to use an empty struct {}?

Best solution is to pass an empty structure because it will only increment a counter in the channel but not assign memory, copy elements and so on.

How do I check if a struct is empty?

1) To check if the structure is empty:fmt. Println( "It is an empty structure." ) fmt. Println( "It is not an empty structure." )

What is an empty struct?

An empty structIt occupies zero bytes of storage. var s struct{} fmt.Println(unsafe.Sizeof(s)) // prints 0. As the empty struct consumes zero bytes, it follows that it needs no padding. Thus a struct comprised of empty structs also consumes no storage.

How do I assign an empty slice in Golang?

To declare the type for a variable that holds a slice, use an empty pair of square brackets, followed by the type of elements the slice will hold.


1 Answers

Before going any deeper, know that according to the spec, a program is correct regardless of whether it yields equal or different addresses for values having zero size, as the spec only states that they may be the same, but does not require them to be the same.

Spec: Size and alignment guarantees:

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

So what you experience is an implementation detail. There are more details and factors to the decisions made, the following explanation is valid and sufficient only to your concrete examples:

In your first example addresses of the backing array(s) for your slices are only used inside the main() function, they don't escape to the heap. What you print is only the result of the comparison of the addresses. These are just bool values, they don't include the address values. So the compiler chooses to use the same address for the backing array of a and b.

In your second example addresses of the backing arrays (more specifically addresses of some elements of the backing arrays) are used outside of the main() function, they are passed to and used inside the fmt.Println() function, because you are also printing these addresses.

We can "prove" this by passing the -gcflags '-m' params to the Go tool, asking it to print the result of the escape analysis.

In your first example, saving your code in play.go, running the go run -gcflags '-m' play.go command, the output is:

./play.go:10:14: "&a == &b" escapes to heap
./play.go:10:29: &a == &b escapes to heap
./play.go:11:14: "&a[0] == &b[0]" escapes to heap
./play.go:11:38: &a[0] == &b[0] escapes to heap
./play.go:8:11: main make([]struct {}, 10) does not escape
./play.go:9:11: main make([]struct {}, 20) does not escape
./play.go:10:26: main &a does not escape
./play.go:10:32: main &b does not escape
./play.go:10:13: main ... argument does not escape
./play.go:11:32: main &a[0] does not escape
./play.go:11:41: main &b[0] does not escape
./play.go:11:13: main ... argument does not escape
&a == &b false
&a[0] == &b[0] true

As we can see, addresses don't escape.

Running go run -gcflags '-m' play.go with your second example, the output is:

./play.go:10:15: a[0] escapes to heap
./play.go:10:20: &a[0] escapes to heap
./play.go:10:20: &a[0] escapes to heap
./play.go:8:11: make([]struct {}, 10) escapes to heap
./play.go:11:14: "&a == &b" escapes to heap
./play.go:11:29: &a == &b escapes to heap
./play.go:12:14: "&a[0] == &b[0]" escapes to heap
./play.go:12:38: &a[0] == &b[0] escapes to heap
./play.go:9:11: main make([]struct {}, 20) does not escape
./play.go:10:13: main ... argument does not escape
./play.go:11:26: main &a does not escape
./play.go:11:32: main &b does not escape
./play.go:11:13: main ... argument does not escape
./play.go:12:32: main &a[0] does not escape
./play.go:12:41: main &b[0] does not escape
./play.go:12:13: main ... argument does not escape
{} &{}
&a == &b false
&a[0] == &b[0] false

As you can see, a[0], &a[0] escape to heap, so the backing array of a is dynamically allocated, and thus it will have a different address than that of b's.

Let's "prove" this further. Let's modify your second example, to have a third variable c whose address will also not be printed, and let's compare b to c:

a := make([]struct{}, 10)
b := make([]struct{}, 20)
c := make([]struct{}, 30)
fmt.Println(a[0], &a[0])
fmt.Println("&a == &b", &a == &b)
fmt.Println("&a[0] == &b[0]", &a[0] == &b[0])
fmt.Println("&b == &c", &b == &c)
fmt.Println("&b[0] == &c[0]", &b[0] == &c[0])

Running go run -gcflags '-m' play.go on this, the output is:

./play.go:11:15: a[0] escapes to heap
./play.go:11:20: &a[0] escapes to heap
./play.go:11:20: &a[0] escapes to heap
./play.go:8:11: make([]struct {}, 10) escapes to heap
./play.go:12:14: "&a == &b" escapes to heap
./play.go:12:29: &a == &b escapes to heap
./play.go:13:14: "&a[0] == &b[0]" escapes to heap
./play.go:13:38: &a[0] == &b[0] escapes to heap
./play.go:14:14: "&b == &c" escapes to heap
./play.go:14:29: &b == &c escapes to heap
./play.go:15:14: "&b[0] == &c[0]" escapes to heap
./play.go:15:38: &b[0] == &c[0] escapes to heap
./play.go:9:11: main make([]struct {}, 20) does not escape
./play.go:10:11: main make([]struct {}, 30) does not escape
./play.go:11:13: main ... argument does not escape
./play.go:12:26: main &a does not escape
./play.go:12:32: main &b does not escape
./play.go:12:13: main ... argument does not escape
./play.go:13:32: main &a[0] does not escape
./play.go:13:41: main &b[0] does not escape
./play.go:13:13: main ... argument does not escape
./play.go:14:26: main &b does not escape
./play.go:14:32: main &c does not escape
./play.go:14:13: main ... argument does not escape
./play.go:15:32: main &b[0] does not escape
./play.go:15:41: main &c[0] does not escape
./play.go:15:13: main ... argument does not escape
{} &{}
&a == &b false
&a[0] == &b[0] false
&b == &c false
&b[0] == &c[0] true

Since only &a[0] is printed but not &b[0] nor &c[0], thus &a[0] == &b[0] will be false but &b[0] == &c[0] will be true.

like image 80
icza Avatar answered Sep 25 '22 01:09

icza