首页 > 其他分享 >Swift 值类型和引用类型深度对比

Swift 值类型和引用类型深度对比

时间:2023-09-12 10:12:13浏览次数:42  
标签:代码 复制 引用 类型 Swift 内存

  • 值类型和引用类型的概念
  • 他们在内存中时如何存储的?
  • 值类型和引用类型分别有哪些表现?
  • 如果将两者混合使用会怎样?
  • 什么时候使用值类型,什么时候使用引用类型?

定义值类型和引用类型

Swift有三种声明类型的方式:classstructenum。 它们可以分为值类型(struct和enum)和引用类型(class)。 它们在内存中的存储方式不同决定它们之间的区别:

  • 值类型存储在栈区。 每个值类型变量都有其自己的数据副本,并且对一个变量的操作不会影响另一个变量。

  • 引用类型存储在其他位置(堆区),我们在内存中有一个指向该位置的引用。 引用类型的变量可以指向相同类型的数据。 因此,对一个变量进行的操作会影响另一变量所指向的数据。

从性能出发

导致Swift结构体(和枚举)与类的性能差异的三个维度是:

  1. 复制消耗的成本;
  2. 创建和销毁时花费成本;
  3. 引用计数造成的成本;

下面我们可能会经常讨论内存,因此请确保你了解什么是内存以及内存是如何存储数据的。

内存段

内存可以理解为字节的集合。字节在内存中有序排列,每个字节都有自己的地址。 所有地址的范围称为地址空间。

iOS应用程序的地址空间在逻辑上由四个部分组成:代码段,数据段,栈区和堆区:

1

代码段包含构成App可执行代码的机器指令。 它是由编译器通过将Swift代码转换为机器代码而产生的。 该段是只读的,并占用固定不变的空间。

数据段存储Swift静态变量,常量和类型元数据。 程序启动时所有需要初始值的全局数据都在此处。

栈区存储临时数据:方法的参数和局部变量。 每次我们调用一个方法时,都会在栈上分配一块新的内存。 该方法退出时,将释放该内存。 除特殊情况(下面会讲),所有Swift值类型都在此处。

堆区存储具有生存期的对象。 这些都是Swift引用类型,还有一些值类型的情况。 堆和栈朝着彼此增长堆区的分配一般按照地址从小到大进行,而栈区的分配一般按照地址从大到小进行分配

一般Swift值类型在栈上分配。 引用类型在堆上分配。

现在,我们已经研究了内存段的工作原理,让我们来看一下内存中的内容是如何存储的。

堆与栈分配的成本

栈区内存分配和销毁的工作原理与数据结构中的栈相同。 你只能从栈顶压栈或出栈。 指向栈顶的指针足以实现这两个操作。 因此,栈指针可以腾出空间来分配其他更多的内存。 当函数执行完退出时,我们将栈指针增加到调用此方法之前的位置。(为什么增加才能回到调用之前的地址,刚说了栈是从大到小进行分配的)

栈分配和释放的成本相当于整数复制的成本【WWDC-416】

堆分配过程涉及的东西很多。 我们必须搜索堆区以找到适合它大小的空内存块。 我们还必须同步堆,因为多个线程可能同时在其中分配内存。 为了从堆中释放内存,我们必须将该内存重新插入适当的位置。

堆分配和释放的成本比栈要大得多

通常值类型和引用类型分别在栈和堆上分配,但是这个规则有一些例外情况需要注意。

Swift 引用类型关于栈的优化

当引用类型的大小固定或可以预测生存期的时候,Swift编译器可能会将引用类型分配到栈中。 这种优化发生在SIL生成阶段。

Swift中间语言(SIL)是Swift特有的高级中间语言,用于对Swift代码的进一步分析和优化。

下面是我通过阅读Swift编译器源代码发现的示例。

Swift值类型 -- 装箱

Swift编译器可以将值类型装箱后放到堆上。 我通过阅读Swift编译器源代码来列出了会出现的几种情况。

