Public account: queer cloud storage

[toc]

background

Golang normal struct is a normal memory block, must occupy a small amount of memory, and the size of the structure is bound, length aligned, but “empty structure” does not occupy memory, size 0;

Tip: The following analysis is based on GO1.13.3 Linux/AMD64.

A common structure is defined as follows:

// Align type variables to 8 bytes;
type Tp struct {
    a uint16
    b uint32
}
Copy the code

According to memory alignment rules, this structure takes 8 bytes of memory.

Empty structure:

var s struct{}
// The variable size is 0;
fmt.Println(unsafe.Sizeof(s))
Copy the code

The variables in this empty structure occupy 0 bytes of memory.

Essentially, empty structures are used for one purpose: to save memory, but more often than not, the memory savings are very limited, and the reason for using empty structures is that you don’t care about the values of the variables in the structure.

The principle of decryption

Special variable: Zerobase

An empty structure is a structure with no memory size. That’s true, but to be more precise, there’s a special starting point: the Zerobase variable, which is a uintptr global variable that takes 8 bytes. When you define an infinite number of struct {} variables anywhere, the compiler just gives out the address of the Zerobase variable. In other words, in Golang, all memory allocations of size 0 are involved, so the same address & Zerobase is used.

Here’s an example:

package main

import "fmt"

type emptyStruct struct {}

func main(a) {
	a := struct{}{}
	b := struct{}{}
	c := emptyStruct{}

	fmt.Printf("%p\n", &a)
	fmt.Printf("%p\n", &b)
	fmt.Printf("%p\n", &c)
}
Copy the code

DLV debugging analysis:

(dlv) p &a
(*struct {})(0x57bb60)
(dlv) p &b
(*struct {})(0x57bb60)
(dlv) p &c
(*main.emptyStruct)(0x57bb60)
(dlv) p &runtime.zerobase
(*uintptr)(0x57bb60)
Copy the code

Summary: Empty structure variables have the same memory addresses.

Memory management special handling

mallocgc

