Question

Not long ago I read an article, mew god value type and reference type, when reading there is a conclusion that the value type is copied when the value type content changes… I wanted to write it down at this point, but on second thought, practice makes sense, so I started with this question: When exactly is a value type assigned? Some research and practice led to this series of articles…

Answer

I wrote the following example in iOS Playground that initializes an Int String Struct Array and immediately assigns:

struct Me {
    let age: Int = 22            / / 8
    let height: Double = 180.0   / / 8
    let name: String = "XiangHui"/ / 24
    var hasGirlFriend: Bool?     / / 1
}
    
var a = 134
var cpa = a
    
var b = "JoJo"
var cpb = b
    
var me = Me(a)var secondMe = me
    
var likes = ["comdy"."animation"."movies"]
var cpLikes = likes
Copy the code

And then a swift pointer method is used to print the memory address of the value type:

    withUnsafeBytes(of: &T, { bytes in
        print("T: \(bytes)")})Copy the code

The address of the copied variable should be the same as that of the original variable if the assignment is made when the value type is changed.

a: UnsafeRawBufferPointer(start: 0x00007ffee3500ef8.count: 8)
cpa: UnsafeRawBufferPointer(start: 0x00007ffee3500f00.count: 8)
b: UnsafeRawBufferPointer(start: 0x00007ffee3500f18.count: 24)
cpb: UnsafeRawBufferPointer(start: 0x00007ffee3500ee0.count: 24)
me: UnsafeRawBufferPointer(start: 0x00007ffee3500fa8.count: 41)
secondMe: UnsafeRawBufferPointer(start: 0x00007ffee3500f40.count: 41)
likes: UnsafeRawBufferPointer(start: 0x00007ffee3500f30.count: 8)
cpliles: UnsafeRawBufferPointer(start: 0x00007ffee3500f08.count: 8)
Copy the code

Obviously, values of value types are not copied when they change, but when they are assigned! This conclusion is clearly questionable! Int, Double, String, Struct, etc.) are copied at assignment time. Why? Because copying on write is sometimes more expensive than copying directly for primitive types! In the case of collection types, of course, my example above is an Array, which copies a reference directly. Collection types (Array, Dictionary, Set) are copied not at assignment time, but at write time!

As directed by Meow God, I used the following method to print the address of the array:

func address<T: AnyObject>(of object: T) -> String {
    let addr = unsafeBitCast(object, to: Int.self)
    return String(format: "%p", addr)
}
    
func address(of object: UnsafeRawPointer) -> String {
    let addr = Int(bitPattern: object)
    return String(format: "%p", addr)
}

var likes = ["animation"."movies"."comdy"]
var cpLikes = likes

print("Array")
print(address(of: &likes))
print(address(of: &cpLikes))

cpLikes.removeLast()

print(address(of: &cpLikes))
Copy the code

The final output is:

Array
0x6080000d4370
0x6080000d4370
0x6080000d5480
Copy the code

Cplikes: cplikes: cplikes: cplikes: cplikes: cplikes: cplikes: cplikes: cplikes: cplikes

Here are the words of Meow God:

Deep in

When this problem is solved, a new question arises:

  • How exactly is memory allocated in the system?
  • How exactly is the data stored in the stack?
  • How is the data stored on the heap?

In response to my three simple but broad questions, I did a lot of reading and practice, and then came to the following thoughts and conclusions:

Concept

Before getting into more abstract memory theory, there are a few basic concepts to understand. The first is the operational memory region. The memory region we use in our program is shown in green:

In this area we can briefly divide into three areas: heap, stack and global area. On a modern CPU, every time it reads data, it reads one Word, in 64-bit, which is 8 bytes.

  • Stack stores method calls; Local variable (Method Invocation; Locial variables)
  • Heap stores objects (all objects!)
  • Global stores Global variables; Constant; Code section