在以下情况,值类型会被装箱:

  1. 当值类型遵循了某个协议

    当值类型遵循了某个协议,且存储在existential(存在性)容器中超过3个机器字长时,除分配成本外,还会产生额外的开销。

    Existential(存在性)容器是用于存储运行时未知类型的值的一种通用容器。 较小的值类型可以内嵌在存在性(existential)容器中。 较大的分配在堆上, 它们的引用存储在存在性(existential)容器缓冲区内。 此类值的生存期由值见证表(Value Witness Table)管理。 当调用协议方法时会产生引用计数和几个间接级别的开销。

    值见证表(Value Witness Table): 一种运行时结构,用于描述如何对未知值进行“ assign”,“ copy”和“ destroy”基本操作。 (例如,复制此值是否需要保留?)

    详解见官方:github.com/apple/swift…

    让我们看看生成的SIL代码他们是如何装箱的。 我们声明一个协议Bar和一个符合它的结构体 Baz

    复制代码`protocol Bar {}
    struct Baz: Bar {}` 
    

    Swift文件转换成SIL语言的命令是:

    复制代码`swiftc -emit-silgen -O main.swift` 
    

    输出显示self被装在init()中:

    复制代码`protocol Bar {
    }
    struct Baz : Bar {
      init()
    }
    // Baz.init()
    sil hidden [ossa] @$s6boxing3BazVACycfC : $@convention(method) (@thin Baz.Type) -> Baz {
    bb0(%0 : $@thin Baz.Type):
      %1 = alloc_box ${ var Baz }, var, name "self"   // user: %2
      ...
    }` 
    
  2. 值类型和引用类型混合时

    结构体中包含类,类中包含结构的情况很常见:

    复制代码`// Class inside a struct
    class A {}
    struct B { 
      let a = A() 
    }
    
    // Struct inside a class
    struct C {}
    class D {
        let c = C()
    }` 
    

    SIL输出显示,在两种情况下,结构BC都分配在堆上:

    复制代码`// B.init()
    sil hidden [ossa] @$s6boxing1BVACycfC : $@convention(method) (@thin B.Type) -> @owned B {
    bb0(%0 : $@thin B.Type):
      %1 = alloc_box ${ var B }, var, name "self"     // user: %2
      ...
    }
    
    // C.init()
    sil hidden [ossa] @$s6boxing1CVACycfC : $@convention(method) (@thin C.Type) -> C {
    bb0(%0 : $@thin C.Type):
      %1 = alloc_box ${ var C }, var, name "self"     // user: %2
      ...
    }` 
    
  3. 带有泛型的值类型。

    让我们声明一个带泛型的结构体:

    复制代码`struct Bas<T> {
        var x: T
    
        init(xx: T) {
            x = xx
        }
    }` 
    

    SIL输出显示self被装在init(xx :)中:

    复制代码`// Bas.init(xx:)
    bb0(%0 : $*Bas<T>, %1 : $*T, %2 : $@thin Bas<T>.Type):
      %3 = alloc_box $<τ_0_0> { var Bas<τ_0_0> } <T>, var, name "self" // user: %4
      ....
    }` 
    
  4. 逃避闭包捕获时。

    Swift的闭包对所有局部变量都是通过引用来捕获的。 如CapturePromotion中所述,有些可能仍被放在栈中。

    CapturePromotion github.com/apple/swift…

  5. Inout参数

    让我们为foo(x :)生成一个接受inout参数的SIL:

    复制代码`func foo(x: inout Int) {
        x += 1
    }` 
    

    SIL输出显示foo(x :)正在装箱:

    复制代码`// foo(x:)
    sil hidden [ossa] @$s6boxing3foo1xySiz_tF : $@convention(thin) (@inout Int) -> () {
    // %0                                             // users: %7, %1
    bb0(%0 : $*Int):
    ...
    }` 
    

复制的成本

众所周知,大多数值类型都分配在栈上的,复制它们需要花费固定的时间。 复制操作速度快的原因是整数和浮点数等基本数据类型存储在CPU寄存器中,复制它们时无需访问RAM内存。 Swift的大多数可扩展类型(例如字符串,数组,集合和字典)都在写入时被复制了( copied on write)。 这意味着复制操作消耗很小。

由于引用类型不会直接存储其数据,因此我们在复制它们时只会产生引用计数成本。 引用计数的增加和减少不像整数变化那么简单,还需要额外的花销。因为堆可能同时被多个线程共享,为了保持原子性也需要额外花销。

关于ARC的讨论和堆分配对象的生命周期我们将来会开专题讨论,欢迎关注公众号:乐Coding获取最新文章。

当我们混合使用值和引用类型时,事情变得很有趣。 如果结构体或枚举包含引用类型时,它们需要的引用计数开销与他们包含引用类型的数量成正比。 下面的代码示例可以最好地证明这一点。 让我们创建一个拥有引用类型属性的结构体和一个具有引用类型属性的类,并打印他们的引用计数。

