This is the second day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021

preface

Hi Coder, I’m CoderStar!

You’ve probably asked yourself the difference between a struct and a class at least once during Swift development, and if you haven’t asked yourself, your interviewer probably has. Probably the biggest difference in the answer to this question is that one is a value type and the other is a reference type, and we’ll talk more about that today.

So before we get into value types and reference types, let’s review the difference between structs and classes.

class & struct

In Swift, there are not many core differences between classes and structs, many of which are inherent in the difference between value types and reference types.

  • classYou can inherit,structCannot inherit (of coursestructYou can useprotocolTo achieve an inheritance-like effect. ; The differences affected by this are:
    • structThe distribution methods of the method are all direct distribution, andclassAccording to the actual situation, there are various distribution modes. Please refer toSwift distribution mechanism;
  • classYou need to define your own constructor,structDefault generation;

    Struct generated by default constructor must include all the members of the parameters, only when all parameters for selection, can be directly without incoming parameters directly simple structure and properties of the class must have a default value, otherwise the compiler error, can be declared by assignment or constructor assignment to attribute to set the default value in one of two ways.

  • classIs a reference type,structIs a value type; The differences affected by this are:
    • structChanging its properties is affected by the let modifier and cannot be changed,classBe unaffected by;
    • structMethod that needs to modify its own properties (noinitMethod), the method requires a prefix modifiermutating;
    • structBecause of the value type, it is automatically thread safe and there is no risk of memory leaks from circular references;
    • .
    • Read more in the next chapter
  • .

In Swift, many of the base types, such as String, Int, and so on, are defined using structs. As for how to choose the two, Apple also gives the differences and official advice in some official documents.

  • choosing_between_structures_and_classes
  • Value and Reference Types
  • ClassesAndStructures

From Choosing_Between_structures_and_classes

When adding new data types to your app, consider the following tips to help you make the right choices.

  • Structure is used by default.
  • Use classes when objective-C interoperability is required.
  • Use classes when you need to control the identity of modeling data.
  • Match the structure with the protocol and adopt the behavior by sharing the implementation.

Value type & reference type

What is the difference between a value type and a reference type in Swift?

  • Storage mode and location: Most value types are stored on the stack and most reference types are stored on the heap;
  • Memory: Value types do not have reference counts and do not have problems with circular references and memory leaks;
  • Thread-safe: Value types are naturally thread-safe, while reference types need to be locked by developers.
  • Copy mode: value type copies content, while reference type copies Pointers, which is called deep copy and shallow copy in a sense.

In Swift, the value types include enum, tuple, and closure/func in addition to class.

Storage mode and location

The ‘heap’ and ‘stack’ mentioned above are the different memory Spaces in which a program is running.

As for the principle of heap and stack storage, meituan’s [basic skills] in-depth analysis of Swift performance optimization gives details, so I will not repeat them here, and probably say the conclusion.

The default value type is stored in the stack area, the stack area memory is continuous, through out of the stack for allocation and destruction, fast, and each thread has its own stack space, so there is no need to consider thread safety issues; The value can be retrieved once the stored content is accessed.

A reference type that stores only Pointers to objects in the stack, to which memory is allocated in the heap. The heap calls functions (MALLOC,FREE) to allocate/FREE memory dynamically, which takes some time, and because the heap space is shared by all threads, use it with thread-safety in mind. To access the contents of the storage, you need to access the memory twice, the first time to get a pointer, and the second time to get the actual data.

On 64-bit systems, iOS adds Tagged Pointer optimization, which stores values directly in Pointers, such as NSNumber and NSString structures.

From the description, the most important conclusion we reached was that it was faster to use value types than reference types. For specific technical specifications, see Why-choo-struct-over-class, and a test item, StructVsClassPerformance.

All classes are stored on the heap, and all structs are stored on the stack. That is the focus of this article. This is fine for the vast majority of cases, but there are always special cases.

Before we read more, let’s look at how to determine whether an object is allocated on the stack or on the heap. The answer to this question can be found in sil.rst. The SIL file generated by Swift compilation contains dispatching instructions. The commands related to memory allocation include alloc-stack and alloc-box commands to help us solve this problem. Simply speaking, the former is the instruction for sorting memory on the stack, while the latter is the instruction for allocating tasks on the heap.

The reference type on the stack

The cost of allocation and deallocation on the stack is much lower than allocation and deallocation on the heap, so sometimes the compiler may promote reference types to also be stored on the stack. This process actually occurs during the SIL optimization phase, which is officially called Memory promotion. Guaranteed Optimization and Diagnostic Passes support this claim.

Memory promotion is implemented as two optimization phases, the first of which performs capture analysis to promote alloc_box instructions to alloc_stack, and the second of which promotes non-address-exposed alloc_stack instructions to SSA registers.

In the SIL phase, memory will be upgraded to the stack memory, and the stack memory will be upgraded to the SSA register memory.

The optimized part of the code can be seen in allocboxtostack.cpp.

The value type on the heap

There is a passage in the Swift Progression book (which appeared in version 3.0 and was removed from version 5.0) :

Swift structures are typically stored on the stack, not on the heap. But this is actually an optimization: by default, the structure is stored on the heap, but most of the time, this optimization takes effect and the structure is stored on the stack. When a structure variable is closed by a function, the optimization is no longer in effect and the structure is stored on the heap.

Some of you will be a little confused by this sentence, why the default structure is stored on the heap and then stored on the stack when optimized. Let’s look at the associated SIL files generated by struct compilation.

struct Test {}
Copy the code

This is a very simple struct, so simple that there are no attributes, we use the swiftc command to generate SIL file, command as follows:

swiftc Test.swift -emit-silgen | xcrun swift-demangle > TestSILGen.sil

This means generating Raw SIL, or native SIL files, without any optimization or processing. For more commands, see the previous article on iOS compilation.

The generated SIL file contains the following contents:

sil_stage raw

import Builtin
import Swift
import SwiftShims

struct Test {
  init(a)
}

// main
sil [ossa] @main : $@convention(c) (Int32.UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8- > > > >)Int32 {
bb0(%0 : $Int32.%1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) :%2 = integer_literal $Builtin.Int32.0          // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32) / /user: % 4return %3 : $Int32                              // id: % 4} / /end sil function 'main'

// Test.init(a)sil hidden [ossa] @$s4main4TestVACycfC : $@convention(method) (@thin Test.Type) - >Test {
// %0 "$metatype"
bb0(%0 : $@thin Test.Type) :%1 = alloc_box ${ var Test }, let, name "self"  // user: %2
  %2 = mark_uninitialized [rootself] %1 : ${ var Test } // users: %5, %3
  %3 = project_box %2 : ${ var Test }, 0          // user: %4
  %4 = load [trivial] %3 : $*Test                 // user: %6
  destroy_value %2 : ${ var Test }                // id: %5
  return %4 : $Test                               // id: %6
} // end sil function '$s4main4TestVACycfC'
Copy the code

We can clearly see the word alloc_box.

We then use the command to generate the optimized SIL file as follows:

swiftc Test.swift -emit-sil | xcrun swift-demangle > TestSIL.sil

sil_stage canonical

import Builtin
import Swift
import SwiftShims

struct Test {
  init(a)
}

// main
sil @main : $@convention(c) (Int32.UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8- > > > >)Int32 {
bb0(%0 : $Int32.%1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) :%2 = integer_literal $Builtin.Int32.0          // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32) / /user: % 4return %3 : $Int32                              // id: % 4} / /end sil function 'main'

// Test.init(a)sil hidden @$s4main4TestVACycfC : $@convention(method) (@thin Test.Type) - >Test {
// %0 "$metatype"
bb0(%0 : $@thin Test.Type) :%1 = alloc_stack $Test.let, name "self"        // user: %3
  %2 = struct $Test(a) / /user: % 4dealloc_stackThe % 1:$*Test                       // id: % 3return %2 : $Test                               // id: % 4} / /end sil function '$s4main4TestVACycfC'
Copy the code

We obviously see the word alloc_stack.

I’m sure you’ve seen what’s going on. Structs actually use heap instructions to generate raw SIL files, and then modify instructions during SIL optimization depending on the code context to determine if they can be optimized on the stack. That can be optimized on the stack in most cases. This process involves the aforementioned allocboxTostack.cpp file.

Of course, there must be a few other cases. Such as:

func uniqueIntegerProvider(a)- > () - >Int {
    // I is an Int, which is essentially a structure
    var i = 0
    return {
        i+ =1
        return i
    }
}
Copy the code

The two SIL files generated for this code have the following core parts:

Before optimization:

// uniqueIntegerProvider()
sil hidden [ossa] @main.uniqueIntegerProvider() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed() - >Int {
bb0:
  %0 = alloc_box ${ var Int }, var, name "i"      // users: %11, %8, %1
  %1 = project_box %0 : ${ var Int }, 0           // users: %9, %6
  %2 = integer_literal $Builtin.IntLiteral.0     // user: %5
  %3 = metatype $@thin Int.Type                   // user: %5
  // function_ref Int.init(_builtinIntegerLiteral:)
  %4 = function_ref @Swift.Int.init(_builtinIntegerLiteral: Builtin.IntLiteral) - >Swift.Int : $@convention(method) (Builtin.IntLiteral.@thin Int.Type) - >Int // user: %5
  %5 = apply %4(%2.%3) : $@convention(method) (Builtin.IntLiteral.@thin Int.Type) - >Int // user: %6
  store %5 to [trivial] %1 : $*Int                // id: %6
  // function_ref closure #1 in uniqueIntegerProvider()
  %7 = function_ref @closure #1() - >Swift.Int in main.uniqueIntegerProvider() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int}) - >Int // user: %10
  %8 = copy_value %0 : ${ var Int }               // user: %10
  mark_function_escape %1 : $*Int                 // id: %9
  %10 = partial_apply [callee_guaranteed] %7(%8) : $@convention(thin) (@guaranteed { var Int}) - >Int // user: %12
  destroy_value %0 : ${ var Int }                 // id: %11
  return %10 : $@callee_guaranteed() - >Int      // id: %12
} // end sil function 'main.uniqueIntegerProvider() -> () -> Swift.Int'
Copy the code

After the optimization:

// uniqueIntegerProvider()
sil hidden @main.uniqueIntegerProvider() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed() - >Int {
bb0:
  %0 = alloc_box ${ var Int }, var, name "i"      // users: %8, %7, %6, %1
  %1 = project_box %0 : ${ var Int }, 0           // user: %4
  %2 = integer_literal $Builtin.Int64.0          // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64) / /user: % 4store% 3toThe % 1:$*Int                          // id/ / : % 4function_ref closure# 1in uniqueIntegerProvider5 = () %function_ref @closure# 1 () - >Swift.Int in main.uniqueIntegerProvider() - > () - >Swift.Int : $@convention(thin) (@guaranteed { var Int}) - >Int // user: %7
  strong_retain %0 : ${ var Int }                 // id: %6
  %7 = partial_apply [callee_guaranteed] %5(%0) : $@convention(thin) (@guaranteed { var Int}) - >Int // user: %9
  strong_release %0 : ${ var Int }                // id: %8
  return %7 : $@callee_guaranteed() - >Int       // id: %9
} // end sil function 'main.uniqueIntegerProvider() -> () -> Swift.Int'
Copy the code

It is obvious that the alloc_box instruction is used both before and after optimization, which means that the variable I is stored on the heap. The reason for this is that the variable I is closed by the function, so even if you exit the scope, you still have to keep I. Of course, this is only one case, there will be other cases.

Summary: So all in SwiftclassIt’s all stored on the heap, all of itstructAll on the stack is problematic, but most of the time it’s true, and some of the time it’s going to mess with you, depending on the context of the structure and the SIL optimization and so on.

Copy the way

The reference type, when copied, actually copies only the pointer to the object stored on the stack; The value type copies the actual value.

For value type Copy, Swift has a copy-on-write (COPy-on-write) optimization mechanism, that is, the real Copy will be carried out only when the value type changes after the assignment. If there is no change, the two share the same memory address.

Apple provides an example in OptimizationTips. The code is very simple, and I believe you can understand it in a moment.

There are some other optimization methods provided by Apple in this document, such as reducing dynamic distribution and so on. Enjoy is recommended.

final class Ref<T> {
  var val: T
  init(_ v: T) {val = v}
}

struct Box<T> {
    var ref: Ref<T>
    init(_ x: T) { ref = Ref(x) }

    var value: T {
        get { return ref.val }
        set {
          // Check whether the current object has only one reference. If not, copy it
          if !isKnownUniquelyReferenced(&ref) {
            ref = Ref(newValue)
            return
          }
          ref.val = newValue
        }
    }
}
Copy the code

In the Swift standard library, String, Array, Dictionary, and Set implement COW by default. For custom objects, we need to implement COW by ourselves.

The last

In the process of writing the local article, I checked some documents under the Swift open source warehouse Docs directory and learned a lot. I also suggest readers and students enjoy!

Try harder!

Let’s be CoderStar!

For more information

  • IOS Swift5: Structs and Classes
  • Why Choose Struct Over Class?
  • Memory Management and Performance of Value Types
  • Value Types and Reference Types in Swift
  • why-choose-struct-over-class
  • reference-vs-value-types-in-swift

It is very important to have a technical circle and a group of like-minded people, come to my technical public account, here only talk about technical dry goods.

Wechat official account: CoderStar