One, foreword

To learn more about Golang, you have to understand memory, and this time I’m going to talk about memory allocation, memory models, and escape analysis. Let’s keep this in mind in programming.

Second, memory allocation

(1) Here are four related data structures

1, mspan

Next and prev form a bidirectional linked list, mSPAN is responsible for managing the address space of N pages starting from startAddr. Is the basic unit of memory allocation. Is a basic unit for managing memory.

// Keep important member variablestypeStruct {next *mspan prev *mspan // uintptr Npages Uintptr // How many pages are in a SPAN SWEEPgen uint32 // GC related INCache bool // Whether the SPAN is occupied by the McAche. Spanclass SPANClass // 0 ~ A value between the _NumSizeClasses, i.e., 3, would split the Mspan into 32byte chunks.Copy the code

2, the McAche

In GO, each P is assigned a McAche, which is private and does not require a lock to allocate memory from there

typeStruct {tiny Uintptr // uintyoffset tinyUintptr // uintyallocs uintptr // number of tinyallocs not countedinOther stats alloc [numSpanClasses]*mspan}Copy the code

3, McEntral

When the McAche is insufficient, the system applies for memory from McEntral. The structure is actually in the MHEAP, so in my opinion, this acts as a bridge.

typeMcEntral struct {lock mutex; Need to lock the spanClass spanClass // corresponding to the MSPAN in the SPANClass nonempty mSpanList // McEntral available mSPAN list empty mSpanList // List of mspans already used in this McEntral}Copy the code

4, mheap

The MHeap actually has a virtual address. When McEntral is insufficient, it applies to the MHeap.

type[_MaxMHeapList] mheap struct {lock mutex // unallocated spanList, For example, free[3] is a linked list consisting of three page mspan freelarge mTreap // mspan, _MaxMHeapList BUSY [_MaxMHeapList]mSpanList // busy lists of large SPANS of given length busylarge Spans list // busy Lists of large SPANS Length >= _MaxMHeapList allSPANS []* MSPAN Arena_start Uintptr // Arena is a continuous virtual address area used to allocate memory in Golang. Arena_alloc Uintptr arena_end Uintptr central [numSpanClasses]struct { McEntral McEntral pad [sys.cachelinesize - safe.Sizeof(McEntral {})% sys.cachelinesize]bytefalseSharing) problem} spanalloc fixalloc // allocatorforSpan * cachealloc fixalloc // McAche allocator}Copy the code

Please take a closer look at the picture below and make sure you understand it.

(2) Details of memory allocation

Do not expand the source code here, just know the allocation rules. (in golang1.10, MacOs 10.12, 32K below changed to 64K)

1, Object size > 32K; Mheap is allocated directly.

2, object size < 16 byte; McAche’s tiny object allocator tiny is allocated directly.

3. If object size > 16 bytes && size <= 32K bytes, the McAche file is allocated first. If no block is available for the McAche, the McEntral requests a block from the MHeap. If the MHeap does not have an appropriate span, the MHeap requests a block from the operating system.

Third, the memory model

Here’s what happens before in Golang (suppose A and B represent two operations performed by A multithreaded program. If A happens-before B, the memory impact of A’s operation will be visible to the thread executing B (and before B).

(1) the Init function

The init function in P2 Happens all operations in BeforeP1 when package P2 is imported in P1

2. All init functions happen Before Main

(2) Channel

1. The send operation on an element Happens Before the corresponding receive operation

The close operation on the channel Happens Before the close notification is received on the receiving end

3. For an uncached Channel, the receive operation on an element Happens Before the corresponding send operation completes

4. If the buffer size of a cached Channel is C, the receive operation Happens Before the send operation. .

Fourth, escape analysis

Why do escape analysis? Because the cost of allocation on the stack is much less than allocation on the heap, and that’s one of the things that a lot of people don’t have right now, including me. Recently, I read some articles in this area, and then went back to my own code, and found a lot of unreasonable places. I hope that through this explanation, we can make progress together.

(1) What is memory escape

In simple terms, objects that should be allocated memory on the stack escape to the heap for allocation. If it can be allocated on the stack, then only two instructions are required, on and off the stack, and the GC pressure is reduced. So it’s a lot cheaper to allocate on the stack.

(2) circumstances causing escape

As a personal summary, if the scope and memory footprint of a variable cannot be determined at compile time, it will escape to the heap.

1, the pointer

As we usually know, passing Pointers can reduce copying of underlying values and improve efficiency, in general, but if you copy a small amount of data, passing Pointers is not necessarily more efficient than value copying.

(1) Pointers are indirect addresses, and the addresses they point to are mostly stored on the heap, so Pointers are not necessarily efficient considering GC. Look at an example

type test struct{}

func main() {
	t1 := test1()
	t2 := test2()
	println("t1", &t1, "t2", &t2)
}

func test(1)test {
	t1 := test{}
	println("t1", &t1)
	return t1
}

func test(2) *test {
	t2 := test{}
	println("t2", &t2)
	return &t2
}
Copy the code

Run to see escape (disable inlining)

go run -gcflags '-m -l' main.go
# command-line-arguments
./main.go:36:16: test1 &t1 does not escape
./main.go:43:9: &t2 escapes to heap
./main.go:41:2: moved to heap: t2
./main.go:42:16: test2 &t2 does not escape
./main.go:31:16: main &t1 does not escape
./main.go:31:27: main &t2 does not escape
t1 0xc420049f50
t2 0x10c1648
t1 0xc420049f70 t2 0xc420049f70

Copy the code

As you can see from above, t2 in the test2 function that returns the pointer escapes to the heap, where it is awaited by the brutal GC.

2 and slice

It is most likely to be allocated to the heap if the size of the slice cannot be determined at compile time or if the slice size is too large to exceed the stack size limit, or if memory is reallocated at append.

// Slice over stack size funcmain(){s := make([]byte, 1, 64 * 1024) _ = smain() {
	s := make([]byte, 1, rand2.Intn(10))
	_ = s
}
Copy the code

So with that out of the way, let’s look at an interesting example. We know that slicing is more efficient than arrays, but is it?

func array() [1000]int {
	var x [1000]int
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}

func slice() []int {
	x := make([]int, 1000)
	for i := 0; i < len(x); i++ {
		x[i] = i
	}
	return x
}

func BenchmarkArray(b *testing.B) {
	for i := 0; i < b.N; i++ {
		array()
	}
}

func BenchmarkSlice(b *testing.B) {
	for i := 0; i < b.N; i++ {
		slice()
	}
}
Copy the code

The result is as follows

go test -bench . -benchmem -gcflags "-N -l -m"BenchmarkArray-4 30000000 52.8 NS /op 0 B/ OP 0 ALlocs /op BenchmarkSlice-4 20000000 82.4 ns/ OP 160 B/op 1 Allocs /opCopy the code

As you can see, we don’t have to use slicing instead of arrays because slicing the underlying array may allocate memory on the heap, and the cost of copying a small array across the stack is not necessarily greater than slicing.

3, the interface

Interface is a feature that we use a lot in Go, which is very useful, but because the interface type is at compile time, it can be difficult to determine its specific type at compile time, which leads to escape. Let’s take the simplest example

func main() {
	s := "abc"
	fmt.Println(s)
}
Copy the code

This code will escape because the method FMT.Println takes an interface parameter. But this is just science, after all, the benefits of interface outweigh its disadvantages

5. References

Segment.com/blog/alloca…