Struct {} (struct {}, struct {}, struct {}, struct {}, struct {}, struct {} (struct {}, struct {});

The code is as follows:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // Allocate a struct with size 0 and give the address of the global variable zerobase;
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
    // ... 
Copy the code

When golang allocates memory using mallocGC, it returns the address of the global variable Zerobase if size is 0.

Having this globally unique address also facilitates some special processing of logic later.

The various postures defined

Native definition

a := struct{} {}Copy the code

Struct {} (struct{}) (struct{} (struct{}) (struct{}) (struct{} (struct{}));

Redefining types

Golang uses the type keyword to define new types, such as:

type emptyStruct struct{}
Copy the code

EmptyStruct {} is the same as emptryStruct {}. The compiler allocates memory for emptryStruct directly to zerobase.

Anonymous nested types

Struct {} is an anonymous field that contains other structures. What does this look like?

Anonymous nesting method one

type emptyStruct struct{}
type Object struct {
    emptyStruct
}
Copy the code

Anonymous nesting method two

type Object1 struct{_struct{}}Copy the code

Object1, Object1, runtime_zerobase, size_zerobase, size_zerobase, size_zerobase, size_zerobase, size_zerobase Does not occupy any memory size.

The built-in field

There is nothing special about the built-in field scenario, but address and length alignment should be considered. Or just 3 points:

  • Empty structure types do not take up memory size;
  • Address offset to align with its own type;
  • The overall type length should be aligned with the longest field type length.

Let’s discuss this in three scenarios:

Scene 1:struct {}In the front

The struct {} field type is first, and it takes no space, so naturally the address of the second field corresponds to the address of the entire variable.

// The Object1 variable takes 1 byte
type Object1 struct {
	s struct {}
	b byte
}

// The Object2 variable takes 8 bytes
type Object2 struct {
	s struct {}
	n int64
}

o1 := Object1{ }
o2 := Object2{ }
Copy the code

How is memory allocated?

  • &o1&o1.sIt’s a consistent variableo1Align the memory size to 1 byte;
  • &o2&o2.sIt’s a consistent variableo2The memory size is aligned to 8 bytes;

This allocation satisfies alignment rules, and the compiler does not do any special byte padding for such struct {} fields.

Scene 2:struct {}In the middle

// The Object1 variable takes 16 bytes
type Object1 struct {
	b  byte
	s  struct{}
	b1 int64
}

o1 := Object1{ }
Copy the code
  • According to alignment rules, variableso1Takes up 16 bytes;
  • &o1.s&o1.b1The same;

The compiler does not do any byte padding on struct {}.

Scenario 3:struct {}In the final

Be a little careful about this scenario, because the compiler will do special byte padding when it encounters it, as follows;

type Object1 struct {
	b byte
	s struct{}}type Object2 struct {
	n int64
	s struct{}}type Object3 struct {
	n int16
	m int16
	s struct{}}type Object4 struct {
	n  int16
	m  int64
	s  struct{}
}

o1 := Object1 { }
o2 := Object2 { }
o3 := Object3 { }
o4 := Object4 { }
Copy the code

When this struct {} is in the last field, the compiler will do special padding. As the last field, the struct {} will be filled to the size of the previous field. The address offset alignment rules remain unchanged.

Now you can think about it in your mind, how much memory is allocated for o1, O2, O3, and O4? Decrypt below:

  • variableo1Size is 2 bytes;
  • variableo2Size is 16 bytes;
  • variableo3The size is 6 bytes;
  • variableo4Size is 24 bytes;

In this case, you need to allocate the padding memory to the struct {} according to the length of the previous field, and then keep the entire variable aligned according to the address and length rules.

struct {}As the receiver

Receiver is the basic feature of the struct in Golang. An empty structure is essentially the same as a structure, and can be used as a receiver to define methods.

type emptyStruct struct{}

func (e *emptyStruct) FuncB(n, m int){}func (e emptyStruct) FuncA(n, m int){}func main(a) {
	a := emptyStruct{}

	n := 1
	m := 2

	a.FuncA(n, m)
	a.FuncB(n, m)
}
Copy the code

Receiver is the foundation of Golang’s object-oriented support. The implementation of receiver is also very simple in nature. The general case (ordinary structure) can be translated as:

func FuncA (e *emptyStruct, n, m int){}func FuncB (e  emptyStruct, n, m int){}Copy the code

The compiler just passes the value or address of the object as the first argument, that’s all. Empty structures are slightly different. Empty structures should be translated as:

func FuncA (e *emptyStruct, n, m int){}func FuncB (n, m int){}Copy the code

Extremely simple code, corresponding to the actual assembly code is as follows:

FuncA, FuncB are as simple as that, as follows:

00000000004525b0 <main.(*emptyStruct).FuncB>:
  4525b0:	c3                   	retq   

00000000004525c0 <main.emptyStruct.FuncA>:
  4525c0:	c3                   	retq    
Copy the code

The main function

00000000004525d0 <main.main>: 4525d0: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx 4525d9: 48 3b 61 10 cmp 0x10(%rcx),%rsp 4525dd: 76 63 jbe 452642 <main.main+0x72> 4525df: 48 83 ec 30 sub $0x30,%rsp 4525e3: 48 89 6c 24 28 mov %rbp,0x28(%rsp) 4525e8: 48 8d 6c 24 28 lea 0x28(%rsp),%rbp 4525ed: 48 C7 44 24 18 01 00 movq $0x1,0x18(% RSP) 4525f6:48 C7 44 24 20 02 00 movq $0x2,0x20(% RSP) 4525ff: 48 8b 44 24 18 mov 0x18(% RSP),%rax 452604:48 89 04 24 mov %rax,(% RSP) 48 c7 44 24 08 02 00 movq $0x2,0x8(% RSP) e8 aa ff ff ff callq 4525c0 <main.emptyStruct.FuncA> 452616: 48 8d 44 24 18 lea 0x18(%rsp),%rax 45261b: 48 89 04 24 mov %rax,(% RSP) 45261f: 48 8b 44 24 18 mov 0x18(% RSP),%rax 452624:48 89 44 24 08 mov %rax,0x8(% RSP) 48 8b 44 24 20 mov 0x20(% RSP),%rax 45262e: 48 89 44 24 10 mov %rax,0x10(% RSP) e8 78 ff ff ff callq 4525b0 <main.(*emptyStruct).FuncB> 452638: 48 8b 6c 24 28 mov 0x28(%rsp),%rbp 45263d: 48 83 c4 30 add $0x30,%rsp 452641: c3 retq 452642: e8 b9 7a ff ff callq 44a100 <runtime.morestack_noctxt> 452647: eb 87 jmp 4525d0 <main.main>Copy the code

Verify a few points with this code:

  1. The receiver is essentially a syntactic sugar that is passed to the function as the first argument;
  2. In the scenario where receiver is a value, there is no need to pass an empty structure as the first parameter, because the empty structure has no value.
  3. In a scenario where the receiver is a pointer, the object address is passed to the function as the first argument, and the compiler is passed in when the function is calledzerobase(this can be verified at compile time);

After binary compilation, the first argument to a call to e. freca is to push &zerobase directly onto the stack.

Summarize several knowledge points:

  • Receiver is essentially a very simple general idea that passes an object value or address as the first argument to a function.
  • Function parameters push the stack from front to back (can debug see);
  • When an object value is used as a receiver, a value copy is involved;
  • Golang’s function definition for a value receiver may generate two functions, one for a value version and one for a pointer version, depending on the actual situation. Is thereinterfaceScene);
  • Where an empty structure is recognized during compilation, the compiler can do special code generation for established facts;

In other words, during compilation, the parameters of the empty structure can be determined, and when the code is generated, the corresponding static code can be generated.

Combined posture

The core reason why empty struct{} exists is to save memory. When you need a structure that doesn’t matter at all what’s inside, consider an empty structure. Golang core several composite structures map, chan, slice can be used with struct{}.

map & struct{}

The common combination of map and struct {} looks like this:

/ / create a map
m := make(map[int]struct{})
/ / assignment
m[1] = struct{} {}// Check that the key store does not exist
_, ok := m[1]
Copy the code

In general, map and struct {} are used together in such a way that only the key is concerned and the value is not concerned. For example, we can use this data structure to query whether the key exists, and judge whether the key exists by the value of OK. The query complexity of map is O(1), and the query is fast.

Map [int]struct{} (struct{}) {int {} (struct{}) {int {} (struct{});

chan & struct{}

The combination of channel and struct{} is one of the most classic scenarios. The struct{} is usually transmitted as a signal without paying attention to its contents. Chan’s analysis has been detailed in previous articles. The chan data structure is essentially a management structure with a ringbuffer. If struct{} is an element, the ringbuffer is allocated by 0.

Chan and struct{} can only be used in one way or another. Empty structures cannot carry values themselves, so they can only be used in one way or another.

// Create a signal channel
waitc := make(chan struct{})

// ...
goroutine 1:
    // Send a signal: send an element
    waitc <- struct{}
    // Send signal: off
    close(waitc)

goroutine 2:
    select {
    // Receive the signal and act accordingly
    case <-waitc:
    }    
Copy the code

So let’s think about this scenario, does it have to be struct{}? Not really, and not many bytes of memory, so it’s really just a matter of not caring about the value of the chan element, so that’s why we use the struct{}.

slice & struct{}

Formally, slice is also combined with struct{}.

s := make([]struct{}, 100)
Copy the code

We create an array that has only 24 bytes of memory (addr, len, cap), no matter how big the allocation is, but to be honest, this is not very useful.

Creating slice is a call to malllocgc, while mallocGC returns the zerobase address when allocating memory size 0. Slice returns the zerobase address when the size is 0.

func growslice(et *_type, old slice, cap int) slice {
    // If the size of the element is 0, then the address of zerobase is assigned directly;
    if et.size == 0 {
        return slice{unsafe.Pointer(&zerobase), old.len.cap}}}Copy the code

conclusion

  1. An empty structure is also a structure, but of type size 0;
  2. All empty structures have a common address:zerobaseThe address;
  3. The null structure can be used as a receiver. When the null structure is used as a value, the compiler actually directly ignores the passing of the first parameter, and the compiler can confirm to generate the corresponding code during compilation.
  4. mapstruct{}Combined use is often used to save a little memory, and the scenario used is usually used to determine whether the key existsmap;
  5. chanstruct{}The combination is generally used in signal synchronization scenarios, not to save memory, but we really don’t care about the value of the chan element;
  6. slicestruct{}It really doesn’t seem to work…

Public account: queer cloud storage