When you declare a primitive type as an object property in a class, it’s actually allocated on the heap. In other words, when you declare a primitive type as an object property in a class, it’s allocated on the heap

class Test {
	let a = 4 // Allocate to the heap
	func printMyName(a) {
		let myName = "JoJo" // Assign on the stack
		print("\(myName)")}}Copy the code

MemoryLayout

 / / value types
 MemoryLayout<Int>.size           / / 8
 MemoryLayout<Int>.alignment      / / 8
 MemoryLayout<Int>.stride         / / 8

 MemoryLayout<String>.size        / / 24
 MemoryLayout<String>.alignment   / / 8
 MemoryLayout<String>.stride      / / 24

 // Reference type T
 MemoryLayout<T>.size             / / 8
 MemoryLayout<T>.alignment        / / 8
 MemoryLayout<T>.stride           / / 8


 // Pointer type
 MemoryLayout<unsafeMutablePointer<T>>.size           / / 8
 MemoryLayout<unsafeMutablePointer<T>>.alignment      / / 8
 MemoryLayout<unsafeMutablePointer<T> >.stride         / / 8

 MemoryLayout<unsafeMutableBufferPointer<T>>.size           / / 16
 MemoryLayout<unsafeMutableBufferPointer<T>>.alignment      / / 16
 MemoryLayout<unsafeMutableBufferPointer<T> >.stride         / / 16
Copy the code

MemoryLayout

is a generic that has three properties to get the memory allocation of a specific Type: size indicates how many bytes are actually used for the Type; Alignment Indicates how many bytes the type must be aligned (8, for example, means that the address’s starting address is divisible by 8). The stride indicates how many bytes it takes to get from start to finish. The size and stride of the basic type in Swift are the same in memory (optional: Double? Nine bytes were actually used, but 16 bytes were needed.) Benefits of memory alignment The benefits of memory alignment are described in detail here, mainly for speed.

Struct Stack Memory

An example of stack memory allocation:

struct Me {
    let age: Int = 22                    
    let height: Double? = 180.0         
    let name: String = "XiangHui"        
    var hasGirlFriend: Bool = false      
 }
 //MemoryLayout
      
       .size 9
      ?>
 //MemoryLayout
      
       .alignment 8
      ?>
 //MemoryLayout
      
       .stride 16
      ?>
 
 class MyClass {
	func test(a) {
		var me = Me(a)print(me)
	}
 }
 
 let myClass = MyClass()
 myclass.test()
 
Copy the code

To make a breakpoint in the method to use the debugger to print memory in the stack, you can guess before you do that, Int takes 8 bytes, Double? Although the size was 9 bytes, the stride was 16 bytes, so it took up 16 bytes, String took up 24 bytes, and finally Bool took up 8 bytes, altogether 8 + 16 + 24 + 8 = 56 bytes, that is to say, this structure took up 56 bytes of memory on the stack. Print the following:

(lldb) po MemoryLayout.size(ofValue: me)
49

(lldb) po MemoryLayout.stride(ofValue: me)
56
Copy the code

Why is the size 49? Since size is the memory occupied from the beginning to the actual end, that is, the size and stride of Bool are both 1 byte. In this case, there are still 7 bytes of unused memory in Word, so the actual size is 49 bytes. Look again at the detailed address print:

(lldb) frame variable -L me
0x00007ffeea2cda50: (MemorySwiftProject.Me) me = {
0x00007ffeea2cda50:   age = 22
0x00007ffeea2cda58:   height = 180
0x00007ffeea2cda68:   name = "XiangHui"
0x00007ffeea2cda80:   hasGirlFriend = false
}
Copy the code

Address increment from bottom of stack all the way up (Boolsize = 1)

If stack storage is so simple in a structure, what if there are declared reference types in the structure? The result is that the reference type takes up one Word (8 bytes of pointer space); So what if I have a method body inside a structure? The conclusion is that the structure does not occupy memory even if there are methods to implement it, which will be solved in the next article! But I can make a tentative guess that it has something to do with static calls to methods, that is, compiler compilations.

