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.s
It’s a consistent variableo1
Align the memory size to 1 byte;&o2
和&o2.s
It’s a consistent variableo2
The 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, variables
o1
Takes up 16 bytes; &o1.s
和&o1.b1
The 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:
- variable
o1
Size is 2 bytes; - variable
o2
Size is 16 bytes; - variable
o3
The size is 6 bytes; - variable
o4
Size 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:
- The receiver is essentially a syntactic sugar that is passed to the function as the first argument;
- 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.
- 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 called
zerobase
(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 there
interface
Scene); - 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
- An empty structure is also a structure, but of type size 0;
- All empty structures have a common address:
zerobase
The address; - 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.
map
和struct{}
Combined use is often used to save a little memory, and the scenario used is usually used to determine whether the key existsmap
;chan
和struct{}
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;slice
和struct{}
It really doesn’t seem to work…
Public account: queer cloud storage