复制代码`class Ref {}

// Struct with references
struct MyStruct {
    let ref1 = Ref()
    let ref2 = Ref()
}

// Class with references
class MyClass {
    let ref1 = Ref()
    let ref2 = Ref()
}` 

让我们为MyStruct打印引用计数:

复制代码`let a = MyStruct()
let anotherA = a
print("self:", CFGetRetainCount(a as CFTypeRef))
print("ref1:", CFGetRetainCount(a.ref1))
print("ref1:", CFGetRetainCount(a.ref2))` 

打印结果:

复制代码`self: 1
ref1: 2
ref1: 2` 

再来看看MyClass:

复制代码`let b = MyClass()
let anotherB = b
print("self:", CFGetRetainCount(b))
print("ref1:", CFGetRetainCount(b.ref1))
print("ref1:", CFGetRetainCount(b.ref2))` 

打印:

复制代码`self: 2
ref1: 1
ref1: 1` 

输出显示MyStruct结构体产生了两倍的引用计数成本

标签:代码,复制,引用,类型,Swift,内存
From: https://www.cnblogs.com/cps666/p/17695273.html

相关文章

  • 类型判断为空
    1★★★例1:判断集合是否为空:2CollectionUtils.isEmpty(null);//控制台打印:true3CollectionUtils.isEmpty(newArrayList());//控制台打印:true4CollectionUtils.isEmpty({a,b});//控制台打印:false56★★★例2:判断集合是否不为空:7CollectionUtils.isNotE......
  • Redis7 10大数据类型(概述)
    一、概述二、数据类型1、redis字符串(String)String(字符串)string是redis最基本的类型,一个key对应一个value。string类型是二进制安全的,意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。string类型是Redis最基本的数据类型,一个redis中字符串value最多可以是51......
  • 3. Java数据类型
    Java数据类型:基本数据类型和引用数据类型前面我们提到 Java 语言是强类型语言,编译器存储在变量中的数值具有适当的数据类型。学习任何一种编程语言都要了解其数据类型,本文将详细介绍Java中的数据类型。Java语言支持的数据类型分为两种:基本数据类型(PrimitiveType)和引用数据......
  • 问题总结:浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals
    浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数,具体原理参考《码出高效》。源代码doublemaxValu......
  • swift5 可选值类型
    在Swift5中,可选值类型指的是一个值可能存在也可能不存在的数据类型。在Swift中,这种类型被表示为Optional<T>,其中T是底层数据类型。可选值类型在Swift中非常重要,因为它允许我们处理可能为空的值。通过使用可选值类型,我们可以避免在运行时出现空指针异常(NullPointerExceptions)的......
  • swift5 区间类型和数组转化
    在Swift5中,你可以使用区间(Range)类型来表示一系列连续的数字,并且可以使用一些内置的函数和方法将区间类型和数组(Array)之间进行转换。首先,我们来了解一下如何创建和使用区间类型。创建区间类型:swiftletrange=1...5//创建一个闭区间,包括1到5letopenRange=1..<5//创建......
  • swift switch case 的复杂用法
    Swift中的 switch 语句非常灵活,可以用于处理各种复杂的条件。下面是一些 switch 语句的复杂用法:匹配值和范围:你可以使用 case 子句来匹配特定的值,也可以匹配一个值范围。例如:swiftletnumber=3switchnumber{case1:print("Numberis1")case2,3,4:prin......
  • Iceberg从入门到精通系列之四:详细整理出Iceberg支持的字段类型,创建包含所有类型的表,并
    Iceberg从入门到精通系列之四:详细整理出Iceberg支持的字段类型,创建包含所有类型的表,并插入数据一、Iceberg表支持的字段类型二、创建包含所有类型的表三、插入数据一、Iceberg表支持的字段类型BOOLEANTINYINTSMALLINTINTEGERBIGINTFLOATDOUBLEDECIMALDATETIMESTAMPSTRINGUUIDFIXE......
  • dotnet 使用增量源代码生成技术的 Telescope 库导出程序集类型
    本文将告诉大家在dotnet里面使用免费完全开源的基于增量源代码生成技术的Telescope库,进行收集导出项目程序集里面指定类型。可以实现性能极高的指定类型收集,方便多模块对接入自己的业务框架此Telescope库是基于最友好的MIT协议开源的,免费开源可商用:https://github.com/do......
  • C语言中几种类型所占字节数
    类型16位32位64位char111shortint222int244unsignedint244float444double888long448longlong888unsignedlong448......