// The method body does not occupy memory in the structure
struct Test {
    let a = 1
    func test01(a){}}let test = Test(a)MemoryLayout.size(ofValue: test)  / / 8
    
struct Test2 {
    func test01(a){}}let test2 = Test2(a)MemoryLayout.size(ofValue: test2) / / 0
Copy the code

Method Stack Memory

I was supposed to get to know the heap, but I found something worth talking about when the method call breakpoint was output, so I decided to talk about the memory in the method stack! The method scheduling, in fact, is a method on the stack, the top of the stack after the execution of the stack, and then the new top of the stack after the execution of the stack. If you’re in the middle of a recursive method, this looks interesting. But instead of talking about method scheduling, let’s talk about how memory is allocated inside a method when it executes. First of all, memory is allocated on the stack during execution!

struct Me {
	let age: Int = 22              / / 8
	let height: Double? = 180.0    // size: 9 stride: 16
	let name: String = "XiangHui"  / / 24
	let a = MemoryClass(a)/ / 8
	let hasGirlFriend = false      / / 1
 }
  
 // MemoryLayout
      
       . Stride 64 (8 + 16 + 24 + 8 + 8 = 64)
      

func test(a) {
	var number = 134        // stride: 8
	var name = "JoJo"       // stride: 8
	var me = Me(a)// stride: 64
	var likes = ["comdy"."animation"."movies"] // stride: 8
	
    withUnsafeBytes(of: &number, { bytes in
        print("number: \(bytes)")
    })
    
    withUnsafeBytes(of: &name, { bytes in
        print("name: \(bytes)")
    })
    
    withUnsafeBytes(of: &me, { bytes in
        print("me: \(bytes)")
    })
    
    withUnsafeBytes(of: &likes, { bytes in
        print("likes: \(bytes)")})}Copy the code

First of all, why is the stride for the structure 64 bytes? Now, is it clear from all of this, that we have an Int Double in this structure? String Class Bool The type is 8 + 16 + 24 + 8 + 8 = 64 bytes. One more little detail why is the stride for the array “likes” 8 bytes? Because you’re still allocating an array pointer on the stack, which points to another chunk of memory, how is the actual array allocated? Leave it to the next article. The code output is as follows:

0x00007ffee46f2ac0: (Int) number = 134
0x00007ffee46f2aa8: (String) name = "JoJo"
0x00007ffee46f2a68: (MemorySwiftProject.Me) me = {
0x00007ffee46f2a68:   age = 22
0x00007ffee46f2a70:   height = 180
0x00007ffee46f2a80:   name = "XiangHui"
scalar:   a = 0x000060c00001de10 {}  // The address of the reference type in the heap
0x00007ffee46f2aa0:   hasGirlFriend = false
}
0x00007ffee46f2a20: ((String]) likes = 3 values {
0x00007ffc9d780500:   [0] = "comdy"
0x00007ffc9d721710:   [1] = "animation"
0x00007ffc9d6443d0:   [2] = "movies"
}
Copy the code

With the withUnsafeBytes(of: &t) {} method, count prints Size. First of all, it is worth noting that the output memory is successively decreasing, that is, the element at the bottom of the stack has a higher memory address, and the element at the bottom of the stack has a smaller address, so the structure is as follows:

And then I change the size of the structure and it turns out that this extra chunk of memory on the method stack is still the same size as the instance of the structure. Why is that? Why is there an extra block of memory allocated to a structure in the method stack that is as large as its size? Keep that question as well!

Heap Memory

After we look at the memory on the stack, the memory on the heap is the same, as shown in the following example:

class MemoryClass {
    static let name = "Naruto"
    let ninjutsu = "rasengan"   / / 24
    let test = TestClass(a)/ / 8
    let age = 22                / / 8
    
    func beatSomeone(a) {
        let a = ninjutsu + ninjutsu
        print(a)
    }
}

func heapTest(a) {
    let myClass = MemoryClass(a)print(myClass)
}

heapTest()

Copy the code

Making a breakpoint in the heapTest() method yields the following output:

(lldb) frame variable -L myClass
scalar: (MemorySwiftProject.MemoryClass) myClass = 0x000060400027ca80 {
0x000060400027ca90:   ninjutsu = "rasengan"
scalar:   test = 0x00006040004456d0 {
0x00006040004456e0:     name = "Hui"
  }
0x000060400027cab0:   age = 22
}
(lldb) po malloc_size(Unmanaged.passUnretained(myClass as AnyObject).toOpaque())
64
Copy the code

According to the output results, the following conclusions can be drawn:

Each memory allocation on the heap

Why start with the last word? Each time a new object is created, the attribute of object is allocated from the 16th byte, so the first two Words of each object must store some other information. Because of the previous OC basis, we can guess that it should store information such as an ISA pointer. But the last eight bytes don’t show up, so the way I’m going to test this is I’m going to increment MyClass and increment Bool member variables, and I’m going to start predicting that every time I increment the number of bytes of Word, The result is that I get 64, 80, 96 memory sizes each time using malloc_size(UnsafeRawPointer). Each time a multiple of 16 bytes is allocated, the size of the heap-allocated variable should be 64 bytes. The result is 64 bytes. If you add a member variable of type Bool, the memory size is 80 bytes, as you guessed. So the bottom line: at least on iOS 64, the heap allocates memory for objects in multiples of 16 bytes each time

class MemoryClass {
    static let name = "Naruto"
    let ninjutsu = "rasengan"   / / 24
    let test = TestClass(a)/ / 8
    let age = 22                / / 8
    
    let age2 = 22               / / 8
}
// malloc_size(Unmanaged.passUnretained(myClass as AnyObject).toOpaque()) 64

class MemoryClass {
    static let name = "Naruto"
    let ninjutsu = "rasengan"   / / 24
    let test = TestClass(a)/ / 8
    let age = 22                / / 8
    
    let age2 = 22               / / 8
    let a = false               // 1 (only one more Bool)
}
// malloc_size(Unmanaged.passUnretained(myClass as AnyObject).toOpaque()) 80

Copy the code

Disappearing type variable

The static name attribute does not occur in memory on the heap when the class instance is initialized. This problem is explained in the first diagram. The entire memory area can be divided into stacks. The heap area; Global variable, static variable area; The constant area; Code section. Here’s what I drew:

Type variables are not allocated on the heap, but are allocated in the Global Data section at compile time, which is why there is no memory allocated for type variables on the heap.

What is the first Word of the object?

The isa pointer in OC points to its class. The result is the same. This article has a very clear explanation: In C++, the isa pointer to an object points to a VTable, which is just a simple list of methods. In Swift, it’s a little more complicated. In fact, all Swift classes are Objective-C classes, and it’s more intuitive if you add @obj or inherit NSObject. But even a pure Swift class is still essentially an Objective-C class. I specifically asked @Mikeash on Twitter about this, and he replied:

Yes, they subclass a hidden SwiftObject class.

So the first word is essentially an ISA pointer to Class; But to be more precise, not necessarily isa Pointers, sometimes ISA Pointers and other things, such as other objects associated with the current object (which also needs to be cleaned up when the current object is released)… But in a general sense we can understand it as isa pointer.

We could do an experiment and change the isa pointer to the current object to point to another type. What would happen?

class Cat {
    var name = "cat"
    
    func bark(a) {
        print("maow")}// Mutable primitive pointer (current instance pointer)
    func headerPointerOfClass(a) -> UnsafeMutableRawPointer {
        return Unmanaged.passUnretained(self as AnyObject).toOpaque()
    }
}

class Dog {
    var name = "dog"
    
    func bark(a) {
        print("wangwang")}// Mutable primitive pointer (current instance pointer)
    func headerPointerOfClass(a) -> UnsafeMutableRawPointer{
        return Unmanaged.passUnretained(self as AnyObject).toOpaque()
    }
}

    func heapTest(a) {
        let cat = Cat(a)let dog = Dog(a)let catPointer = cat.headerPointerOfClass()
        let dogPointer = dog.headerPointerOfClass()
        
        catPointer.advanced(by: 0)
            .bindMemory(to: Dog.self, capacity: 1)
            .initialize(to: dogPointer.assumingMemoryBound(to: Dog.self).pointee, count: 1)
        
        cat.bark()  // wangwang
    }
Copy the code

Because the ISA pointer of cat instance points to the type Dog, methods in Swift are statically distributed, and only dynamic keyword is dynamically distributed. In this case, the first word of CAT refers to Dog, and it directly calls the first method in the method list. The question is: What if bark() was preceded by another method like fuck()? The answer is to implement Fuck ()! Because it is not dynamic to find the method of execution, just use offset to find the corresponding method of execution! The default swift class is static distribution, according to the offset to find the corresponding method.

What is the structure of the Class that isa points to? The Swift class is essentially an OC class, so let’s look at the OC class definition. Objective-c class definition is open source, so let’s look at the following:

	Class isa
	Class super_class
	const char *name
	long version
	long info
	long instance_size
	struct objc_ivar_list *ivars
	struct objc_method_list天安门事件methodLists
	struct objc_cache *cache
	struct objc_protocol_list *protocols
Copy the code

The in-memory Class stores the Class name; Its instance size; Attribute list; Method list; Protocol list; Caching (speeds up method scheduling), etc… But, after all, this is an Objective-C Class structure, and the fact that Swift Class has everything in objective-C Class and it adds a few things, But in essence,Swift Class is just an Objective-C Class with a lot more

	uint32_t flags;
	uint32_t instanceAddressOffset;
	uint32_t instanceSize;
	uint16_t instanceAlignMask;
	uint16_t reserved;
	uint32_t classSize;
	uint32_t classAddressOffset;
	void *description;
Copy the code

The second Word in the object

Well, the first Word stores simply a pointer to Class, but what about the second Word? In fact, the second Word store is reference count, in Swift is used to manage the object’s life cycle reference count, Swift has two kinds of reference count, one is strong reference, one is weak reference, and both in this Word, each kind of reference count size 31 bytes! Then the next picture should be complete:

conclusion

In fact, this article down or to learn a lot of things, next I will go through a context:

  • First, when exactly are value types copied: primitive data types copied at assignment time, and collection types (Array, Set, Dictionary) copied at write time
  • Then introduce some basic concepts about memory: MemoryLayout, three properties, etc
  • Through some examples to understand the storage structure of Struct in the stack, pay attention to the bottom of the stack position and address increase direction
  • Then it explains the storage structure of Method in the Method stack. The bottom of the stack is at the top, and the address is decreasing from the bottom of the stack to the top of the stack. If there is a structure in the Method stack, it can also conform to the storage structure
  • Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap Heap And when allocating memory for objects, memory is incremented by multiples of 16 bytes.

However, it also leaves itself with some questions, which will be answered in the next article:

  1. How is Swift’s collection type memory allocated?
  2. There is no storage space for methods in the Swift structure. why?
  3. How are methods in a class scheduled (statically and dynamically)?
  4. How are protocols stored? What about the structure succession agreement? What about the class inheritance agreement?
  5. If there is a structure in the stack, there is more space to match the size of the structure. Why is this?

Reference article:

Unsafe Swift: (1) Swift advanced Memory model And method scheduling Printing A variable memory address in swift

Finally, attach my Blog address, if you feel good to pay attention to my nuggets, or often visit my Blog~~